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
フックを使用できるようになります。
import React from "react";
import ReactDOM from "react-dom";
import { Auth0Provider } from "@auth0/auth0-react";
import "bootstrap/dist/css/bootstrap.min.css";
import { App } from "./App";
ReactDOM.render(
<Auth0Provider
domain={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";
async function signUp() {
try {
const user = await Auth.signUp({
username,
password,
attributes: {
email,
phone_number,
},
});
console.log({ user });
} catch (error) {
console.log("error signing up:", error);
}
}
メソッドしか用意されておらず、ログインユーザの情報などを React アプリでグローバルに保持する仕組みは自分で用意する必要があります。amplify-js でも Auth0 のような使いやすい DX(開発者体験)にしたい! ということが本記事のモチベーションです。つまり、以下のように使用したいわけです。
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import "./index.css";
import CognitoAuthProvider from "./cognito/CognitoAuthProvider";
ReactDOM.render(
<CognitoAuthProvider>
<App />
</CognitoAuthProvider>,
document.getElementById("root")
);
<App/>
コンポーネントを <CognitoAuthProvider>
でラップするだけで、認証系の処理やログインユーザのステートを取り出す useAuth
フックが使えるようにしていきます。
import React from "react";
import { useAuth } from "../../cognito/CognitoAuthProvider";
export default function LogoutButton(props: any) {
const { isAuthenticated, signOut } = useAuth();
if (!isAuthenticated) return null;
return <Button onClick={() => signOut()} {...props} />;
}
React.Context とは
React の Context は配下の子コンポーネントにデータを渡すための便利な方法です。従来は props
を使用することで、子コンポーネントにデータを渡していましたが、コンポーネントのネストが深くなると非常に面倒で複雑になります。 Context を使用することで 認証 や UI テーマ など多くのコンポーネントが使用する情報を共有して保持・取得できます。
React.createContext
Context オブジェクトを作成します。React がこの Context オブジェクトが登録されているコンポーネントをレンダーする場合、ツリー内の最も近い上位の一致する Provider から現在の Context の値を読み取ります。
const MyContext = React.createContext(defaultValue);
Context.Provider
全ての Context オジェクトには Context.Provider コンポーネントが付属しています。これにより Context.Consumer コンポーネントは Context の変更を購読できます。実際のユースケースでは Consumer ではなく、useContext フックを使用することが多いでしょう。
<MyContext.Provider value={/* 何らかの値 */}>
useContext
Context オブジェクトを受け取り、その Context の value を返します。<MyContext.Provider/>
が更新されると、このフックは MyContext.Provider
に渡された value を使用してコンポーネントを再レンダーします。
const value = useContext(MyContext);
認証情報を Context に集約する
さて、認証情報として以下のようなメソッドとステートを保持する Context を作っていきます。これらの値があればログイン、ログアウト、サインアップ、確認コード入力の一連の流れが実装できます。
項目 | 概要 |
---|---|
isAuthenticated | ログインしているか |
isLoading | ローディング中か(画面制御で使用) |
user | ログインしているユーザの情報 |
error | ログイン処理、サインアップ処理などでエラーがあれば詰める |
signIn | サインインする。 |
signUp | サインアップする。 |
confirmSignUp | サインアップ確認コードを入力する |
signOut | サインアウトする。 |
State
Context が保持するステートの定義(インタフェース)を作成します。
import { CognitoUser } from "amazon-cognito-identity-js";
export interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
user?: CognitoUser;
error?: any;
}
const initialState: AuthState = {
isAuthenticated: false,
isLoading: false,
};
const stub = (): never => {
throw new Error(
"You forgot to wrap your component in <CognitoAuthProvider>."
);
};
export const initialContext = {
...initialState,
signIn: stub,
signUp: stub,
confirmSignUp: stub,
signOut: stub,
};
Context
Context オブジェクトを作成します。各コンポーネントから取り出すためのカスタムフック useAuth()
を合わせて作成しておきます。
import React, { 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";
interface IAuthContext extends AuthState {
signIn: (signInOption: LoginOption) => Promise<void>;
signUp: (params: SignUpParams) => Promise<CognitoUser | undefined>;
confirmSignUp: (params: any) => Promise<void>;
signOut: () => void;
}
export const AuthContext = React.createContext<IAuthContext>(initialContext);
export const useAuth = () => useContext(AuthContext);
Provider
最後に Provider には Cognito とやりとりする処理と、認証情報を保持する処理を実装します。
import React from "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";
import Amplify from "aws-amplify";
import { AuthContext } from "./AuthContext";
export type LoginOption = {
username: string;
password: string;
};
interface ICognitoAuthProviderParams {
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;
}
export default function CognitoAuthProvider(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();
}, []);
const checkAuthenticated = () => {
setIsLoading(true);
Auth.currentSession()
.then((data) => {
if (data) setIsAuthenticated(true);
})
.catch((err) => console.log("current session error", err))
.finally(() => {
setIsLoading(false);
});
};
const currentAuthenticatedUser = async (): Promise<void> => {
const user: CognitoUser = await Auth.currentAuthenticatedUser();
setUser(user);
};
const signIn = async ({ username, password }: LoginOption): Promise<void> => {
setIsLoading(true);
try {
await Auth.signIn(username, password);
setIsAuthenticated(true);
} catch (error) {
console.log("error signing in", error);
setError(error);
setIsAuthenticated(false);
}
setIsLoading(false);
};
const signUp = async (
param: SignUpParams
): Promise<CognitoUser | undefined> => {
setIsLoading(true);
let result;
try {
result = await Auth.signUp(param);
setUser(result.user);
} catch (error) {
console.log("error signing up", error);
setError(error);
}
setIsLoading(false);
return result?.user;
};
const confirmSignUp = async ({ username, code }: any): Promise<void> => {
setIsLoading(true);
try {
await Auth.confirmSignUp(username, code);
setIsAuthenticated(true);
} catch (error) {
console.log("error confirming sign up", error);
setError(error);
}
setIsLoading(false);
};
const signOut = () => {
setIsLoading(true);
Auth.signOut()
.then(() => {
setIsAuthenticated(false);
})
.catch((err) => console.log("error signing out: ", err))
.finally(() => {
setIsLoading(false);
});
};
return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
signIn,
signUp,
confirmSignUp,
signOut,
user,
error,
}}
>
{props.children}
</AuthContext.Provider>
);
}
使用方法
ここまで準備ができれば使用する側はこの CognitoAuthProvider
でコンポーネントをラップすることで useAuth()
フック経由で各種ステートの値またはメソッドを使用できます。
amplifyConfig として設定値は外部ファイルで保持しています。
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
import CognitoAuthProvider from "./cognito/CognitoAuthProvider";
import awsconfig from "./aws-exports";
ReactDOM.render(
<CognitoAuthProvider amplifyConfig={awsconfig}>
<App />
</CognitoAuthProvider>,
document.getElementById("root")
);
amplifyConfig は以下のようなファイルになります。
const amplifyConfig = {
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",
};
export default amplifyConfig;
ログアウトボタンのコンポーネントです。コードベースをシンプルにできました。
import React from "react";
import { useAuth } from "../../cognito/CognitoAuthProvider";
export default function LogoutButton(props: any) {
const { isAuthenticated, signOut } = useAuth();
if (!isAuthenticated) return null;
return <Button onClick={() => signOut()} {...props} />;
}
さいごに
React の Context を使用することで、認証情報などのグローバルな値を一元的に管理できるようになります。
ただ、 Context は多くのコンポーネントからアクセスされる場合に使用することとしましょう。
Context はコンポーネントの再利用をより難しくする為、慎重に利用してください。
本記事で紹介した React.Context を使用したカスタムフックを使用するという発想はそのうち amplify-js に PullRequest しようと思います。Cognito ユーザ(または Amplify ユーザ)が個別にこのような実装をしなくとも、ライブラリとして提供し、すぐに簡単なインタフェースで認証処理を実現できるようにしていきたいですね。