教程:使用本机身份验证将用户注册到 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 标头。

设置 React 应用以调用本机身份验证 API 并处理响应

若要使用本机身份验证 API 完成身份验证流(例如注册流),应用会发出调用并处理响应。 例如,应用启动注册流并等待响应,然后提交用户属性并再次等待,直到用户成功注册。

设置对原生身份验证 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 属性显示客户端应用支持的身份验证方法。 此应用使用电子邮件加密码进行登录,因此挑战类型值为“密码 oob 重定向”。 详细了解挑战类型

创建 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 标头。

后续步骤