다음을 통해 공유


자습서: 네이티브 인증(미리 보기)을 사용하여 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 속성은 클라이언트 앱이 지원하는 인증 방법을 보여 줍니다. 이 앱은 이메일과 비밀번호를 사용하여 로그인을 하며, 챌린지 유형 값은 암호 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 프록시 서버를 설정해야 합니다.

다음 단계