次の方法で共有


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

適用対象: 灰色の X 記号がある白い円。 従業員テナント 白いチェック マーク記号がある緑の円。 外部テナント (詳細情報)

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

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

  • React プロジェクトを作成します。
  • アプリの UI コンポーネントを追加します。
  • ユーザー名 (電子メール) とパスワードを使用してユーザーをサインアップするようにプロジェクトをセットアップします。

[前提条件]

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

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

npm config set legacy-peer-deps true
npx create-react-app reactspa --template typescript
cd reactspa
npm install ajv
npm installreact-router-dom
npm install

アプリの構成ファイルを追加する

src/config.jsという名前ファイルを作成し、次のコードを追加します。

// App Id obatained from the Microsoft Entra portal 
export const CLIENT_ID = "Enter_the_Application_Id_Here";

// URL of the CORS proxy server
const BASE_API_URL = `http://localhost:3001/api`;

// Endpoints URLs for Native Auth APIs
export const ENV = {
    urlSignupStart: `${BASE_API_URL}/signup/v1.0/start`,
    urlSignupChallenge: `${BASE_API_URL}/signup/v1.0/challenge`,
    urlSignupContinue: `${BASE_API_URL}/signup/v1.0/continue`,
}
  • Enter_the_Application_Id_Here 値を見つけて、Microsoft Entra 管理センターに登録したアプリの アプリケーション ID (clientId) に置き換えます。

  • BASE_API_URL は、このチュートリアル シリーズの後半で設定した、クロスオリジン リソース共有 (CORS) プロキシ サーバーを指しています。 ネイティブ認証 API は CORS をサポートしていないため、REACT SPA とネイティブ認証 API の間に CORS プロキシ サーバーを設定して CORS ヘッダーを管理します。

ネイティブ認証 API を呼び出して応答を処理するように React アプリを設定する

ネイティブ認証 API を使用してサインアップ フローなどの認証フローを完了するために、アプリは calla dn で応答を処理します。 たとえば、アプリはサインアップ フローを開始し、応答を待ってからユーザー属性を送信し、ユーザーが正常にサインアップされるまでもう一度待機します。

ネイティブ認証 API へのクライアント呼び出しを設定する

このセクションでは、ネイティブ認証を呼び出し、応答を処理する方法を定義します。

  1. srcクライアント というフォルダーを作成します。

  2. scr/client/RequestClient.ts という名前ファイルを作成し、次のコード スニペットを追加します。

    import { ErrorResponseType } from "./ResponseTypes";
    
    export const postRequest = async (url: string, payloadExt: any) => {
    const body = new URLSearchParams(payloadExt as any);
    
    const response = await fetch(url, {
        method: "POST",
        headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        },
        body,
    });
    
    if (!response.ok) {
        try {
        const errorData: ErrorResponseType = await response.json();
        throw errorData;
        } catch (jsonError) {
        const errorData = {
            error: response.status,
            description: response.statusText,
            codes: [],
            timestamp: "",
            trace_id: "",
            correlation_id: "",
        };
        throw errorData;
        }
    }
    
    return await response.json();
    };
    

    このコードでは、アプリがネイティブ認証 API を呼び出し、応答を処理する方法を定義します。 アプリが認証フローを開始する必要がある場合は常に、URL とペイロード データを指定して postRequest 関数を使用します。

アプリがネイティブ認証 API に対して行う呼び出しの種類を定義する

サインアップ フロー中に、アプリはネイティブ認証 API を複数回呼び出します。

これらの呼び出しを定義するには、scr/client/RequestTypes.ts という名前のファイルを作成し、次のコード スニペットを追加します。

    //SignUp 
    export interface SignUpStartRequest {
        client_id: string;
        username: string;
        challenge_type: string;
        password?: string;
        attributes?: Object;
    }
    
    export interface SignUpChallengeRequest {
        client_id: string;
        continuation_token: string;
        challenge_type?: string;
    }
    
    export interface SignUpFormPassword {
        name: string;
        surname: string;
        username: string;
        password: string;
    }
    
    //OTP
    export interface ChallengeForm {
        continuation_token: string;
        oob?: string;
        password?: string;
    }

ネイティブ認証 API からアプリが受け取る応答の種類を定義する

サインアップ操作のネイティブ認証 API からアプリが受信できる応答の種類を定義するには、src/client/ResponseTypes.ts という名前のファイル作成し、次のコード スニペットを追加します。

    export interface SuccessResponseType {
    continuation_token?: string;
    challenge_type?: string;
    }
    
    export interface ErrorResponseType {
        error: string;
        error_description: string;
        error_codes: number[];
        timestamp: string;
        trace_id: string;
        correlation_id: string;
    }
        
    export interface ChallengeResponse {
        binding_method: string;
        challenge_channel: string;
        challenge_target_label: string;
        challenge_type: string;
        code_length: number;
        continuation_token: string;
        interval: number;
    }

サインアップ要求を処理する

このセクションでは、サインアップ フロー要求を処理するコードを追加します。 これらの要求の例としては、サインアップ フローの開始、認証方法の選択、ワンタイム パスコードの送信があります。

これを行うには、src/client/SignUpService.ts という名前のファイルを作成し、次のコード スニペットを追加します。

import { CLIENT_ID, ENV } from "../config";
import { postRequest } from "./RequestClient";
import { ChallengeForm, SignUpChallengeRequest, SignUpFormPassword, SignUpStartRequest } from "./RequestTypes";
import { ChallengeResponse } from "./ResponseTypes";

//handle start a sign-up flow
export const signupStart = async (payload: SignUpFormPassword) => {
const payloadExt: SignUpStartRequest = {
    attributes: JSON.stringify({
    given_name: payload.name,
    surname: payload.surname,
    }),
    username: payload.username,
    password: payload.password,
    client_id: CLIENT_ID,
    challenge_type: "password oob redirect",
};

return await postRequest(ENV.urlSignupStart, payloadExt);
};

//handle selecting an authentication method
export const signupChallenge = async (payload: ChallengeForm):Promise<ChallengeResponse> => {
    const payloadExt: SignUpChallengeRequest = {
        client_id: CLIENT_ID,
        challenge_type: "password oob redirect",
        continuation_token: payload.continuation_token,
    };

    return await postRequest(ENV.urlSignupChallenge, payloadExt);
};

//handle submit one-time passcode
export const signUpSubmitOTP = async (payload: ChallengeForm) => {
    const payloadExt = {
        client_id: CLIENT_ID,
        continuation_token: payload.continuation_token,
        oob: payload.oob,
        grant_type: "oob",
    };

    return await postRequest(ENV.urlSignupContinue, payloadExt);
};

challenge_type プロパティは、クライアント アプリがサポートする認証方法を示します。 このアプリ サインでは電子メールとパスワードが使用されるため、チャレンジの種類の値は password oob redirect です。 のチャレンジの種類について詳しく読む。

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

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

  1. src フォルダーの中に、/pages/SignUp という名前のフォルダーを作成します。

  2. サインアップ フォームを作成、表示、送信するには、src/pages/SignUp/SignUp.tsx ファイルを作成し、次のコードを追加します。

        import React, { useState } from 'react';
        import { signupChallenge, signupStart } from '../../client/SignUpService';
        import { useNavigate } from 'react-router-dom';
        import { ErrorResponseType } from "../../client/ResponseTypes";
    
        export const SignUp: React.FC = () => {
            const [name, setName] = useState<string>('');
            const [surname, setSurname] = useState<string>('');
            const [email, setEmail] = useState<string>('');
            const [error, setError] = useState<string>('');
            const [isLoading, setIsloading] = useState<boolean>(false);
            const navigate = useNavigate();
            const validateEmail = (email: string): boolean => {
              const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
              return re.test(String(email).toLowerCase());
            };
    
            const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
              e.preventDefault();
              if (!name || !surname || !email) {
                setError('All fields are required');
                return;
              }
              if (!validateEmail(email)) {
                setError('Invalid email format');
                return;
              }
              setError('');
              try {
                setIsloading(true);
                const res1 = await signupStart({ name, surname, username: email, password });
                const res2 = await signupChallenge({ continuation_token: res1.continuation_token });
                navigate('/signup/challenge', { state: { ...res2} });
              } catch (err) {
                setError("An error occurred during sign up " + (err as ErrorResponseType).error_description);
              } finally {
                setIsloading(false);
              }
            };
    
            return (
              <div className="sign-up-form">
                <form onSubmit={handleSubmit}>
                  <h2>Sign Up</h2>
                  <div className="form-group">
                    <label>Name:</label>
                    <input
                      type="text"
                      value={name}
                      onChange={(e) => setName(e.target.value)}
                      required
                    />
                  </div>
                  <div className="form-group">
                    <label>Last Name:</label>
                    <input
                      type="text"
                      value={surname}
                      onChange={(e) => setSurname(e.target.value)}
                      required
                    />
                  </div>
                  <div className="form-group">
                    <label>Email:</label>
                    <input
                      type="email"
                      value={email}
                      onChange={(e) => setEmail(e.target.value)}
                      required
                    />
                  </div>
                  {error && <div className="error">{error}</div>}
                  {isLoading && <div className="warning">Sending request...</div>}
                  <button type="submit">Sign Up</button>
                </form>
              </div>
            );
          };
    
  3. ワンタイム パスコード フォームを作成、表示、送信するには、src/pages/signup/SignUpChallenge.tsx ファイルを作成し、次のコードを追加します。

    import React, { useState } from "react";
    import { useNavigate, useLocation } from "react-router-dom";
    import { signUpSubmitOTP } from "../../client/SignUpService";
    import { ErrorResponseType } from "../../client/ResponseTypes";
    
    export const SignUpChallenge: React.FC = () => {
      const { state } = useLocation();
      const navigate = useNavigate();
      const { challenge_target_label, challenge_type, continuation_token, code_length } = state;
    
      const [code, setCode] = useState<string>("");
      const [error, setError] = useState<string>("");
      const [isLoading, setIsloading] = useState<boolean>(false);
    
      const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (!code) {
          setError("All fields are required");
          return;
        }
    
        setError("");
        try {
          setIsloading(true);
          const res = await signUpSubmitOTP({ continuation_token, oob: code });
          navigate("/signup/completed");
        } catch (err) {
          setError("An error occurred during sign up " + (err as ErrorResponseType).error_description);
        } finally {
          setIsloading(false);
        }
      };
    
      return (
        <div className="sign-up-form">
          <form onSubmit={handleSubmit}>
            <h2>Insert your one time code received at {challenge_target_label}</h2>
            <div className="form-group">
              <label>Code:</label>
              <input maxLength={code_length} type="text" value={code} onChange={(e) => setCode(e.target.value)} required />
            </div>
            {error && <div className="error">{error}</div>}
            {isLoading && <div className="warning">Sending request...</div>}
            <button type="submit">Sign Up</button>
          </form>
        </div>
      );
    };
    
  4. src/pages/signup/SignUpCompleted.tsx ファイルを作成し、次のコードを追加します。

    import React from 'react';
    import { Link } from 'react-router-dom';
    
    export const SignUpCompleted: React.FC = () => {
      return (
        <div className="sign-up-completed">
          <h2>Sign Up Completed</h2>
          <p>Your sign-up process is complete. You can now log in.</p>
          <Link to="/signin" className="login-link">Go to Login</Link>
        </div>
      );
    };
    

    このページには、成功メッセージと、ユーザーが正常にサインアップした後にサインイン ページに移動するためのボタンが表示されます。

  5. src/App.tsx ファイルを開き、その内容を次のコードに置き換えます。

    import React from "react";
    import { BrowserRouter, Link } from "react-router-dom";
    import "./App.css";
    import { AppRoutes } from "./AppRoutes";
    
    function App() {
      return (
        <div className="App">
          <BrowserRouter>
            <header>
              <nav>
                <ul>
                  <li>
                    <Link to="/signup">Sign Up</Link>
                  </li>
                  <li>
                    <Link to="/signin">Sign In</Link>
                  </li>
                  <li>
                    <Link to="/reset">Reset Password</Link>
                  </li>
                </ul>
              </nav>
            </header>
            <AppRoutes />
          </BrowserRouter>
        </div>
      );
    }
    
    export default App;
    
  6. React アプリを正しく表示するには:

    1. src/App.css ファイルを開き、App-header クラスに次のプロパティを追加します。

      min-height: 100vh;
      
    2. src/Index.css ファイルを開いて、中身を src/index.css のコードに置き換えます

アプリ ルートを追加する

src/AppRoutes.tsx という名前のファイルを作成し、次のコードを追加します。

import { Route, Routes } from "react-router-dom";
import { SignUp } from "./pages/SignUp/SignUp";
import { SignUpChallenge } from "./pages/SignUp/SignUpChallenge";
import { SignUpCompleted } from "./pages/SignUp/SignUpCompleted";

export const AppRoutes = () => {
  return (
    <Routes>
      <Route path="/" element={<SignUp />} />
      <Route path="/signup" element={<SignUp />} />
      <Route path="/signup/challenge" element={<SignUpChallenge />} />
      <Route path="/signup/completed" element={<SignUpCompleted />} />
   
    </Routes>
  );
};

この時点で、React アプリはネイティブ認証 API にサインアップ要求を送信できますが、CORS ヘッダーを管理するように CORS プロキシ サーバーを設定する必要があります。

次のステップ

チュートリアル: ネイティブ認証 の CORS ヘッダーを管理するように CORS プロキシ サーバーをセットアップする