AWS Cognito は認証・認可を提供している AWS のサービスです。Amplify と統合することで、超高速に構築できます。Cognito を使用することで、API Gateway や S3 など他の AWS サービスとの統合がより簡単にできるようになります。
本記事では、Cognito を使用した React アプリケーションの実装例を紹介します。Cognito へのアクセスには amplify-jsというライブラリを使用します。さらに React の Context.Provider という機能を使うことで認証に関連する処理をカスタムフックに集約する方法を考察します。
本記事で実装されたアプリケーションは以下のような動作をします。ログイン、ログアウト、サインアップ、確認メールなど。
本アプリケーションは Vercel にデプロイされています。
https://task-app.geeawa.vercel.app/login
また、以下の GitHub リポジトリにホストしています。
https://github.com/daisuke-awaji/task-app
amplify-js でも React Hooks を使いたい
先週は React アプリに Auth0 でシュッと認証を組み込んで Vercel に爆速デプロイするという記事を書きました。Auth0 のクライアントライブラリは非常に使い勝手がよく、<Auth0Provider>
という Provider で包むだけで useAuth0
フックを使用できるようになります。
importReactfrom"react";importReactDOMfrom"react-dom";import{Auth0Provider}from"@auth0/auth0-react";import"bootstrap/dist/css/bootstrap.min.css";import{App}from"./App";ReactDOM.render(<Auth0Providerdomain={process.env.REACT_APP_AUTH0_DOMAIN!}clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!}redirectUri={window.location.origin}><App/></Auth0Provider>,document.querySelector("#root"));
一方で amplify-js にはこのような機能はありません。認証系処理のメソッドは Auth
モジュールから取り出して使う必要があります。以下はサインアップするメソッドです。参考: 公式 Sign up, Sign in & Sign out
import{Auth}from"aws-amplify";asyncfunctionsignUp(){try{constuser=awaitAuth.signUp({username,password,attributes:{email,phone_number,},});console.log({user});}catch(error){console.log("error signing up:",error);}}
メソッドしか用意されておらず、ログインユーザの情報などを React アプリでグローバルに保持する仕組みは自分で用意する必要があります。amplify-js でも Auth0 のような使いやすい DX(開発者体験)にしたい! ということが本記事のモチベーションです。つまり、以下のように使用したいわけです。
importReactfrom"react";importReactDOMfrom"react-dom";importAppfrom"./App";import*asserviceWorkerfrom"./serviceWorker";import"./index.css";importCognitoAuthProviderfrom"./cognito/CognitoAuthProvider";ReactDOM.render(<CognitoAuthProvider><App/></CognitoAuthProvider>,document.getElementById("root"));
<App/>
コンポーネントを <CognitoAuthProvider>
でラップするだけで、認証系の処理やログインユーザのステートを取り出す useAuth
フックが使えるようにしていきます。
importReactfrom"react";import{useAuth}from"../../cognito/CognitoAuthProvider";exportdefaultfunctionLogoutButton(props:any){const{isAuthenticated,signOut}=useAuth();if(!isAuthenticated)returnnull;return<ButtononClick={()=>signOut()}{...props}/>;}
React.Context とは
React の Contextは配下の子コンポーネントにデータを渡すための便利な方法です。従来は props
を使用することで、子コンポーネントにデータを渡していましたが、コンポーネントのネストが深くなると非常に面倒で複雑になります。 Context を使用することで 認証や UI テーマなど多くのコンポーネントが使用する情報を共有して保持・取得できます。
React.createContext
Context オブジェクトを作成します。React がこの Context オブジェクトが登録されているコンポーネントをレンダーする場合、ツリー内の最も近い上位の一致する Provider から現在の Context の値を読み取ります。
constMyContext=React.createContext(defaultValue);
Context.Provider
全ての Context オジェクトには Context.Provider コンポーネントが付属しています。これにより Context.Consumer コンポーネントは Context の変更を購読できます。実際のユースケースでは Consumer ではなく、useContext フックを使用することが多いでしょう。
<MyContext.Providervalue={/* 何らかの値 */}>
useContext
Context オブジェクトを受け取り、その Context の value を返します。<MyContext.Provider/>
が更新されると、このフックは MyContext.Provider
に渡された value を使用してコンポーネントを再レンダーします。
constvalue=useContext(MyContext);
認証情報を Context に集約する
さて、認証情報として以下のようなメソッドとステートを保持する Context を作っていきます。これらの値があればログイン、ログアウト、サインアップ、確認コード入力の一連の流れが実装できます。
項目 | 概要 |
---|---|
isAuthenticated | ログインしているか |
isLoading | ローディング中か(画面制御で使用) |
user | ログインしているユーザの情報 |
error | ログイン処理、サインアップ処理などでエラーがあれば詰める |
signIn | サインインする。 |
signUp | サインアップする。 |
confirmSignUp | サインアップ確認コードを入力する |
signOut | サインアウトする。 |
State
Context が保持するステートの定義(インタフェース)を作成します。
import{CognitoUser}from"amazon-cognito-identity-js";exportinterfaceAuthState{isAuthenticated:boolean;isLoading:boolean;user?:CognitoUser;error?:any;}constinitialState:AuthState={isAuthenticated:false,isLoading:false,};conststub=():never=>{thrownewError("You forgot to wrap your component in <CognitoAuthProvider>.");};exportconstinitialContext={...initialState,signIn:stub,signUp:stub,confirmSignUp:stub,signOut:stub,};
Context
Context オブジェクトを作成します。各コンポーネントから取り出すためのカスタムフック useAuth()
を合わせて作成しておきます。
importReact,{useContext}from"react";import{SignUpParams}from"@aws-amplify/auth/lib-esm/types";import{CognitoUser}from"amazon-cognito-identity-js";import{AuthState,initialContext}from"./AuthState";import{LoginOption}from"./CognitoAuthProvider";interfaceIAuthContextextendsAuthState{signIn:(signInOption:LoginOption)=>Promise<void>;signUp:(params:SignUpParams)=>Promise<CognitoUser|undefined>;confirmSignUp:(params:any)=>Promise<void>;signOut:()=>void;}exportconstAuthContext=React.createContext<IAuthContext>(initialContext);exportconstuseAuth=()=>useContext(AuthContext);
Provider
最後に Provider には Cognito とやりとりする処理と、認証情報を保持する処理を実装します。
importReactfrom"react";import{useState,useEffect}from"react";import{SignUpParams}from"@aws-amplify/auth/lib-esm/types";import{CognitoUser}from"amazon-cognito-identity-js";import{Auth}from"aws-amplify";importAmplifyfrom"aws-amplify";import{AuthContext}from"./AuthContext";exporttypeLoginOption={username:string;password:string;};interfaceICognitoAuthProviderParams{amplifyConfig:{aws_project_region:string;aws_cognito_identity_pool_id:string;aws_cognito_region:string;aws_user_pools_id:string;aws_user_pools_web_client_id:string;oauth:{domain:string;scope:string[];redirectSignIn:string;redirectSignOut:string;responseType:string;};federationTarget:string;};children:any;}exportdefaultfunctionCognitoAuthProvider(props:ICognitoAuthProviderParams){Amplify.configure(props.amplifyConfig);const[isAuthenticated,setIsAuthenticated]=useState(false);const[isLoading,setIsLoading]=useState(true);const[error,setError]=useState(null);const[user,setUser]=useState<CognitoUser>();useEffect(()=>{checkAuthenticated();currentAuthenticatedUser();},[]);constcheckAuthenticated=()=>{setIsLoading(true);Auth.currentSession().then((data)=>{if(data)setIsAuthenticated(true);}).catch((err)=>console.log("current session error",err)).finally(()=>{setIsLoading(false);});};constcurrentAuthenticatedUser=async():Promise<void>=>{constuser:CognitoUser=awaitAuth.currentAuthenticatedUser();setUser(user);};constsignIn=async({username,password}:LoginOption):Promise<void>=>{setIsLoading(true);try{awaitAuth.signIn(username,password);setIsAuthenticated(true);}catch(error){console.log("error signing in",error);setError(error);setIsAuthenticated(false);}setIsLoading(false);};constsignUp=async(param:SignUpParams):Promise<CognitoUser|undefined>=>{setIsLoading(true);letresult;try{result=awaitAuth.signUp(param);setUser(result.user);}catch(error){console.log("error signing up",error);setError(error);}setIsLoading(false);returnresult?.user;};constconfirmSignUp=async({username,code}:any):Promise<void>=>{setIsLoading(true);try{awaitAuth.confirmSignUp(username,code);setIsAuthenticated(true);}catch(error){console.log("error confirming sign up",error);setError(error);}setIsLoading(false);};constsignOut=()=>{setIsLoading(true);Auth.signOut().then(()=>{setIsAuthenticated(false);}).catch((err)=>console.log("error signing out: ",err)).finally(()=>{setIsLoading(false);});};return(<AuthContext.Providervalue={{isAuthenticated,isLoading,signIn,signUp,confirmSignUp,signOut,user,error,}}>{props.children}</AuthContext.Provider>);}
使用方法
ここまで準備ができれば使用する側はこの CognitoAuthProvider
でコンポーネントをラップすることで useAuth()
フック経由で各種ステートの値またはメソッドを使用できます。
amplifyConfig として設定値は外部ファイルで保持しています。
importReactfrom"react";importReactDOMfrom"react-dom";importAppfrom"./App";import"./index.css";importCognitoAuthProviderfrom"./cognito/CognitoAuthProvider";importawsconfigfrom"./aws-exports";ReactDOM.render(<CognitoAuthProvideramplifyConfig={awsconfig}><App/></CognitoAuthProvider>,document.getElementById("root"));
amplifyConfig は以下のようなファイルになります。
constamplifyConfig={aws_project_region:"ap-northeast-1",aws_cognito_identity_pool_id:"ap-northeast-1:12345678909876543234567890",aws_cognito_region:"ap-northeast-1",aws_user_pools_id:"ap-northeast-1_xxxxxxxx",aws_user_pools_web_client_id:"xxxxxxxxxxxxxxx",oauth:{domain:"mydomain.auth.ap-northeast-1.amazoncognito.com",scope:["phone","email","openid","profile","aws.cognito.signin.user.admin",],redirectSignIn:"http://localhost:3000/",redirectSignOut:"http://localhost:3000/logout/",responseType:"code",},federationTarget:"COGNITO_USER_POOLS",};exportdefaultamplifyConfig;
ログアウトボタンのコンポーネントです。コードベースをシンプルにできました。
importReactfrom"react";import{useAuth}from"../../cognito/CognitoAuthProvider";exportdefaultfunctionLogoutButton(props:any){const{isAuthenticated,signOut}=useAuth();if(!isAuthenticated)returnnull;return<ButtononClick={()=>signOut()}{...props}/>;}
さいごに
React の Context を使用することで、認証情報などのグローバルな値を一元的に管理できるようになります。
ただ、 Context は多くのコンポーネントからアクセスされる場合に使用することとしましょう。
Context はコンポーネントの再利用をより難しくする為、慎重に利用してください。
本記事で紹介した React.Context を使用したカスタムフックを使用するという発想はそのうち amplify-jsに PullRequest しようと思います。Cognito ユーザ(または Amplify ユーザ)が個別にこのような実装をしなくとも、ライブラリとして提供し、すぐに簡単なインタフェースで認証処理を実現できるようにしていきたいですね。