この記事は?
Cognitoの各種リソース、APIを使った認可機能を実装したのでメモ
クライアントはNext.jsを使用する
※初めてNext.js触ったのでアホな実装してたらごめんなさい
※この記事の実装を実際のアプリでそのまま利用することは想定していません(諸々ガバガバなのでCognito + Next.jsの取っ掛かり程度でお願いします)
ちなみにOAuth2.0何もわからんという人は↓の記事を読んだあと
↓の記事の1.認可コードフロー
を読むのがおすすめ
OAuth2.0の概要
OAuth2.0による認可方法についてざっくりと説明する
OAuth2.0には認可コードフロー(Authorization Code Grant)
、Implicit Grant Type
、Client Credentials Grant Type
など様々なフローが用意されている
今回紹介するのは認可コードフローのみなので悪しからず
登場人物
まずはOAuth2.0のフローに出ててくる登場人物についてまとめる
- リソース所有者
-
保護対象リソース
へのアクセス権を持つ人 - 要するにアプリのユーザー
-
- クライアント
-
リソース所有者
に代わって実際に保護対象リソース
へアクセスするプログラムないしソフトウェア
-
- 認可サーバー
-
クライアント
に対して保護対象リソース
へのアクセス権(アクセストークン)を発行するサーバー
-
- 保護対象リソース
-
リソース所有者
がアクセス権を持っているリソース(≒Web API)
-
認可のフロー
これらの登場人物がどのように振舞うかをまとめたシーケンス図↓
フローの補足と用語の説明
2. AuhtorizationCode発行画面へリダイレクト
リソース所有者にこのクライアントに対してAuthorizationCodeを発行してもよいか尋ねる画面へリダイレクトする
この際、クエリパラメータとして以下の値を渡すことになる
client_id
認可サーバーが各クライアントに対して発行するID
クライアントは予めこのクライアントIDを発行しておき、諸々リクエストする際にはこれを使って認可サーバーから信頼されたクライアントであることを示す必要がある
response_type
このエンドポイントでリソース所有者が承認した場合のレスポンスとして何を返してもらうか制御するパラメータ
認可コードフローの場合はcode
を指定する(=AuthorizationCodeを返してもらう)
その他のフローの場合はtoken
を指定して直接アクセストークンをもらったりもする
redirect_uri
リソース所有者が承認した後にリダイレクトするURI
このエンドポイントへAuthorizationCodeと一緒にリダイレクトすることになるので、後述のAuthorizationCodeとアクセストークンの交換を実行するパスを指定しておこう
state
UUIDv4などの推測できないランダム文字列を入れる
AuhtorizationCodeと同様にredirect_uriへリダイレクトする際にこの値が返ってくるので、リクエスト時に入力した値と同じか検証することでCSRF攻撃を防ぐことができる
※今回は触れないもののstateの代わりにPKCEという仕組みを利用する場合もある(というかそっちの方がより安全)
※↓わかりやすく説明されている記事
scope
今回は特に触れないので概要だけ
この一連の流れで取得するアクセストークンの権限の範囲を指定するパラメータ
Adminなら全部許可とか一般のユーザーなら一部だけとか細かく権限を分けたいときに使う
6. stateの検証
認可サーバーからクライアントへリダイレクトしてくる際、クエリパラメータにstateが入っている
この値が2
で送ったものと一致しているかをここで検証する
具体的には、2
でstateを発行する際にセッションIDも発行しそれと紐づけて適当なデータストアに保存しておき、CookieにはセッションIDだけ保存する
そして、この処理に入った時にCookieからセッションIDを取得してそれに紐づくstateとクエリパラメータのstateが一致するか検証、という流れになる
7. AuhtorizationCodeとAccessTokenの交換をリクエスト
以下のパラメータを使ってリクエストする(説明済みの値は省略)
client_secret
client_idと同様に認可サーバーから発行してもらう認証情報
なおclient_idとはクライアント(より具体的にはWebアプリのバックエンド、Next.jsでいう/pages/api
以下のファイル)と認可サーバーのやり取りの中でしか使われず、ユーザーの目に入ることがないという点で異なる
client_secretはHTMLなどユーザーが閲覧可能な領域に保存しないよう注意!
※ちなみに、バックエンドのサーバーを持たないアプリの場合(どうあがいてもclient_secretを秘匿できない場合)は認可コードフローではなくclient_secretを使う必要のない以外のフローを選択することになる
※当然その場合はclient_secretを発行する必要はない
code
5
で受け取ったAuthorizationCodeを入れる
grant_type
OAuth2.0のどの認可フローを使ってアクセストークンを取得するか指定する
今回は認可コードフローなのでauthorization_code
になる
redirect_uri
2
で送ったのと同じURIを入れる
認可サーバーにPOSTしているのでこのURIにリダイレクトするわけではないけれど、grant_type=authorization_codeの場合は検証のためこの値が要求される
CognitoをOAuth2.0にあてはめる
前項の認可フローをCognitoのリソースをあてはめてみる
登場人物
OAuth2.0の登場人物とCognitoのリソースは↓のような対応関係にある
-
ユーザー
- Cognito UserPoolというリソースで管理されるユーザー
-
Cognito アプリクライアント
- Cognitoの認可APIとやりとりするクライアントを抽象化したもの
- 前述のclient_idやclient_secretはこのアプリクライアントを作成することで発行される
-
Cognito API
- AuthorizationCode発行を承認する画面やAuthorizationCodeとアクセストークンの交換をするAPIなどを提供してくれる
-
API
- API GatewayのAuthorizerにCognitoを指定したAPI
- 要するにCognitoが発行したトークンがないとアクセスできないAPIのこと
シーケンス
Cognitoからアクセストークンをもらってくるまでの手順を整理する
まず最初に、認可してもらうユーザー自体の作成が必要
→Cognito UserPoolにサインアップする、いわばOAuth2.0の前段の話
その次にアクセストークンを取得し、それを使って保護されたAPIを叩くことになる
したがって、サインアップとトークン取得、API実行という大きく分けて3つのシーケンスが存在する
サインアップシーケンス
Cognito UserPoolにユーザーを新規作成するシーケンス
補足
SecretHash
これの正体はclient_id、client_secret、ユーザー名のハッシュ値
具体的な生成方法はこちらを参照
CognitoのAPIリファレンス
それぞれ以下のリンク先を参照
トークン取得シーケンス
API実行シーケンス
↑で取得したトークンを使って保護されたAPIを叩くシーケンス
実装例
構成図
ローカル環境にNext.jsで作ったクライアントを動かし、AWS上のリソースとやりとりする構成
コード
インフラ
クライアント
環境構築手順
インフラの構築
cognito-example-infra
のディレクトリへ移動し、カスタムドメインのプレフィックスをつける(ここ)
→他のユーザーが使っているものと被らなければOK
そうしたらCDKのデプロイコマンドを叩く
npm install
cdk deploy CognitoExampleStack
クライアントの構築
cognito-example-client
のディレクトリへ移動し、以下のコマンドで環境変数を設定する
bash scripts/init-env.sh
これによって.env
ファイルにAPI GatewayのエンドポイントやCognito UserPoolのIDなどが登録される
完了後、以下のコマンドでクライアントを立ち上げる
npm install
npm run dev
動作確認&コードの説明
http://localhost:3000 にアクセスしてアカウント作成画面を開く
アカウント作成
このコンポーネントがレンダリングされる
メアドとパスワード入力して次へ
をクリックすると、CognitoのSignUp APIを実行してユーザーが作成される
メールアドレスの検証
メールアドレスの検証画面へ移動する
この段階でユーザープールの画面を開くと、先ほど入力したメアドでユーザーが作成されている
検証コードがメールで届くのでそれを入力して検証
ボタンをクリック
この時、CognitoのConfirmSignUpを実行している
するとユーザーの確認ステータスが確認済み
となり、これでユーザー作成は完了
ログイン(トークン取得)
ログイン画面へ移動するのでログイン画面へ
ボタンをクリック
この時の処理がちょっと複雑なので順を追って説明
- セッションIDとstateを生成
- DynamoDBにセッションIDとstateを、CookieにセッションIDのみを保存
- stateなどでAuthorizeURIを生成
- AuthorizeURIへリダイレクト
するとCognitoの画面へ移動するのでメアド、パスワードを入力する
そうしたら再びクライアントへ返ってきて、このファイルの処理が走る
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const authorizationCode = req.query.code as string;
const state = req.query.state;
const sessionId = req.cookies.session || "";
const session = await sessionsTableService.get(sessionId);
if (!state || session.state !== state) {
throw new Error("Validate state failed");
}
const tokens = await cognitoService.getTokenWithAuthorizationCode(
authorizationCode
);
await sessionsTableService.create({
id: sessionId,
state: "",
idToken: tokens.idToken,
expireIn: tokens.expireIn,
});
res.redirect(Path.Test);
} catch (error) {
console.error(error);
res.status(400).json({ message: "Signin callback failed" });
}
}
処理の流れは、
- クエリパラメータから
AuthorizationCode
とstate
を取得する - CookieからsessionIDを取得し、それでDynamoDBから
state
を取得する -
state
が一致するか確認する - Cognitoのトークンエンドポイントにリクエストする
- トークンをDynamoDBに保存する
保護対象リソースへアクセス
この画面でAPIを実行
ボタンをクリックすると、Cognitoによって保護されたAPI Gatewayへアクセスできる
この時の処理の流れが↓(ファイルはこれ)
- CookieからsessionID取得
- DynamoDBからIDトークン取得
- リクエストヘッダーにIDトークンをセットしてリクエスト