適用於:灰色 X 符號的白色圓圈 工作人員租戶
外部租戶(深入了解)
在本教學課程中,您將瞭解如何建置 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 管理中心註冊之應用程式的 應用程式識別碼 (clientId)。BASE_API_URL
指向 跨原始資源分享 (CORS) 代理伺服器,我們將在這個教學系列中稍後設定。 原生驗證 API 不支援 CORS,因此我們在 React SPA 與原生驗證 API 之間設定 CORS Proxy 伺服器來管理 CORS 標頭。
設定 React 應用程式以呼叫原生驗證 API 並處理回應
若要完成驗證流程,例如註冊流程,使用原生驗證 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
屬性會顯示用戶端應用程式支援的驗證方法。 此應用程式使用電子郵件與密碼進行驗證,因此挑戰類型的值為 密碼 oob 重新導向。 深入了解 挑戰類型。
建立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/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 Proxy 伺服器來管理 CORS 標頭。