다음을 통해 공유


자습서: 네이티브 인증 JavaScript SDK(미리 보기)를 사용하여 React 단일 페이지 앱에 사용자 등록

적용 대상: 외부 테넌트에 다음 내용이 적용되었음을 나타내는 흰색 확인 표시 기호가 있는 녹색 원입니다. 외부 테넌트(자세한 정보)

이 자습서에서는 네이티브 인증의 JavaScript SDK를 사용하여 사용자를 등록하는 React 단일 페이지 앱을 빌드하는 방법을 알아봅니다.

이 자습서에서는 다음을 수행합니다.

  • React Next.js 프로젝트를 만듭니다.
  • MSAL JS SDK를 추가합니다.
  • 앱의 UI 구성 요소를 추가합니다.
  • 사용자를 등록하도록 프로젝트를 설치합니다.

필수 조건

React 프로젝트 만들기 및 종속성 설치

컴퓨터에서 선택한 위치에서 다음 명령을 실행하여 reactspa 이름이새 React 프로젝트를 만들고 프로젝트 폴더로 이동한 다음 패키지를 설치합니다.

npx create-next-app@latest
cd reactspa
npm install

명령을 성공적으로 실행한 후에는 다음과 같은 구조의 앱이 있어야 합니다.

spasample/
└──node_modules/
   └──...
└──public/
   └──...
└──src/
   └──app/
      └──favicon.ico
      └──globals.css
      └──page.tsx
      └──layout.tsx
└──postcss.config.mjs
└──package-lock.json
└──package.json
└──tsconfig.json
└──README.md
└──next-env.d.ts
└──next.config.ts

프로젝트에 JavaScript SDK 추가

앱에서 네이티브 인증 JavaScript SDK를 사용하려면 터미널을 사용하여 다음 명령을 사용하여 설치합니다.

npm install @azure/msal-browser

네이티브 인증 기능은 라이브러리의 azure-msal-browser 일부입니다. 네이티브 인증 기능을 사용하려면 .@azure/msal-browser/custom-auth 다음은 그 예입니다.

  import CustomAuthPublicClientApplication from "@azure/msal-browser/custom-auth";

클라이언트 구성 추가

이 섹션에서는 네이티브 인증 공용 클라이언트 애플리케이션에 대한 구성을 정의하여 SDK의 인터페이스와 상호 작용할 수 있도록 합니다. 이렇게 하려면 src/config/auth-config.ts 파일을 만든 다음, 다음 코드를 추가합니다.

export const customAuthConfig: CustomAuthConfiguration = {
  customAuth: {
    challengeTypes: ["password", "oob", "redirect"],
    authApiProxyUrl: "http://localhost:3001/api",
  },
  auth: {
    clientId: "Enter_the_Application_Id_Here",
    authority: "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com",
    redirectUri: "/",
    postLogoutRedirectUri: "/",
    navigateToLoginRequestUrl: false,
  },
  cache: {
    cacheLocation: "sessionStorage",
  },
  system: {
    loggerOptions: {
      loggerCallback: (
        level: LogLevel,
        message: string,
        containsPii: boolean
      ) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
        }
      },
    },
  },
};

코드에서 자리 표시자를 찾습니다.

  • Enter_the_Application_Id_Here 그런 다음 이전에 등록한 앱의 애플리케이션(클라이언트) ID로 바꿉니다.

  • Enter_the_Tenant_Subdomain_Here 그런 다음 Microsoft Entra 관리 센터의 테넌트 하위 도메인으로 바꿉니다. 예를 들어, 테넌트 기본 도메인이 contoso.onmicrosoft.com인 경우 contoso를 사용합니다. 테넌트 이름이 없는 경우 테넌트 세부 정보를 읽는 방법을 알아봅니다.

UI 구성 요소 만들기

이 앱은 지정된 이름, 사용자 이름(이메일), 암호 및 사용자로부터 일회성 암호와 같은 사용자 세부 정보를 수집합니다. 따라서 앱에는 이 정보를 수집하는 양식이 있어야 합니다.

  1. src 폴더에 src/app/sign-up이라는 폴더를 만듭니다.

  2. 등록/구성 요소/InitialForm.tsx 파일을 만든 다음, 등록/구성 요소/InitialForm.tsx의 코드를 붙여넣습니다. 이 구성 요소는 사용자 등록 특성을 수집하는 양식을 표시합니다.

  3. 등록/구성 요소/CodeForm.tsx 파일을 만든 다음, 등록/구성 요소/CodeForm.tsx의 코드를 붙여넣습니다. 이 구성 요소는 사용자에게 보낸 일회성 암호를 수집하는 양식을 표시합니다. 암호가 있는 전자 메일이나 일회용 암호 인증 방법을 사용하는 전자 메일에 이 양식이 필요합니다.

  4. 선택한 인증 방법이 암호가 있는 전자 메일인 경우 등록/구성 요소/PasswordForm.tsx 파일을 만든 다음 등록/구성 요소/PasswordForm.tsx의 코드를 붙여넣습니다. 이 구성 요소는 암호 입력 양식을 표시합니다.

양식 상호 작용 처리

이 섹션에서는 사용자 등록 세부 정보 제출, 일회성 암호 또는 암호 제출과 같은 등록 양식 상호 작용을 처리하는 코드를 추가합니다.

등록/page.tsx를 만들어 등록 흐름에 대한 논리를 처리합니다. 이 파일에서 다음을 확인할 수 있습니다.

  • 필요한 구성 요소를 가져오고 상태에 따라 적절한 양식을 표시합니다. 등록/page.tsx의 전체 예제를 참조하세요.

        import { useEffect, useState } from "react";
        import { customAuthConfig } from "../../config/auth-config";
        import { styles } from "./styles/styles";
        import { InitialFormWithPassword } from "./components/InitialFormWithPassword";
    
        import {
        CustomAuthPublicClientApplication,
        ICustomAuthPublicClientApplication,
        SignUpCodeRequiredState,
        // Uncomment if your choice of authentication method is email with password
        // SignUpPasswordRequiredState,
        SignUpCompletedState,
        AuthFlowStateBase,
      } from "@azure/msal-browser/custom-auth";
    
        import { SignUpResultPage } from "./components/SignUpResult";
        import { CodeForm } from "./components/CodeForm";
        import { PasswordForm } from "./components/PasswordForm";    
    export default function SignUpPassword() {
        const [authClient, setAuthClient] = useState<ICustomAuthPublicClientApplication | null>(null);
        const [firstName, setFirstName] = useState("");
        const [lastName, setLastName] = useState("");
        const [jobTitle, setJobTitle] = useState("");
        const [city, setCity] = useState("");
        const [country, setCountry] = useState("");
        const [email, setEmail] = useState("");
        //Uncomment if your choice of authentication method is email with password
        //const [password, setPassword] = useState("");
        const [code, setCode] = useState("");
        const [error, setError] = useState("");
        const [loading, setLoading] = useState(false);
        const [signUpState, setSignUpState] = useState<AuthFlowStateBase | null>(null);
        const [loadingAccountStatus, setLoadingAccountStatus] = useState(true);
        const [isSignedIn, setSignInState] = useState(false);
    
        useEffect(() => {
            const initializeApp = async () => {
                const appInstance = await CustomAuthPublicClientApplication.create(customAuthConfig);
                setAuthClient(appInstance);
            };
            initializeApp();
        }, []);
    
        useEffect(() => {
            const checkAccount = async () => {
                if (!authClient) return;
                const accountResult = authClient.getCurrentAccount();
                if (accountResult.isCompleted()) {
                    setSignInState(true);
                }
                setLoadingAccountStatus(false);
            };
            checkAccount();
        }, [authClient]);
    
        const renderForm = () => {
            if (loadingAccountStatus) {
                return;
            }
            if (isSignedIn) {
                return (
                    <div style={styles.signed_in_msg}>Please sign out before processing the sign up.</div>
                );
            }
            if (signUpState instanceof SignUpCodeRequiredState) {
                return (
                    <CodeForm
                        onSubmit={handleCodeSubmit}
                        code={code}
                        setCode={setCode}
                        loading={loading}
                    />
                );
            } 
            //Uncomment the following block of code if your choice of authentication method is email with password 
            /*
            else if(signUpState instanceof SignUpPasswordRequiredState) {
                return <PasswordForm
                    onSubmit={handlePasswordSubmit}
                    password={password}
                    setPassword={setPassword}
                    loading={loading}
                />;
            }
            */
            else if (signUpState instanceof SignUpCompletedState) {
                return <SignUpResultPage />;
            } else {
                return (
                    <InitialForm
                        onSubmit={handleInitialSubmit}
                        firstName={firstName}
                        setFirstName={setFirstName}
                        lastName={lastName}
                        setLastName={setLastName}
                        jobTitle={jobTitle}
                        setJobTitle={setJobTitle}
                        city={city}
                        setCity={setCity}
                        country={country}
                        setCountry={setCountry}
                        email={email}
                        setEmail={setEmail}
                        loading={loading}
                    />
                );
            }
        }
        return (
            <div style={styles.container}>
                <h2 style={styles.h2}>Sign Up</h2>
                {renderForm()}
                {error && <div style={styles.error}>{error}</div>}
            </div>
        );
    }
    

    또한 이 코드는 클라이언트 구성을 사용하여 네이티브 인증 공용 클라이언트 앱의 인스턴스를 만듭니다.

    const appInstance = await CustomAuthPublicClientApplication.create(customAuthConfig);
    setAuthClient(appInstance);
    
  • 초기 양식 제출을 처리하려면 다음 코드 조각을 사용합니다. 코드 조각을 배치할 위치를 알아보려면 등록/page.tsx 에서 전체 예제를 참조하세요.

    const handleInitialSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        if (!authClient) return;
    
        const attributes: UserAccountAttributes = {
            displayName: `${firstName} ${lastName}`,
            givenName: firstName,
            surname: lastName,
            jobTitle: jobTitle,
            city: city,
            country: country,
        };
    
        const result = await authClient.signUp({
            username: email,
            attributes
        });
        const state = result.state;
    
        if (result.isFailed()) {
            if (result.error?.isUserAlreadyExists()) {
                setError("An account with this email already exists");
            } else if (result.error?.isInvalidUsername()) {
                setError("Invalid uername");
            } else if (result.error?.isInvalidPassword()) {
                setError("Invalid password");
            } else if (result.error?.isAttributesValidationFailed()) {
                setError("Invalid attributes");
            } else if (result.error?.isMissingRequiredAttributes()) {
                setError("Missing required attributes");
            } else {
                setError(result.error?.errorData.errorDescription || "An error occurred while signing up");
            }
        } else {
            setSignUpState(state);
        }
        setLoading(false);
    };
    

    SDK의 인스턴스 메서드는 signUp() 등록 흐름을 시작합니다.

  • 일회성 암호 제출을 처리하려면 다음 코드 조각을 사용합니다. 코드 조각을 배치할 위치를 알아보려면 등록/page.tsx 에서 전체 예제를 참조하세요.

    const handleCodeSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        try {
            if (signUpState instanceof SignUpCodeRequiredState) {
                const result = await signUpState.submitCode(code);
                if (result.error) {
                    if (result.error.isInvalidCode()) {
                        setError("Invalid verification code");
                    } else {
                        setError("An error occurred while verifying the code");
                    }
                    return;
                }
                if (result.state instanceof SignUpCompletedState) {
                    setSignUpState(result.state);
                }
            }
        } catch (err) {
            setError("An unexpected error occurred");
            console.error(err);
        } finally {
            setLoading(false);
        }
    };
    
  • 암호 제출을 처리하려면 다음 코드 조각을 사용합니다. 선택한 인증 방법이 암호 가 있는 전자 메일인 경우 암호 제출을 처리합니다. 코드 조각을 배치할 위치를 알아보려면 등록/page.tsx 에서 전체 예제를 참조하세요.

        const handlePasswordSubmit = async (e: React.FormEvent) => {
            e.preventDefault();
            setError("");
            setLoading(true);
    
            if (signUpState instanceof SignUpPasswordRequiredState) {
                const result = await signUpState.submitPassword(password);
                const state = result.state;
    
                if (result.isFailed()) {
                    if (result.error?.isInvalidPassword()) {
                        setError("Invalid password");
                    } else {
                        setError(result.error?.errorData.errorDescription || "An error occurred while submitting the password");
                    }
                } else {
                    setSignUpState(state);
                }
            }
    
            setLoading(false);
        };
    
  • signUpState instanceof SignUpCompletedState 이 값을 사용하여 사용자가 등록되었고 흐름이 완료되었음을 나타냅니다. 등록/page.tsx에서 전체 예제를 참조하세요.

    if (signUpState instanceof SignUpCompletedState) {
        return <SignUpResultPage/>;
    }
    

등록 오류 처리

등록하는 동안 모든 작업이 성공하는 것은 아닙니다. 예를 들어 사용자가 이미 사용한 전자 메일 주소로 등록하거나 잘못된 전자 메일 일회용 암호를 제출하려고 할 수 있습니다. 다음과 같은 경우 오류를 올바르게 처리해야 합니다.

  • 메서드에서 등록 흐름을 시작합니다 signUp() .

  • 메서드에서 일회성 암호를 제출합니다 submitCode() .

  • 메서드에서 암호를 제출합니다 submitPassword() . 등록 흐름이 전자 메일 및 암호로 선택한 경우 이 오류를 처리합니다.

메서드signUp()에서 result.error?.isRedirectRequired() 발생할 수 있는 오류 중 하나는 . 이 시나리오는 네이티브 인증이 인증 흐름을 완료하기에 충분하지 않은 경우에 발생합니다. 예를 들어 권한 부여 서버에 클라이언트가 제공할 수 없는 기능이 필요한 경우입니다. 네이티브 인증 웹 대체 및 React 앱에서 웹 대체를 지원하는 방법에 대해 자세히 알아봅니다.

선택 사항: 등록 후 자동으로 사용자 로그인

사용자가 성공적으로 등록한 후에는 새 로그인 흐름을 시작하지 않고 앱에 직접 로그인할 수 있습니다. 이렇게 하려면 다음 코드 조각을 사용합니다. 등록/page.tsx에서 전체 예제를 참조하세요.

if (signUpState instanceof SignUpCompletedState) {
    const result = await signUpState.signIn();
    const state = result.state;
    if (result.isFailed()) {
        setError(result.error?.errorData?.errorDescription || "An error occurred during auto sign-in");
    }
    
    if (result.isCompleted()) {
        setData(result.data);
        setSignUpState(state);
    }
}

앱 실행 및 테스트

  1. 터미널 창을 열고 앱의 루트 폴더로 이동합니다.

    cd reactspa
    
  2. CORS 프록시 서버를 시작하려면 터미널에서 다음 명령을 실행합니다.

    npm run cors
    
  3. React 앱을 시작하려면 다른 터미널 창을 연 다음, 다음 명령을 실행합니다.

    cd reactspa
    npm start
    
  4. 웹 브라우저를 열고 http://localhost:3000/sign-up로 이동합니다. 등록 양식이 나타납니다.

  5. 계정에 등록하려면 세부 정보를 입력하고 계속 단추를 선택한 다음 프롬프트를 따릅니다.

다음으로, React 앱을 업데이트하여 사용자를 로그인하거나 사용자의 암호를 재설정할 수 있습니다.

next.config.js poweredByHeader를 false로 설정

기본적으로 x-powered-by 헤더는 HTTP 응답에 포함되어 애플리케이션이 Next.js의해 구동됨을 나타냅니다. 그러나 보안 또는 사용자 지정 이유로 이 헤더를 제거하거나 수정할 수 있습니다.

const nextConfig: NextConfig = {
  poweredByHeader: false,
  /* other config options here */
};

다음 단계