次の方法で共有


チュートリアル: ネイティブ認証を使用して React シングルページ アプリにユーザーをサインインする (プレビュー)

適用対象: 灰色の X 記号がある白い円。 従業員テナント 白いチェック マーク記号がある緑の円。 外部テナント (詳細情報)

このチュートリアルでは、ネイティブ認証を使用して React シングルページ アプリ (SPA) にユーザーをサインインさせる方法について説明します。

このチュートリアルでは、次の操作を行います。

  • ユーザー名 (電子メール) とパスワードを使用してユーザーをサインインするように React アプリを更新します。
  • サインイン フローをテストします。

前提条件

アプリがネイティブ認証 API に対して行う呼び出しの種類を定義する

サインイン フロー中、アプリは、サインイン要求の開始、認証方法の選択、セキュリティ トークンの要求など、ネイティブ認証 API を複数回呼び出します。

これらの呼び出しを定義するには、scr/client/RequestTypes.ts ファイルを開き、次のコード スニペットを追加します。

   export interface TokenRequestType {
        continuation_token: string;
        client_id: string;
        grant_type: string;
        scope: string;
        password?: string;
        oob?: string;
        challenge_type?: string;
    }

    // Sign in
    export interface TokenSignInType {
        continuation_token: string;
        grant_type: string;
        password?: string;
        oob?: string;
    }

    export interface ChallengeRequest {
        client_id: string;
        challenge_type: string;
        continuation_token: string;
    }

    export interface SignInStartRequest {
        client_id: string;
        challenge_type: string;
        username: string;
    }

    export interface SignInTokenRequest {
        client_id: string;
        grant_type: string;
        continuation_token: string;
        scope: string;
        challenge_type?: string;
        password?: string;
        oob?: string;
    }

ネイティブ認証 API からアプリが受け取る応答の種類を定義する

サインイン操作のためにアプリがネイティブ認証 API から受信できる応答の種類を定義するには、src/client/ResponseTypes.ts ファイルを開き、次のコード スニペットを追加します。

    export interface TokenResponseType {
        token_type: string;
        scope: string;
        expires_in: number;
        access_token: string;
        refresh_token: string;
        id_token: string;
    }

サインイン要求を処理する

このセクションでは、サインイン フロー要求を処理するコードを追加します。 これらの要求の例としては、サインイン フローの開始、認証方法の選択、セキュリティ トークンの要求があります。

これを行うには、src/client/SignInService.ts という名前のファイルを作成し、次のコード スニペットを追加します。

    import { CLIENT_ID, ENV } from "../config";
    import { postRequest } from "./RequestClient";
    import { ChallengeRequest, SignInStartRequest, TokenRequestType, TokenSignInType } from "./RequestTypes";
    import { TokenResponseType } from "./ResponseTypes";

    export const signInStart = async ({ username }: { username: string }) => {
        const payloadExt: SignInStartRequest = {
            username,
            client_id: CLIENT_ID,
            challenge_type: "password oob redirect",
        };

        return await postRequest(ENV.urlOauthInit, payloadExt);
    };

    export const signInChallenge = async ({ continuation_token }: { continuation_token: string }) => {
        const payloadExt: ChallengeRequest = {
            continuation_token,
            client_id: CLIENT_ID,
            challenge_type: "password oob redirect",
        };

        return await postRequest(ENV.urlOauthChallenge, payloadExt);
    };

    export const signInTokenRequest = async (request: TokenSignInType): Promise<TokenResponseType> => {
        const payloadExt: TokenRequestType = {
            ...request,
            client_id: CLIENT_ID,
            challenge_type: "password oob redirect",
            scope: "openid offline_access",
        };

        if (request.grant_type === "password") {
            payloadExt.password = request.password;
        }

        if (request.grant_type === "oob") {
            payloadExt.oob = request.oob;
        }

        return await postRequest(ENV.urlOauthToken, payloadExt);
    };

challenge_type プロパティは、クライアント アプリがサポートする認証方法を示します。 このアプリ サインでは電子メールとパスワードが使用されるため、チャレンジの種類の値は password oob redirect です。 のチャレンジの種類について詳しく読む。

UI コンポーネントを作成する

サインイン フロー中に、このアプリはユーザーの資格情報、ユーザー名 (電子メール)、パスワードを収集してユーザーをサインインさせます。 ユーザーが正常にサインインすると、アプリにユーザーの詳細が表示されます。

  1. /pages/signin という名前のフォルダーをsrc フォルダー内に作成します。

  2. サインイン フォームを作成、表示、送信するには、src/pages/signin/SignIn.tsx ファイルを作成し、次のコードを追加します。

        import React, { useState } from "react";
        import { Link as LinkTo, useNavigate } from "react-router-dom";
        import { signInStart, signInChallenge, signInTokenRequest } from "../../client/SignInService";
        import { ErrorResponseType } from "../../client/ResponseTypes";
    
        export const SignIn: React.FC = () => {
          const [email, setEmail] = useState<string>("");
          const [password, setPassword] = 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 (!validateEmail(email)) {
              setError("Invalid email format");
              return;
            }
            setError("");
            setIsloading(true);
            try {
              const res1 = await signInStart({
                username: email,
              });
              const res2 = await signInChallenge({ continuation_token: res1.continuation_token });
              const res3 = await signInTokenRequest({
                continuation_token: res2.continuation_token,
                grant_type: "password",
                password: password,
              });
              navigate("/user", { state: res3 });
            } catch (err) {
              setError("An error has occured " + (err as ErrorResponseType).error_description);
            } finally {
              setIsloading(false);
            }
          };
    
          return (
            <div className="login-form">
              <form onSubmit={handleSubmit}>
                <h2>Login</h2>
                <div className="form-group">
                  <label>Email:</label>
                  <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
                </div>
                <div className="form-group">
                  <label>Password:</label>
                  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
                </div>
                {error && <div className="error">{error}</div>}
                {isLoading && <div className="warning">Sending request...</div>}
                <button type="submit" disabled={isLoading}>Login</button>
              </form>
            </div>
          );
        };
    
  3. サインインが成功した後にユーザーの詳細を表示するには:

    1. client/Utils.ts というファイルを作成し、次のコード スニペットを追加します。

          export function parseJwt(token: string) {
              var base64Url = token.split(".")[1];
              var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
              var jsonPayload = decodeURIComponent(
                  window
                  .atob(base64)
                  .split("")
                  .map(function (c) {
                      return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
                  })
                  .join("")
              );
              return JSON.parse(jsonPayload);
          }
      
    2. src/pages フォルダーで user というフォルダーを作成します。

    3. src/pages/user/UserInfo.tsx という名前のファイルを作成し、次のコード スニペットを追加します。

      // User.tsx
      import React from "react";
      import { useLocation } from "react-router-dom";
      import { parseJwt } from "../../client/Utils";
      
      export const UserInfo: React.FC = () => {
        const { state } = useLocation();
        const decodedToken = parseJwt(state.access_token);
        const { given_name, scp, family_name, unique_name: email } = decodedToken;
      
        console.log(decodedToken);
        const familyName = family_name;
        const givenName = given_name;
        const tokenExpireTime = state.expires_in;
        const scopes = state.scope;
      
        return (
          <div className="user-info">
            <h2>User Information</h2>
            <div className="info-group">
              <label>Given Name:</label>
              <span>{givenName}</span>
            </div>
            <div className="info-group">
              <label>Family Name:</label>
              <span>{familyName}</span>
            </div>
            <div className="info-group">
              <label>Email:</label>
              <span>{email}</span>
            </div>
            <div className="info-group">
              <label>Token Expire Time:</label>
              <span>{tokenExpireTime}</span>
            </div>
            <div className="info-group">
              <label>Scopes:</label>
              <span>{scopes}</span>
            </div>
            <div className="info-group">
              <label>Token payload:</label>
              <span><pre>{JSON.stringify(decodedToken, null, 2)}</pre></span>
            </div>
          </div>
        );
      };
      

アプリ ルートを追加する

src/AppRoutes.tsx ファイルを開き、その内容を次のコードに置き換えます。

import { Route, Routes } from "react-router-dom";
import { SignIn } from "./pages/SignIn/SignIn";
import { UserInfo } from "./pages/User/UserInfo";
import { SignUp } from "./pages/SignUp/SignUp";
import { SignUpChallenge } from "./pages/SignUp/SignUpChallenge";
import { SignUpCompleted } from "./pages/SignUp/SignUpCompleted";
//For password reset
//import { ResetPassword } from "./pages/ResetAccount/ResetPassword";

export const AppRoutes = () => {
  return (
    <Routes>
      <Route path="/" element={<SignUp />} />
      <Route path="/signin" element={<SignIn />} />
      <Route path="/user" element={<UserInfo />} />
      <Route path="/signup" element={<SignUp />} />
      <Route path="/signup/challenge" element={<SignUpChallenge />} />
      <Route path="/signup/completed" element={<SignUpCompleted />} />
      //For password reset
      //<Route path="/reset" element={<ResetPassword />} />
    </Routes>
  );
};

アプリを実行してテストする

実行とテスト の手順に従ってアプリを実行しますが、今回は、先ほどサインアップしたユーザー アカウントを使用してサインイン フローをテストします。

次のステップ

チュートリアル: ネイティブ認証 を使用して外部テナントのユーザーの React アプリでパスワードをリセットする