チュートリアル:ネイティブ認証 JavaScript SDK を使用して React シングルページ アプリにユーザーをサインアップする

適用対象: 次の内容が外部テナントに適用されることを示す白いチェック マーク記号が付いた緑の円。 外部テナント (詳細)

このチュートリアルでは、ネイティブ認証の JavaScript SDK を使用してユーザーをサインアップする React シングルページ アプリを構築する方法について説明します。

このチュートリアルでは、次の操作を行います。

  • React Next.js プロジェクトを作成します。
  • MSAL JS SDK を追加します。
  • アプリの UI コンポーネントを追加します。
  • ユーザーをサインアップするようにプロジェクトをセットアップします。

[前提条件]

React プロジェクトを作成して依存関係をインストールする

コンピューター内の任意の場所で、次のコマンドを実行して、reactspa 名前の新しい React プロジェクトを作成し、プロジェクト フォルダーに移動してからパッケージをインストールします。

npx create-next-app@latest
cd reactspa
npm install

コマンドを正常に実行すると、次の構造のアプリが作成されます。

spasample/
└──node_modules/
   └──...
└──public/
   └──...
└──src/
   └──app/
      └──favicon.ico
      └──globals.css
      └──page.tsx
      └──layout.tsx
└──postcss.config.mjs
└──package-lock.json
└──package.json
└──tsconfig.json
└──README.md
└──next-env.d.ts
└──next.config.ts

JavaScript SDK をプロジェクトに追加する

アプリでネイティブ認証 JavaScript SDK を使用するには、ターミナルを使用して次のコマンドを使用してインストールします。

npm install @azure/msal-browser

ネイティブ認証機能は、 azure-msal-browser ライブラリの一部です。 ネイティブ認証機能を使用するには、 @azure/msal-browser/custom-authからインポートします。 例えば次が挙げられます。

  import CustomAuthPublicClientApplication from "@azure/msal-browser/custom-auth";

クライアント構成の追加

このセクションでは、ネイティブ認証パブリック クライアント アプリケーションが SDK のインターフェイスと対話できるように構成を定義します。 これを行うには、 src/config/auth-config.ts という名前のファイルを作成し、次のコードを追加します。

export const customAuthConfig: CustomAuthConfiguration = {
  customAuth: {
    challengeTypes: ["password", "oob", "redirect"],
    authApiProxyUrl: "http://localhost:3001/api",
  },
  auth: {
    clientId: "Enter_the_Application_Id_Here",
    authority: "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com",
    redirectUri: "/",
    postLogoutRedirectUri: "/",
    navigateToLoginRequestUrl: false,
  },
  cache: {
    cacheLocation: "sessionStorage",
  },
  system: {
    loggerOptions: {
      loggerCallback: (
        level: LogLevel,
        message: string,
        containsPii: boolean
      ) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
        }
      },
    },
  },
};

コード内で、次のプレースホルダーを見つけてください。

  • Enter_the_Application_Id_Here をクリックし、先ほど登録したアプリのアプリケーション (クライアント) ID に置き換えます。

  • Enter_the_Tenant_Subdomain_Here 次に、Microsoft Entra 管理センターのテナント サブドメインに置き換えます。 たとえば、テナントのプライマリ ドメインが contoso.onmicrosoft.com の場合は、contoso を使用します。 テナント名がない場合は、テナントの詳細を読み取る方法を確認してください

UI コンポーネントを作成する

このアプリは、指定された名前、ユーザー名 (電子メール)、パスワード、ワンタイム パスコードなどのユーザーの詳細をユーザーから収集します。 そのため、アプリには、この情報を収集するフォームが必要です。

  1. src/app/sign-up という名前のフォルダーを src フォルダーに作成します。

  2. サインアップ/コンポーネント/InitialForm.tsx ファイルを作成し、サインアップ/コンポーネント/InitialForm.tsx からコードを貼り付けます。 このコンポーネントには、ユーザーサインアップ属性を収集するフォームが表示されます。

  3. サインアップ/コンポーネント/CodeForm.tsx ファイルを作成し、サインアップ/components/CodeForm.tsx からコードを貼り付けます。 このコンポーネントは、ユーザーに送信されたワンタイム パスコードを収集するフォームを表示します。 このフォームは、パスワードを含むメール、またはワンタイム パスコード認証方法を使用した電子メールに必要です。

  4. 認証方法が パスワード付きの電子メールの場合は、 サインアップ/コンポーネント/PasswordForm.tsx ファイルを作成し、 サインアップ/コンポーネント/PasswordForm.tsx からコードを貼り付けます。 このコンポーネントは、パスワード入力フォームを表示します。

フォームの操作を処理する

このセクションでは、ユーザーのサインアップの詳細やワンタイム パスコードやパスワードの送信など、サインアップ フォームの操作を処理するコードを追加します。

サインアップ フローのロジックを処理する sign-up/page.tsx を作成します。 このファイルでは、次の操作を行います。

  • 必要なコンポーネントをインポートし、状態に基づいて適切なフォームを表示します。 サインアップ/ページ.tsx の完全な例を参照してください。

        import { useEffect, useState } from "react";
        import { customAuthConfig } from "../../config/auth-config";
        import { styles } from "./styles/styles";
        import { InitialFormWithPassword } from "./components/InitialFormWithPassword";
    
        import {
        CustomAuthPublicClientApplication,
        ICustomAuthPublicClientApplication,
        SignUpCodeRequiredState,
        // Uncomment if your choice of authentication method is email with password
        // SignUpPasswordRequiredState,
        SignUpCompletedState,
        AuthFlowStateBase,
      } from "@azure/msal-browser/custom-auth";
    
        import { SignUpResultPage } from "./components/SignUpResult";
        import { CodeForm } from "./components/CodeForm";
        import { PasswordForm } from "./components/PasswordForm";    
    export default function SignUpPassword() {
        const [authClient, setAuthClient] = useState<ICustomAuthPublicClientApplication | null>(null);
        const [firstName, setFirstName] = useState("");
        const [lastName, setLastName] = useState("");
        const [jobTitle, setJobTitle] = useState("");
        const [city, setCity] = useState("");
        const [country, setCountry] = useState("");
        const [email, setEmail] = useState("");
        //Uncomment if your choice of authentication method is email with password
        //const [password, setPassword] = useState("");
        const [code, setCode] = useState("");
        const [error, setError] = useState("");
        const [loading, setLoading] = useState(false);
        const [signUpState, setSignUpState] = useState<AuthFlowStateBase | null>(null);
        const [loadingAccountStatus, setLoadingAccountStatus] = useState(true);
        const [isSignedIn, setSignInState] = useState(false);
    
        useEffect(() => {
            const initializeApp = async () => {
                const appInstance = await CustomAuthPublicClientApplication.create(customAuthConfig);
                setAuthClient(appInstance);
            };
            initializeApp();
        }, []);
    
        useEffect(() => {
            const checkAccount = async () => {
                if (!authClient) return;
                const accountResult = authClient.getCurrentAccount();
                if (accountResult.isCompleted()) {
                    setSignInState(true);
                }
                setLoadingAccountStatus(false);
            };
            checkAccount();
        }, [authClient]);
    
        const renderForm = () => {
            if (loadingAccountStatus) {
                return;
            }
            if (isSignedIn) {
                return (
                    <div style={styles.signed_in_msg}>Please sign out before processing the sign up.</div>
                );
            }
            if (signUpState instanceof SignUpCodeRequiredState) {
                return (
                    <CodeForm
                        onSubmit={handleCodeSubmit}
                        code={code}
                        setCode={setCode}
                        loading={loading}
                    />
                );
            } 
            //Uncomment the following block of code if your choice of authentication method is email with password 
            /*
            else if(signUpState instanceof SignUpPasswordRequiredState) {
                return <PasswordForm
                    onSubmit={handlePasswordSubmit}
                    password={password}
                    setPassword={setPassword}
                    loading={loading}
                />;
            }
            */
            else if (signUpState instanceof SignUpCompletedState) {
                return <SignUpResultPage />;
            } else {
                return (
                    <InitialForm
                        onSubmit={handleInitialSubmit}
                        firstName={firstName}
                        setFirstName={setFirstName}
                        lastName={lastName}
                        setLastName={setLastName}
                        jobTitle={jobTitle}
                        setJobTitle={setJobTitle}
                        city={city}
                        setCity={setCity}
                        country={country}
                        setCountry={setCountry}
                        email={email}
                        setEmail={setEmail}
                        loading={loading}
                    />
                );
            }
        }
        return (
            <div style={styles.container}>
                <h2 style={styles.h2}>Sign Up</h2>
                {renderForm()}
                {error && <div style={styles.error}>{error}</div>}
            </div>
        );
    }
    

    このコードでは、 クライアント構成を使用してネイティブ認証パブリック クライアント アプリのインスタンスも作成します。

    const appInstance = await CustomAuthPublicClientApplication.create(customAuthConfig);
    setAuthClient(appInstance);
    
  • 最初のフォーム送信を処理するには、次のコード スニペットを使用します。 コード スニペットを配置する場所については、 サインアップ/ページ.tsx の完全な例を参照してください。

    const handleInitialSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        if (!authClient) return;
    
        const attributes: UserAccountAttributes = {
            displayName: `${firstName} ${lastName}`,
            givenName: firstName,
            surname: lastName,
            jobTitle: jobTitle,
            city: city,
            country: country,
        };
    
        const result = await authClient.signUp({
            username: email,
            attributes
        });
        const state = result.state;
    
        if (result.isFailed()) {
            if (result.error?.isUserAlreadyExists()) {
                setError("An account with this email already exists");
            } else if (result.error?.isInvalidUsername()) {
                setError("Invalid uername");
            } else if (result.error?.isInvalidPassword()) {
                setError("Invalid password");
            } else if (result.error?.isAttributesValidationFailed()) {
                setError("Invalid attributes");
            } else if (result.error?.isMissingRequiredAttributes()) {
                setError("Missing required attributes");
            } else {
                setError(result.error?.errorData.errorDescription || "An error occurred while signing up");
            }
        } else {
            setSignUpState(state);
        }
        setLoading(false);
    };
    

    SDK のインスタンス メソッド signUp() 、サインアップ フローを開始します。

  • ワンタイム パスコードの送信を処理するには、次のコード スニペットを使用します。 コード スニペットを配置する場所については、 サインアップ/ページ.tsx の完全な例を参照してください。

    const handleCodeSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        try {
            if (signUpState instanceof SignUpCodeRequiredState) {
                const result = await signUpState.submitCode(code);
                if (result.error) {
                    if (result.error.isInvalidCode()) {
                        setError("Invalid verification code");
                    } else {
                        setError("An error occurred while verifying the code");
                    }
                    return;
                }
                if (result.state instanceof SignUpCompletedState) {
                    setSignUpState(result.state);
                }
            }
        } catch (err) {
            setError("An unexpected error occurred");
            console.error(err);
        } finally {
            setLoading(false);
        }
    };
    
  • パスワードの送信を処理するには、次のコード スニペットを使用します。 認証方法の選択がパスワード 付きの電子メールである場合は、パスワードの送信を処理します。 コード スニペットを配置する場所については、 サインアップ/ページ.tsx の完全な例を参照してください。

        const handlePasswordSubmit = async (e: React.FormEvent) => {
            e.preventDefault();
            setError("");
            setLoading(true);
    
            if (signUpState instanceof SignUpPasswordRequiredState) {
                const result = await signUpState.submitPassword(password);
                const state = result.state;
    
                if (result.isFailed()) {
                    if (result.error?.isInvalidPassword()) {
                        setError("Invalid password");
                    } else {
                        setError(result.error?.errorData.errorDescription || "An error occurred while submitting the password");
                    }
                } else {
                    setSignUpState(state);
                }
            }
    
            setLoading(false);
        };
    
  • signUpState instanceof SignUpCompletedStateを使用して、ユーザーがサインアップされ、フローが完了したことを示します。 サインアップ/ページ.tsx の完全な例を参照してください。

    if (signUpState instanceof SignUpCompletedState) {
        return <SignUpResultPage/>;
    }
    

サインアップ時にユーザー名 (エイリアス) を収集する

ユーザーがメールに加えてユーザー名 (エイリアス) でサインアップできるようにすることができます。 ユーザー名 (エイリアス) は、顧客 ID、アカウント番号、または選択した別の値などの代替サインイン識別子です。

サインアップ時には、プライマリ識別子としてユーザー名 (電子メール) が常に必要であり、ユーザー名 (エイリアス) によって置き換えられることはありません。 既定では、ユーザー名 (エイリアス) は省略可能ですが、管理者は必要に応じて構成できます。 アプリは常にユーザー名 (電子メール) を収集し、エイリアスを電子メールと共に属性として収集します。 サインイン時に、ユーザーはユーザー名 (電子メール) またはユーザー名 (エイリアス) を使用してサインインできます。 Username 属性をオプションまたは必須として構成する方法については、「ユーザー入力の種類とページ レイアウトを構成する」を参照してください。

サインアップ時にユーザー名 (エイリアス) を収集するには:

  1. サインアップ ユーザー フローで Username 組み込みユーザー属性が有効になっていることを確認します。 手順については、「 サインイン識別子ポリシーでユーザー名を有効にする」を参照してください。

  2. サインアップページにflatUsername状態を追加し、signUp() に渡す UserAccountAttributesflatusername 属性を含めます。

    const [flatUsername, setFlatUsername] = useState("");
    
    const attributes: UserAccountAttributes = {
        displayName: `${firstName} ${lastName}`,
        //...
        flatusername: flatUsername,
    };
    
  3. InitialForm.tsx にエイリアス入力を追加して、ユーザー名 (エイリアス) の値を収集します。

    <input
        type="text"
        placeholder="Username (alias)"
        value={flatUsername}
        onChange={(e) => setFlatUsername(e.target.value)}
        style={styles.input}
    />
    
  4. ユーザー名 (エイリアス) に関連するエラーを処理します。

    • result.error?.isUserAlreadyExists() には、重複する電子メール または 重複するユーザー名 (エイリアス) が含まれます。 それに応じてメッセージを更新します。たとえば、 このメールまたはユーザー名を持つアカウントが既に存在します
    • 無効なユーザー名 (エイリアス) は、result.error?.isAttributesValidationFailed()ではなくresult.error?.isInvalidUsername()によって表示されます。 ユーザー名固有のメッセージを表示するには、このメソッドで分岐します。

サインアップ エラーを処理する

サインアップ中、すべてのアクションが成功するわけではありません。 たとえば、ユーザーが既に使用されているメール アドレスでサインアップしようとしたり、無効なメールのワンタイム パスコードを送信したりすることがあります。 次の操作を行う際は、エラーを適切に処理してください。

  • signUp() メソッドでサインアップ フローを開始します。

  • submitCode() メソッドでワンタイム パスコードを送信します。

  • submitPassword()メソッドでパスワードを送信します。 このエラーは、選択したサインアップ フローが電子メールとパスワードで行われる場合に処理します。

signUp() メソッドの結果として発生する可能性があるエラーの 1 つがresult.error?.isRedirectRequired()。 このシナリオは、認証フローを完了するのにネイティブ認証だけでは十分ではない場合に発生します。 たとえば、承認サーバーにクライアントが提供できない機能が必要な場合です。 ネイティブ認証 Web フォールバックの詳細と、React アプリで Web フォールバックをサポートする方法について説明します。

任意: サインアップ後に自動的にサインインする

ユーザーが正常にサインアップしたら、新しいサインイン フローを開始せずに、アプリに直接サインインできます。 これを行うには、次のコード スニペットを使用します。 サインアップ/ページ.tsx の完全な例を参照してください。

if (signUpState instanceof SignUpCompletedState) {
    const result = await signUpState.signIn();
    const state = result.state;
    if (result.isFailed()) {
        setError(result.error?.errorData?.errorDescription || "An error occurred during auto sign-in");
    }
    
    if (result.isCompleted()) {
        setData(result.data);
        setSignUpState(state);
    }
}

アプリを実行してテストする

  1. ターミナル ウィンドウを開き、アプリのルート フォルダーに移動します。

    cd reactspa
    
  2. CORS プロキシ サーバーを起動するには、ターミナルで次のコマンドを実行します。

    npm run cors
    
  3. React アプリを起動するには、別のターミナル ウィンドウを開き、次のコマンドを実行します。

    cd reactspa
    npm start
    
  4. Web ブラウザーを開き、http://localhost:3000/sign-upに移動します。 サインアップ フォームが表示されます。

  5. アカウントにサインアップするには、詳細を入力し、[ 続行 ] ボタンを選択し、プロンプトに従います。

次に、React アプリを更新してユーザーをサインインさせたり、ユーザーのパスワードをリセットしたりできます。

next.config.js で poweredByHeader を false に設定する

既定では、 x-powered-by ヘッダーは HTTP 応答に含まれており、アプリケーションが Next.jsを使用していることを示します。 ただし、セキュリティまたはカスタマイズの理由から、このヘッダーを削除または変更することが必要になる場合があります。

const nextConfig: NextConfig = {
  poweredByHeader: false,
  /* other config options here */
};

次のステップ