Cognito の認証や JWT は、概念だけを見るとどうしても分かりづらく、実装でつまずきやすいポイントです。
本記事では、ログイン〜APIを実行する一連の流れ全体 を「コードベース」で丁寧に解説し、実際の実装イメージがつかみやすいように構成しました。
この記事を読むと、次のことが理解できます:
- ユーザーがログインした瞬間に どのタイミングで JWT が発行されるか
- 発行された JWT が どこに保存され、どうやってフロントで使われるのか
- API リクエスト時に JWT が どのように送信されるか(Bearer トークン)
- バックエンドが JWT をどう検証してユーザーを認証しているか
- Cognito × Next.js × Express の構成での 具体的な実装パターン
ログインするまで
1. cognitoへログインリクエスト(フロントエンド)
- email, passを使用するログインフォーム送信コードがある
-
signIn関数を使ってログインリクエストをcognitoに対して送信する-
signInは、cognitoと直接通信する関数 -
signInは、以下の環境変数を読み込んで、通信先のcognitoを特定するNEXT_PUBLIC_COGNITO_USER_POOL_ID=ap-northeast-1_xxxxxxx # ユーザープールID NEXT_PUBLIC_COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxx # クライアントID NEXT_PUBLIC_COGNITO_REGION=ap-northeast-1 # リージョン
-
- ソースコード(フロントエンド)
import { signIn } from 'aws-amplify/auth'; async function login(email: string, password: string) { const res = await signIn({ username: email, password: password, }); console.log(res); }
2. cognitoがemail, passを検証し、JWTトークンを発行(cognitoの内部処理)
- 認証がOKならば、JWTトークンがブラウザのローカルストレージに送られて保存される
- 例
- キー
CognitoIdentityServiceProvider.<CLIENT_ID>.<USERNAME> - 値
{ "UserAttributes": [ { "Name": "sub", "Value": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, { "Name": "email", "Value": "john.doe@example.com" }, { "Name": "email_verified", "Value": "true" } ], "SignInUserSession": { "IdToken": { "jwtToken": "eyJraWQiOiJLT2p...長いJWTトークン...", "payload": { "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "email": "john.doe@example.com", "token_use": "id", "exp": 1712345678, "iat": 1712342078 } }, "AccessToken": { "jwtToken": "eyJraWQiOiJLT2p...別のJWT...", "payload": { ... } }, "RefreshToken": { "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, "ClockDrift": 0 }, "Username": "john.doe@example.com" }
- キー
3. ログイン後に見られる箇所が見られるようになる(フロントエンド)
- フロント側での制御部は以下のようになる
- 認証済みかどうか、フラグを設ける:
isAuth - 認証情報を取る関数
Auth.currentAuthenticatedUserを使用する - この関数も
signInと同じ環境変数を読む("aws-amplify"ライブラリがこれらの環境変数を読むものである)'use client'; import { useEffect, useState } from "react"; import { Auth } from "aws-amplify"; import { useRouter } from "next/navigation"; export function ProtectedComponent({ children }: { children: React.ReactNode }) { // null: 認証中, true: 認証成功, false: 認証失敗 const [isAuth, setIsAuth] = useState<boolean | null>(null); useEffect(() => { async function checkAuth() { try { const user = await Auth.currentAuthenticatedUser(); setIsAuth(true); } catch { setIsAuth(false); } } checkAuth(); }, []); if (isAuth === null) return <p>認証中...</p>; if (!isAuth) return <p>パスワード、ユーザーが違います</p> return <>{children}</>; // 認証成功なら中身を表示 }
ログイン後のAPI
1. JWTトークンを取得し、リクエストに付与して送信(フロントエンド)
-
Auth.currentSessionで、ログイン済みユーザーのJWTセッション情報を取得- ブラウザのローカルストレージに保存されているJWT関連の情報を読み取って取得
-
session.getAccessToken().getJwtToken()で取得してセッション情報からAPIリクエスト用のBearerトークンを取得- Bearerトークンとは、
Bearer <JWTトークン>という文字列 - Bare認証方式であることを伝えるためのもの
- Bearerトークンとは、
- リクエストヘッダに、Bearerトークンをつけてリクエスト送信
import { Auth } from "aws-amplify"; async function fetchNotes() { try { // JWT を取得 const session = await Auth.currentSession(); const token = session.getAccessToken().getJwtToken(); // API にリクエスト const res = await fetch("/api/notes", { method: "GET", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!res.ok) throw new Error("Failed to fetch notes"); const data = await res.json(); return data; // メモ一覧 } catch (err) { console.error("メモ取得失敗:", err); throw err; } }
2. サーバー側でトークンが検証され、正しければフロントにpayloadを返す(バックエンド)
- JWTは、以下の構成で成り立っている
JWT = ヘッダー + 本文 + 署名- 公開鍵を使って署名されている
- これが、偽造されていないか確かめたい
- 検証する関数
jsonwebtokenのjwt.veryfyを使用する jwt.verify(token, publicKey, { algorithms: ['RS256'] });- ここで偽造が発覚すると、エラーをthrowする
- 認証OKなら、「本文」の部分が帰ってくる(以下のような)
{ "sub": "1234-5678-90", "email": "abc@example.com", "email_verified": true, "cognito:groups": [], "iat": 1234567890, "exp": 1234569999, "aud": "xxxxClientIdxxxx", "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_aaaaaaa" }
- 検証する関数
- 実際のバックエンドでは、まずトークン検証のための関数
verifyJwtTokenを作成する// verifyJwtToken.ts import jwt, { JwtPayload } from "jsonwebtoken"; import jwkToPem from "jwk-to-pem"; // ⓪ 環境変数読み込み const REGION = process.env.COGNITO_REGION!; # リージョン const USER_POOL_ID = process.env.COGNITO_USER_POOL_ID!; # ユーザープールID const JWK_URL = `https://cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}/.well-known/jwks.json`; // cognitoの公開鍵URL export async function verifyJwtToken(token: string): Promise<JwtPayload> { // 引数のtokenは、リクエストヘッダーに含まれていたJWTトークン // ① 公開鍵(JWK)を取得(キャッシュなし) const res = await fetch(JWK_URL); const { keys } = await res.json(); // ② JWTヘッダを読んで kid を取得 const decodedHeader = jwt.decode(token, { complete: true }) as any; if (!decodedHeader?.header?.kid) { throw new Error("Invalid token header"); } const kid = decodedHeader.header.kid; // ③ kid に対応する公開鍵を探す // ユーザープールには、複数の公開鍵あるので、どのキーかを特定する情報(kid=キーID)がある。 const jwk = keys.find((k: any) => k.kid === kid); if (!jwk) { throw new Error("Unknown kid"); } // ④ JWK → PEM(公開鍵)に変換 // ユーザープールにはJSON形式でキーが入っているが、jwt.verifyではpem形式を要求しているため変換 const publicKey = jwkToPem(jwk); // ⑤ JWT 検証(署名検証 + 有効期限チェック) return jwt.verify(token, publicKey) as JwtPayload; } - ミドルウェアで、この
verifyJwtTokenを使用してトークン認証必須にする -
authMiddlewareを作成// authMiddleware.ts import { Request, Response, NextFunction } from "express"; import { verifyJwtToken } from "./verifyJwtToken"; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const auth = req.headers.authorization; // リクエストヘッダーにトークンがなければエラーを吐いて終了 if (!auth || !auth.startsWith("Bearer ")) { return res.status(401).json({ message: "Token missing" }); } // JWTトークンを取り出す const token = auth.replace("Bearer ", "").trim(); try { // トークンを検証する const payload = await verifyJwtToken(token); req.user = payload; // reqにuser=JWTの中身という情報を付与して、本処理に渡す next(); // 本処理に進む } catch (err) { console.error("JWT verification error:", err); return res.status(401).json({ message: "Invalid token" }); } } - エンドポイントにミドルウェアを設定したサーバーは以下
// server.ts import express from "express"; import { authMiddleware } from "./authMiddleware"; const app = express(); app.use(express.json()); // 認証必須メモ API // ミドルウェアで、reqに付与されているJWTトークンが検証される // ミドルウェアを通過した場合のみ、メモがフロントに返却される app.get("/api/memos", authMiddleware, (req, res) => { console.log("User:", req.user); // ログ例 return res.json({ user: req.user, memos: [ { id: 1, text: "牛乳を買う" }, { id: 2, text: "Next.js + Cognito 完了!" } ] }); }); app.listen(3001, () => { console.log("Backend running at http://localhost:3001"); });
3. フロントエンドにpayloadが返ってくる(フロントエンド)
- フロントエンドで、バックエンドから送られてきたpayloadを受け取る
- UI上に表示したし、処理で使用できる