1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

CognitoとNext.jsでOAuth2.0の認可機能を実装

Posted at

この記事は?

Cognitoの各種リソース、APIを使った認可機能を実装したのでメモ
クライアントはNext.jsを使用する
※初めてNext.js触ったのでアホな実装してたらごめんなさい
※この記事の実装を実際のアプリでそのまま利用することは想定していません(諸々ガバガバなのでCognito + Next.jsの取っ掛かり程度でお願いします)

ちなみにOAuth2.0何もわからんという人は↓の記事を読んだあと

↓の記事の1.認可コードフローを読むのがおすすめ

OAuth2.0の概要

OAuth2.0による認可方法についてざっくりと説明する

OAuth2.0には認可コードフロー(Authorization Code Grant)Implicit Grant TypeClient Credentials Grant Typeなど様々なフローが用意されている
今回紹介するのは認可コードフローのみなので悪しからず

登場人物

まずはOAuth2.0のフローに出ててくる登場人物についてまとめる
登場人物.png

  • リソース所有者
    • 保護対象リソースへのアクセス権を持つ人
    • 要するにアプリのユーザー
  • クライアント
    • リソース所有者に代わって実際に保護対象リソースへアクセスするプログラムないしソフトウェア
  • 認可サーバー
    • クライアントに対して保護対象リソースへのアクセス権(アクセストークン)を発行するサーバー
  • 保護対象リソース
    • リソース所有者がアクセス権を持っているリソース(≒Web API)

認可のフロー

これらの登場人物がどのように振舞うかをまとめたシーケンス図↓
シーケンス図.png

フローの補足と用語の説明

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.png

  • ユーザー
    • Cognito UserPoolというリソースで管理されるユーザー
  • Cognito アプリクライアント
    • Cognitoの認可APIとやりとりするクライアントを抽象化したもの
    • 前述のclient_idclient_secretはこのアプリクライアントを作成することで発行される
  • Cognito API
    • AuthorizationCode発行を承認する画面やAuthorizationCodeとアクセストークンの交換をするAPIなどを提供してくれる
  • API
    • API GatewayのAuthorizerにCognitoを指定したAPI
    • 要するにCognitoが発行したトークンがないとアクセスできないAPIのこと

シーケンス

Cognitoからアクセストークンをもらってくるまでの手順を整理する

まず最初に、認可してもらうユーザー自体の作成が必要
→Cognito UserPoolにサインアップする、いわばOAuth2.0の前段の話
その次にアクセストークンを取得し、それを使って保護されたAPIを叩くことになる

したがって、サインアップとトークン取得、API実行という大きく分けて3つのシーケンスが存在する

サインアップシーケンス

Cognito UserPoolにユーザーを新規作成するシーケンス
アカウント作成シーケンス.png

補足

SecretHash

これの正体はclient_id、client_secret、ユーザー名のハッシュ値
具体的な生成方法はこちらを参照

CognitoのAPIリファレンス

それぞれ以下のリンク先を参照

トークン取得シーケンス

↑で作成したユーザーでトークンを取得しに行くシーケンス
Cognitoトークン取得.png

API実行シーケンス

↑で取得したトークンを使って保護されたAPIを叩くシーケンス
API実行.png

実装例

構成図

構成図.png
ローカル環境に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 にアクセスしてアカウント作成画面を開く

アカウント作成

スクリーンショット 2022-10-03 222020.png

このコンポーネントがレンダリングされる
メアドとパスワード入力して次へをクリックすると、CognitoのSignUp APIを実行してユーザーが作成される

メールアドレスの検証

スクリーンショット 2022-10-03 222057.png

メールアドレスの検証画面へ移動する
この段階でユーザープールの画面を開くと、先ほど入力したメアドでユーザーが作成されている

スクリーンショット 2022-10-03 222137.png

検証コードがメールで届くのでそれを入力して検証ボタンをクリック
この時、CognitoのConfirmSignUpを実行している

スクリーンショット 2022-10-03 222212.png

するとユーザーの確認ステータスが確認済みとなり、これでユーザー作成は完了

ログイン(トークン取得)

スクリーンショット 2022-10-03 222221.png

ログイン画面へ移動するのでログイン画面へボタンをクリック
この時の処理がちょっと複雑なので順を追って説明

  1. セッションIDとstateを生成
  2. DynamoDBにセッションIDとstateを、CookieにセッションIDのみを保存
  3. stateなどでAuthorizeURIを生成
  4. AuthorizeURIへリダイレクト

するとCognitoの画面へ移動するのでメアド、パスワードを入力する
そうしたら再びクライアントへ返ってきて、このファイルの処理が走る

/pages/api/signin/callback.ts
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" });
  }
}

処理の流れは、

  1. クエリパラメータからAuthorizationCodestateを取得する
  2. CookieからsessionIDを取得し、それでDynamoDBからstateを取得する
  3. stateが一致するか確認する
  4. Cognitoのトークンエンドポイントにリクエストする
  5. トークンをDynamoDBに保存する

保護対象リソースへアクセス

スクリーンショット 2022-10-03 222251.png

この画面でAPIを実行ボタンをクリックすると、Cognitoによって保護されたAPI Gatewayへアクセスできる

この時の処理の流れが↓(ファイルはこれ)

  1. CookieからsessionID取得
  2. DynamoDBからIDトークン取得
  3. リクエストヘッダーにIDトークンをセットしてリクエスト
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?