Open5
Next15 × Cognito でtokenをリフレッシュする
モチベーションとか
- Next歴0ヶ月のエケチェンが仕事で最近Nextを触る機会があったので、やったことや詰んだことのメモ
- キャッチアップの思考整理
前提
- フロントエンドとバックエンドを RESTful API により分離し、HTTP 通信を通じてデータの送受信を行う構成となっています。
- ユーザー認証は AWS Cognito を用いて行い、認証成功時に発行される ID トークン(idToken)を用いてフロントエンドからバックエンド API へのリクエストに対する認可を行います。
- Nextは15
やりたいこと
- バックエンドとAPIでデータ送受信を行う際に、リクエストヘッダーにidTokenを差してるんだけども、有効期限が1時間なので、期限が切れたらリフレッシュしてリクエスト認可を通したい。
ここでやらないこと
- Cognitoの設定と画面からログインするまで
- 良さげなエラーハンドリング、エラー定義など
API Routes
認証周りのルーティング。
リフレッシュするのにシークレットハッシュを作成する必要があり、cryptoを使いたかったのでこの構成にしてrefresh-tokenはNodeで動かすよう明示的に宣言した。
app
├── api
│ └── auth
│ ├── [...nextauth]
│ │ ├── auth-options.ts
│ │ └── route.ts
│ └── refresh-token
│ └── route.ts
REFRESH TOKEN
- generateSecretHashでuserNameを引数で渡してる
- https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash
- userNameに何を渡す?
- ここはCognito ユーザプールでのサインイン属性やら設定やらに依存しそう。ちょっと詰まった。
refresh-token/route.ts
export const runtime = 'nodejs'; // cryptoがNodeでしか動かせないので
import crypto from 'crypto';
import {
CognitoIdentityProvider,
InitiateAuthCommandInput,
} from '@aws-sdk/client-cognito-identity-provider';
import { NextRequest, NextResponse } from 'next/server';
import { env } from '@/env.mjs';
function generateSecretHash(
userName: string,
clientId: string,
clientSecret: string
): string {
return crypto
.createHmac('sha256', clientSecret)
.update(`${userName}${clientId}`)
.digest('base64');
}
async function refreshAccessToken({
refreshToken,
userName,
}: {
refreshToken: string;
userName: string;
}) {
const cognito = new CognitoIdentityProvider({ region: 'ap-northeast-1' });
const secretHash = generateSecretHash(
userName, // Cognitoのサインイン設定によって何を渡して良いか変わりそう。今回はproviderAccountIdを渡す
env.COGNITO_CLIENT_ID,
env.COGNITO_CLIENT_SECRET
);
const params: InitiateAuthCommandInput = {
AuthFlow: 'REFRESH_TOKEN',
ClientId: env.COGNITO_CLIENT_ID,
AuthParameters: {
REFRESH_TOKEN: refreshToken,
SECRET_HASH: secretHash,
},
};
const res = await cognito.initiateAuth(params);
const result = res.AuthenticationResult!;
return {
accessToken: result.AccessToken,
idToken: result.IdToken ?? null,
expiresIn: result.ExpiresIn,
};
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
if (!body.refreshToken || !body.userName) {
return NextResponse.json(
{ error: 'Missing refreshToken or userName' },
{ status: 400 }
);
}
const newTokens = await refreshAccessToken({
refreshToken: body.refreshToken,
userName: body.userName,
});
return NextResponse.json(newTokens);
} catch (err) {
console.error('[refresh-token API] Failed to refresh:', err);
return NextResponse.json({ error: 'Refresh failed' }, { status: 500 });
}
}
jwtコールバック
jwtのコールバック内でidTokenの有効期限が切れていたら、refresh-tokenに定義したエンドポイントを叩きます。
[...nextauth]/auth-options.ts
export const { auth, handlers, signIn, signOut } = NextAuth({
debug: process.env.NODE_ENV === 'development',
secret: env.AUTH_SECRET,
trustHost: true,
providers: [
CognitoProvider({
clientId: env.COGNITO_CLIENT_ID,
clientSecret: env.COGNITO_CLIENT_SECRET,
issuer: env.COGNITO_ISSUER,
}),
],
callbacks: {
async jwt({ token, account, user }) {
if (account && user) {
// 初回サインイン時にトークンを保存
token.idToken = account.id_token;
token.refreshToken = account.refresh_token;
token.accessTokenExpires =
Date.now() + (account.expires_at || 3600) * 1000;
token.userName = account.providerAccountId;
return token;
}
// 有効期限チェック
if (Date.now() < (token.accessTokenExpires as number) - 60 * 1000) {
return token;
}
// リフレッシュに必要な要素がない → セッション無効として扱う
if (!token.refreshToken || !token.userName) {
return {
...token,
error: 'MissingRefreshInfo',
};
}
// 有効期限切れならリフレッシュ
try {
const res = await fetch(`${env.AUTH_URL}/api/auth/refresh-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: token.refreshToken,
userName: token.userName,
}),
});
if (!res.ok) {
throw new Error('Refresh failed');
}
const refreshed = await res.json();
token.idToken = refreshed.idToken;
token.accessTokenExpires = Date.now() + refreshed.expiresIn * 1000;
console.log('[JWT refresh] Access token refreshed!!!');
return token;
} catch (err) {
console.error('[JWT refresh] Failed to refresh access token', err);
return { ...token, error: 'RefreshAccessTokenError' };
}
},