Next.js × Amplify 認証アーキテクチャ実践ガイド
みなさんこんにちは!
突然ですが、こんな依頼をされたことはありませんか?
「じゃあ、プロトタイプ作っといてくれる?」
「あ、あと認証もつけといてね!」
私はあります。
しかも、だいたい期限は今週中。
そんな時に真っ先に思い浮かぶ AWS のサービスといえば….
Amplify
ですよね!
フロントもバックエンドもまとめて面倒を見てくれて、
認証も CI/CD も用意されている。
とても便利なサービスです。
…ただ!
いざ実務で使うとなると、
こんな不安が頭をよぎったりしませんか?
- 認証は実装できた…気がするけど、本当に合ってるの?
- 結局 OAuth と OIDC ってどう違うんだっけ?
- PKCE? state? nonce? 何それおいしいの?
- レビューで認証の手順突っ込まれませんように…。
そうなんです!
Amplify を使えば認証の仕組みは簡単に実装できるのですが、
裏を返せば、アーキテクチャをブラックボックスに包み、
「なぜ動いているのか」の理解を難しくしているのもまたAmplifyです。
私自身、Next.js(SSR)と Amplify(Cognito)を組み合わせた実装を行う中で、
「動いたけど、責務境界が説明できない…。」
という状態が続いていました。
この記事では、その状態を脱却するために整理した内容をベースに、
Next.jsのSSRを「認証と責務分離の境界」として扱う設計
を前提として、
- 認証基盤としての Cognito 構築
- IAM によるフロントエンド権限管理
- OAuth × OIDC 認証フローの理解
- PKCE / state / nonce の役割整理
を、実践構成ベースで解説していきます。
- Amplify チュートリアルで認証付きサイトを構築したことがある方
- Hosted UI でログインはできたが仕組みを説明できない方
- OAuth / OIDC / PKCE の違いが曖昧な方
- Next.js(SSR)での認証責務設計に悩んでいる方
はもちろん、
Amplify を使ったシステム設計に興味がある方にも
役立つ内容になっています。
そんな今回の記事を一言で表すと…
『なんか知らんけど動いた』を終わらせる
Next.js × Amplify 認証アーキテクチャ実践ガイド!!
それでは、続きをご覧ください!
注)本記事は Mac 環境を想定しています。
目次
- 1.なぜAmplifyの認証は理解が難しいのか
- 2.認証アーキテクチャ全体像
- 3.トークンと認証プロトコルの整理
- 4.認可コードフローの分解
- 5.Tokenの検証(JWTの中身を見る)
- 6.Next.js(SSR)を認証境界にする設計
- 7.まとめ
- 付録1.ソースコード(GitHub リポジトリ)
- 付録2.クイックスタート
※結論を早く知りたい方は6.Next.js(SSR)を認証境界にする設計へどうぞ!
1.なぜAmplifyの認証は理解が難しいのか
「とりあえず動いた!」
をゴールにするのであれば、
Amplify に認証を実装することは
そこまで難しくありませんよね。
チュートリアルも充実していますし、
Hosted UI を使えばログイン機能は
比較的簡単に構築できます。
けれども、
『実装の裏で何が行われているのか』
を理解しようとした瞬間に、
どこから手をつけていいのかわからなくなる。
結果として、
『Amplify の認証は理解が難しい』
そう感じてしまうのも無理はありません。
しかし、よくよく紐解いてみると、
Amplify 自体が特別難しい…というより、
別の構造的な要因が見えてきます。
Amplify の認証は単一の仕組みではなく、
複数の仕様がレイヤー状に組み合わされて構成されています。
ここで言う『レイヤー』とは、
認証処理を構成する技術要素の階層構造を指します。
これを細かく分解していくと、
次のような構造が見えてきます。
- ID / Password 認証
- OAuth(認可)
- OIDC(認証情報の標準化)
- PKCE(認可コード奪取対策)
- state(CSRF対策)
- nonce(ID Token 防御)
ぱっと見、どうでしょうか?
結構多いですよね。
Amplify はこれらの仕様を内部でラッピングし、
我々開発者にシンプルな API として
提供してくれています。
非常にありがたい仕組みです。
ただし裏側を理解するためには、
これらのレイヤーを1つ1つ分解して
理解していく必要が出てきます。
つまり、
『複数仕様の積み重ね構造こそが、理解を難しくしている正体』
というわけです。
本記事では、このレイヤーを順番に分解しながら、
- 認証フロー
- トークンの役割
- セキュリティ対策
- Next.js における責務境界
を整理し、
Amplify 認証をブラックボックスから
引き剥がして理解していきます。
次章では、この構造を踏まえた上で、
認証アーキテクチャ全体像を整理していきます。
2.認証アーキテクチャ全体像
今回構築する構成は、非常にシンプルです。
- フロントエンド:Next.js(SSR + CSR)
- データストア:DynamoDB
- IdP(認証基盤):Cognito
Next.js のサーバーサイドから
直接 DynamoDB を操作する構成とします。
外部 API サーバーは構築しません。
このアーキテクチャで最も重要になるのが
『トークンの扱い』
です。
Cognito から発行されるトークンは主に2種類あります。
Access Token(認可トークン)
ID Token(認証トークン)
この2つのトークンを、
- どこで受け取り
- どこで検証し
- どこまで信頼するのか
を明確にすることが
設計の核心になります。
今回の設計では、
そうした認証責務の境界を
Next.js(SSR + CSR)
で実現します。
ここで言う SSR はサーバーサイド処理、
CSR はブラウザ側で実行される処理を指します。
つまり、
- トークンの受け取り・検証・セッション化は SSR で行う
- CSR ではトークン検証結果のみを利用する
という
ブラウザを認証主体として信頼せず、
トークン検証責務をサーバー側に集約する設計方針
で構成します。
この仕組みを実現するために、
内部では次の技術要素が組み合わされています。
- ID / Password 認証
- OAuth(認可)
- OIDC(認証情報の標準化)
- PKCE(認可コード奪取対策)
- state(CSRF対策)
- nonce(ID Token 防御)
これらを連携させることで
安全な認証フローを構成しています。
まずは全体の流れを
イラストで俯瞰してみましょう。
3.トークンと認証プロトコルの整理
アーキテクチャで最も重要な
『トークン と OAuth / OIDC / Cognito の関係』
について整理していきましょう。
それぞれの役割を一言でまとめると、次の通りです。
OAuth: 『何をしてよいか(権限)』をアプリに委任する仕組み(認可)
OIDC: 『誰がログインしたか(本人確認)』をトークンで扱う仕組み(認証)
Cognito: OAuth / OIDC に基づき、トークンを発行する認証基盤(IdP)
ここでポイントなのが、
OIDC は OAuth の "上に乗る拡張仕様"
だということです。
OAuth にはもともと認可フローがあり、
その結果として『何をしてよいか(権限)』を表す
Access Token(認可トークン) を
受け取れるようになっています。
OIDC はこの OAuth のフローを土台にしつつ、
『誰がログインしたか(本人確認)』を表す
ID Token(認証トークン) を
追加で発行できるようにした拡張仕様という位置づけです。
OAuthとは?
OAuth は一言で言うと、
『Access Token(認可トークン) を介した権限の委任』
です。
OAuth のポイントは、ユーザーが持つ
『何をしてよいか(権限)』 を、
Access Token(認可トークン)という形で扱うことで、
アプリが権限を安全に受け取れるようにしたところにあります。
OIDCとは?
OIDC(OpenID Connect)は、
『誰がログインしたか(本人確認)』 を ID Token(認証トークン) として扱う
ための仕組みです。
OIDC は OAuth の拡張仕様です。
OAuth のフロー(認可コードフローなど)を使って
ID Token(認証トークン)を取得できるという点がポイントです。
Cognitoとは?
Cognito(User Pool)は OAuth / OIDC に対応した認証基盤(IdP)で、
次のトークンをまとめて発行してくれるAWSのマネージドサービスです。
Access Token(認可トークン : OAuth が扱う)
ID Token(認証トークン : OIDC が扱う)
今回のアーキテクチャのポイントは、
これらのトークンを どのように取り扱うか にあります。
中でも重要なのは、次の考え方です。
トークンが "発行されること" と、
発行されたトークンを "信頼してよいこと" は別問題
『Cognito が発行したトークンだから』と
無条件で信頼するのではなく、
- どこで受け取り
- どこで検証し(署名・期限・発行者・宛先など)
- どこまで信頼するのか
を Next.js(SSR)を境界にして設計する、
というのが本記事の方針です。
ここからは Step に分けて、
OAuth / OIDC を構成する技術要素を
順番に分解していきます。
4.認可コードフローの分解
Step1: 認可コードフロー
Amplify(Cognito Hosted UI)が採用する
認可コードフロー(Authorization Code Flow)は以下の通りです。
- ブラウザが Cognito のログイン画面へリダイレクト
- Cognito のログイン画面でユーザーが認証を行う
- Cognito が 認可コード(code) を発行し、
ブラウザ経由でアプリのコールバックURLへリダイレクトする - サーバー(SSR)が code を受け取り、
Cognito のトークンエンドポイントへ問い合わせてトークンを取得する - サーバーがトークンを検証し、
セッションを確立した上で HttpOnly Cookie を発行する - 検証済みの状態に基づいてレスポンスをブラウザへ返す
このフローで重要なのは、トークンはブラウザに直接渡されるのではなく、
事前にサーバー側で交換・検証される点です。
Step2: PKCE(Proof Key for Code Exchange)
ここで一度、攻撃者の立場に立って、
認可コードフローにどのような攻撃余地があるかを考えてみましょう。
まず思い浮かぶのが、認可コードの奪取
(Authorization Code Interception)です。
Cognito のログイン画面でユーザーが認証を行うと、
Cognito はブラウザに 302 リダイレクトレスポンスを返します。
その Location ヘッダに含まれる URL に、認可コード(code)が含まれます。
つまり、認可コードは一時的にブラウザのアドレスバーに現れるのです。
悪意あるアプリや拡張機能、あるいは特定の実装ミスによって code が盗まれた場合、
攻撃者がその code をトークンに交換できてしまう可能性があります。
ここで登場するのが PKCE です。
PKCE は一言で言うと、
「その code を発行した正規クライアントだけが、トークンに交換できる」
ことを保証する仕組みです。
PKCE の登場人物は次の2つです。
code_verifier:クライアントが秘密として保持するランダム文字列(原本)
code_challenge:code_verifier から生成したハッシュ値(公開)
認可リクエストを送る前に、サーバー(SSR)で
code_verifier と code_challenge のペアを生成します。
ブラウザが Cognito のログイン画面へリダイレクトする際に、
code_challenge(ハッシュ値)を送信します。
Cognito はこの code_challenge を認可コードに紐づけて保持します。
その後、認可コード(code)を受け取ったサーバー(SSR)が、
トークンエンドポイント(Cognito)に code と code_verifier(原本)を送信します。
Cognito は保存していた code_challenge と、
受け取った code_verifier から再計算した値を比較し、
一致した場合のみトークンを発行します。
つまり、
code を盗まれても、code_verifier が無ければトークンに交換できない
という状態を作り出すのが PKCE です。
PKCE は『code を盗ませない』仕組みではなく、
『盗まれても使えなくする』 仕組みである
というのがポイントです。
Step3: state の役割(CSRF対策)
次は state です。
code を盗む以外にも、攻撃者はこんな攻撃を考えます。
「標的に気づかれないように、認証フローをすり替え、
攻撃者自身のアカウントでログインさせてしまおう。
その結果、標的が自分のアカウントだと誤認したまま、
機密情報を入力してしまうように誘導しよう。」
これは CSRF(Cross-Site Request Forgery)の一種で、
特に OAuth の文脈では Login CSRF と呼ばれます。
攻撃の流れはこうです。
- 攻撃者が自分のアカウントで認可コードを取得する
- その code を含むURLを標的に踏ませる
- 標的のブラウザがそのURLにアクセス
- アプリがその code を正規のものと誤認してトークン交換
- 標的は「自分のアカウントにログインした」と勘違いする
これを防ぐのが state です。
state は、
「この認証フローは、確かに自分が開始したものか?」
を検証するためのランダム値です。
基本的な実装は次の通りです。
- 認証開始時にランダムな state を生成する
- それを Cookie やサーバー側セッション(DynamoDBなど)に保存する
- 認可リクエスト時に state を送信する
- コールバック時に返ってきた state と照合する
- 一致しなければ破棄する
これにより、
『自分が開始していない認証フロー』
を検知し、無効化することができます。
重要なのは、state が
認証フローの整合性を保証するための仕組みであり、
認可コードの奪取を防ぐものではないという点です。
state は
『その認証フローが自分のものであるか』
を確認するための仕組みというわけです。
Step4: nonce の役割(ID Token 防御)
ここまで、
- 認可コードを正しい依頼者だけが交換できるようにする(PKCE)
- 認可フローを開始したのが自分であることを確認する(state)
といったように、
認証手続きそのものの正当性を守る仕組みを見てきました。
次に登場する nonce は、
発行された ID Token そのものの正当性 を確認するための仕組みです。
認可リクエスト時にランダムな nonce を送信すると、
Cognito はその値を ID Token の中に埋め込んで返します。
コールバック時に受け取った ID Token を検証し、
内部に含まれる nonce が、自分が送信した nonce と一致するかを確認します。
これにより、
「この ID Token は、確かに自分が開始した認証の結果として発行されたものだ。」
と確認することができます。
state が 『認証フローの整合性』 を守る仕組みであるのに対し、
nonce は 『ID Token のリプレイや差し替え』 を防ぐための仕組みです。
Step5: Access Token はどう守るのか?
ここまでで、
PKCE によって code の交換権限を守り、
state によって認証フローの整合性を守り、
nonce によって ID Token の正当性を守りました。
では、Access Token はどのように検証するのでしょうか?
Access Token は、API へのアクセス権を示すトークンです。
つまり、受け取った側(Resource Server)は、
- 署名は正しいか
- 有効期限は切れていないか
- 発行者(iss)は正しいか
- Audience(aud)または client_id は正しいか
- Scope は適切か
といった点を検証する必要があります。
Cognito の Access Token は多くの構成で JWT 形式です。
JWT の署名は、Cognito が公開している JWK(公開鍵) を使って
検証することができます。
Access Token の検証責務は、
本来 Resource Server(API側) にあります。
今回の構成では、Next.js(SSR)がその役割も兼ねることになります。
5.Tokenの検証(JWTの中身を見る)
ここからは、実際に発行されたトークンの中身を確認しながら、
- どのクレームを
- なぜ
- どのように
検証すべきかを整理していきます。
Cognito が発行する ID Token / Access Token は、
多くの構成で JWT(JSON Web Token)形式です。
JWT は次の3つの部分で構成されています。
- header:署名アルゴリズムや鍵情報(kid)
- payload:ユーザー情報や有効期限などのクレーム
- signature:改ざん検知のための署名
検証の本質は、
『このJWTが、正しく発行され、改ざんされておらず、今も有効か』
を確認することです。
ID Token の検証
ID Token は「誰がログインしたか」を示すトークンであり、
SSRで検証し、CSRで使用します。
トークンに含まれる次の項目を検証することができます。
1.署名(signature)
- JWK(公開鍵)を取得
- headerの kid に一致する鍵をJWKから抽出し、署名検証
これにより、このトークンは本当に Cognito が発行したものかを確認できます。
署名検証をしない場合、攻撃者が自作したJWTを受け入れてしまう可能性があります。
2.発行者(iss)
想定しているユーザープールと一致するかを確認できます。
これにより、別の認証基盤が発行したトークンを
誤って受け入れないことが保証されます。
3.Audience(aud)
ID Token の aud には App Client ID が入ります。
これを確認することで、このトークンが
自分のアプリ向けに発行されたものかを確認できます。
4.有効期限(exp)
- exp:有効期限。現在時刻が exp を超えていないことを確認する
- nbf:この時刻以前は無効であることを示す(存在する場合)
- iat:発行時刻。異常な未来時刻でないかを確認できる
JWTは発行された瞬間に有効でもなければ、永久に有効でもありません。
トークンは時間制約付きの主張であるというわけです。
5.nonce(前章参照)
ID Token 内の nonce が、認証開始時に送信した値と一致するか確認します。
これにより、トークンのリプレイや差し替えを防ぎます。
Access Token の検証
Access Token は「何ができるか」を示すトークンであり、
Resource Server(API)、今回の構成では、Next.js(SSR)が
その役割を兼ねます。
トークンに含まれる次の項目を検証することができます。
1.署名
ID Token と同様に、JWK を使って署名を検証します。
2.発行者(iss)
ID Token と同様に確認します。
3.Audience(aud)
Cognito の Access Token では、
client_id クレームに App Client ID が含まれることが多いです。
想定するアプリクライアントであることを確認します。
4.有効期限(exp)
期限切れでないことを確認します。
5.token_use
Cognito のトークンには token_use が含まれます。
- ID Token : "id"
- Access Token : "access"
これを確認することで、ID Token を誤って Access Token として扱う
といったミスを防ぐことができます。
6.Scope
Access Token には scope が含まれます。
scope は、そのトークンが持つ「操作範囲」を示します。
Scope には大きく2種類あります。
- API向けスコープ(API操作権限)
- OIDC標準スコープ(ユーザー属性へのアクセス範囲)
a.API向けスコープ
API向けスコープは read:profile や write:post のように定義し、
API側で Access Token を検証する際の認可制御に利用します。
エンドポイントごとに必要な scope を定義し、
含まれていない場合は 403 を返す、といった制御が可能です。
b.OIDC標準スコープ
OIDC標準スコープは openid profile email のように指定します。
-
openidは ID Token を発行させるために必須のスコープ -
profileやemailは、対応するユーザー属性へのアクセスを要求するスコープ
これらは API操作権限ではなく、
ID Token や UserInfo に含まれるユーザー属性の範囲を指定するためのものです。 - ID Token は『認証の証明』
- Access Token は『認可の証明』
どちらも単なる文字列ではなく、
検証されて初めて意味を持ちます。
そして今回の設計では、
その検証責務を SSR 側に集約する
という思想を採用しています。
6.Next.js(SSR)を認証境界にする設計
ここまでで、OAuth / OIDC / PKCE / state / nonce / JWT 検証といった
『トークンを安全に発行・検証するための部品』 を整理してきました。
この章では、それらを踏まえた上で、
本記事の結論である Next.js(SSR)を認証境界にする設計 を説明します。
ポイントはシンプルです。
-
ブラウザ(CSR)ではセキュリティ上の判断を行わない
ブラウザ上の値(LocalStorage、JS変数、Zustandなど)は改ざん可能です。
そのため、ブラウザ(CSR)の処理は信頼しません。
認証・認可の判断はブラウザでは行いません。
-
セキュリティ上の判断はすべて SSR 側に集約する
PKCE / state / nonce の照合、JWT の署名検証(iss/aud/exp/token_use など)、
リダイレクト先の制限(open redirect対策)といった
セキュリティ上の判断 は SSR のみが行います。
-
CSRは「検証済みの結果」だけを扱う
CSRはトークンを保持・検証せず、
SSRが確立したセッション(HttpOnly Cookie)を前提に、
画面の表示切り替えやリダイレクトを行うだけに留めます。
-
Cognito は「入口」、継続ログインは「アプリセッション」
Cognito(Hosted UI)はログインとトークン発行を担当し、
アプリ側では sessionId(HttpOnly Cookie)+ DynamoDB により
セッションを管理します。
これにより Refresh Token をブラウザに保持せずに運用できます。
つまり、この設計は
「トークンを守る」のではなく、「信頼できる境界を明確にする」設計です。
ブラウザはあくまで表示層として扱い、
改ざん可能な領域にセキュリティ判断を持ち込まないことで、
認証・認可の責務をシンプルに保ちます。
そのうえで、SSRを唯一の信頼できる境界とし、
すべての検証と判断をそこに集約することで、
安全性と実装の見通しを両立した認証アーキテクチャを実現します。
認証アーキテクチャ図
ディレクトリ構成
sample/
├─ amplify/
│ └─ backend の定義
├─ app/
│ └─ frontend (nextjs) の定義
├─ components/
│ └─ nextjs が参照するクライアントコンポーネントの定義
├─ lib/
│ ├─ auth/
│ │ ├─ cognito/
│ │ │ └─ nextjs からcognitoに連携するための関数を定義
│ │ ├─ flows/
│ │ │ └─ login, callback, logout 各APIから呼ばれる関数を定義
│ │ ├─ oauth/
│ │ │ └─ PKCE用code_verifier,code_challengeおよび
│ │ │ CSRF用stateを発行する関数を定義
│ │ ├─ oidc/
│ │ │ └─ idToken解析、nonce発行のための関数を定義
│ │ ├─ session/
│ │ │ └─ cookieを読み書きする関数を定義
│ │ └─ utils/
│ │ └─ Encodeなど汎用関数を定義
│ ├─ ddb/
│ │ └─ DynamoDBにアクセスする関数を定義
│ ├─ http/
│ │ └─ Requestヘッダーを読み書きする関数を定義
│ └─ utils/
│ └─ SSMパラメータへのアクセスなど汎用関数を定義
└─ ...
7.まとめ
Amplify は便利ですが、
責務境界を設計しなければ 『なんか知らんけど動いた』 で終わってしまいます。
SSR を認証境界とすることで、
トークン・セッション・責務の所在を明確にし、
『説明できる認証アーキテクチャ』 に昇華させることができます。
本記事で整理したポイントは次の3つです。
- OAuth / OIDC / PKCE / state / nonce の役割を分解すること
- トークンは「身分証」ではなく「検証可能な主張」であること
- 認証・認可の判断は SSR に集約し、ブラウザを信頼しないこと
これらを意識するだけで、
Amplify を使った認証実装はブラックボックスではなくなります。
いかがでしたでしょうか?
『なんか知らんけど動いた』から、
『なぜ動いているか説明できる』状態へ。
皆さんが理解を深める上で
少しでもお役に立てたなら嬉しいです。
それでは、次回の記事でお会いしましょう!
付録1.ソースコード(GitHub リポジトリ)
この記事で解説した構成(Next.js / Amplify)の
サンプルを GitHub に公開しています。
📦 GitHub リポジトリ
https://github.com/dellacraft/qiita-sample-amplify-oidc
付録2.クイックスタート
このリポジトリは、以下の手順でローカル起動およびデプロイができます。
1. テンプレートからリポジトリを作成
GitHub の「Use this template」ボタンから新しいリポジトリを作成します。
2. ローカルにクローン
git clone https://github.com/your-repo.git
cd your-repo
3. 依存関係のインストール(pnpm)
pnpm install
4. ローカル起動
pnpm dev
ブラウザで http://localhost:3000 を開きます。
5. Amplify にデプロイ
- Amplify コンソールで新規アプリを作成
- GitHub リポジトリを接続
- amplify/auth/oauthUrls.tsにて、appDomainNamesにAmplifyのドメインを追加
- デプロイ実行
💡 補足
- 認証は Cognito Hosted UI を利用しています
- セキュリティ判定はすべて SSR 側で行います
- CSR 側は表示専用(props経由のデータのみ使用)
❓ トラブルシュート
-
依存関係でエラーが出る場合
→pnpm install --frozen-lockfileを試してください -
Amplify デプロイに失敗する場合
→ 環境変数(AWS_APP_ID / AWS_BRANCH)を確認してください


