適用対象: 従業員テナント
外部テナント (詳細情報)
このチュートリアルでは、ネイティブ認証を使用してユーザーをサインアップする React シングルページ アプリを構築する方法について説明します。
このチュートリアルでは、次の操作を行います。
- React プロジェクトを作成します。
- アプリの UI コンポーネントを追加します。
- ユーザー名 (電子メール) とパスワードを使用してユーザーをサインアップするようにプロジェクトをセットアップします。
[前提条件]
- 「クイック スタート: ネイティブ認証 API を使用してサンプルの React シングルページ アプリケーションでユーザーをサインインする」の手順を完了します。 このクイックスタートでは、外部テナントを準備し、サンプルの React コード サンプルを実行する方法について説明します。
- Visual Studio Code または別のコードエディター。
- Node.js。
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 へのクライアント呼び出しを設定する
このセクションでは、ネイティブ認証を呼び出し、応答を処理する方法を定義します。
srcに クライアント というフォルダーを作成します。
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 コンポーネントを作成する
このアプリは、指定された名前、姓 (電子メール)、パスワード、ワンタイム パスコードなどのユーザーの詳細をユーザーから収集します。 そのため、アプリにはサインアップとワンタイム パスコード収集フォームが必要です。
src フォルダーの中に、/pages/SignUp という名前のフォルダーを作成します。
サインアップ フォームを作成、表示、送信するには、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> ); };
ワンタイム パスコード フォームを作成、表示、送信するには、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> ); };
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> ); };
このページには、成功メッセージと、ユーザーが正常にサインアップした後にサインイン ページに移動するためのボタンが表示されます。
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;
React アプリを正しく表示するには:
src/App.css ファイルを開き、
App-header
クラスに次のプロパティを追加します。min-height: 100vh;
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 プロキシ サーバーをセットアップする