🐾
Next.js × Auth.js(NextAuth) × Cognitoでカスタムログイン画面を作成しセッション管理をする
環境
ライブラリ・フレームワーク・言語 | バージョン |
---|---|
next | 13.4.19 |
react | 18.2.0 |
react-dom | 18.2.0 |
typescript | 5.2.2 |
next-auth | 4.24.5 |
@aws-sdk/client-cognito-identity-provider | 3.454.0 |
※Next.jsはpage routerを使用しています
前提条件
- 一般的な認証機能付きのwebアプリケーションの開発を前提
- メールアドレス、パスワードで認証する
- バックエンドのAPIは別で準備されている
- 認証後、Authorization: Bearer ヘッダにIDトークン(またはアクセストークン)を入れて、バックエンドのAPIを叩くことでアプリケーションとして成立する
- トークンの有効期限が切れた場合はリフレッシュトークンを使用し更新する
手順
1. cognito側の設定
2. ログイン画面のUIを作成
お好きなデザインのログイン画面を作ってください
3. Auth.jsの設定
- ライブラリのインストール
npm i next-auth @aws-sdk/client-cognito-identity-provider
- .env.localを作成
任意の値を入れてくださいCOGNITO_CLIENT_ID= COGNITO_CLIENT_SECRET= COGNITO_REGION= COGNITO_USER_POOL_ID= NEXTAUTH_SECRET=
- pages/api/auth/[...nextauth].tsを作成[...nextauth].ts
import NextAuth, { NextAuthOptions } from 'next-auth' import { Issuer } from 'openid-client' import { jwtDecode } from 'jwt-decode' import * as crypto from 'crypto' import CognitoProvider from 'next-auth/providers/cognito' import { getSecretHash } from '@/features/auth' import { CognitoIdentityProvider, InitiateAuthCommand, } from '@aws-sdk/client-cognito-identity-provider' import CredentialsProvider from 'next-auth/providers/credentials' const cognitoProvider = CognitoProvider({ clientId: process.env.COGNITO_CLIENT_ID || '', clientSecret: process.env.COGNITO_CLIENT_SECRET || '', issuer: process.env.COGNITO_ISSUER, }) // ** リフレッシュトークンを使用してID、アクセストークンを更新する関数 const refreshAccessToken = async (refreshToken?: string) => { if (!refreshToken) { return null } const client_id = process.env.COGNITO_CLIENT_ID ?? '' const client_secret = process.env.COGNITO_CLIENT_SECRET ?? '' const issuer = await Issuer.discover(cognitoProvider.wellKnown ?? '') const token_endpoint = issuer.metadata.token_endpoint ?? '' const basicAuthParams = `${client_id}:${client_secret}` const basicAuth = Buffer.from(basicAuthParams).toString('base64') const params = new URLSearchParams({ client_id, client_secret, grant_type: 'refresh_token', refresh_token: refreshToken, }) try { const response = await fetch(token_endpoint, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${basicAuth}`, }, method: 'POST', body: params.toString(), }) const newTokens = await response.json() return { idToken: newTokens.id_token, accessToken: newTokens.access_token, } } catch (error) { console.error('Error refreshing access token') throw error } } export const authOptions: NextAuthOptions = { providers: [ CredentialsProvider({ name: 'Cognito', credentials: { email: { label: 'Email', type: 'text' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { const cognitoClient = new CognitoIdentityProvider({ region: process.env.COGNITO_REGION, }) const email = credentials?.email ?? '' const password = credentials?.password ?? '' try { const response = await cognitoClient.send( new InitiateAuthCommand({ AuthFlow: 'USER_PASSWORD_AUTH', ClientId: process.env.COGNITO_CLIENT_ID, AuthParameters: { USERNAME: email, PASSWORD: password, SECRET_HASH: getSecretHash(email), }, }), ) if (response.AuthenticationResult) { if (!response.AuthenticationResult.IdToken) { throw new Error('No Id Token') } const { IdToken, AccessToken, ExpiresIn, RefreshToken } = response.AuthenticationResult return { email, idToken: IdToken, accessToken: AccessToken, expiresIn: ExpiresIn, refreshToken: RefreshToken, } } else { throw new Error('No Auth Response Result') } } catch (error) { throw new Error('Auth Error') } }, }), ], callbacks: { async jwt({ token, user }) { if (user) { token.idToken = user.idToken token.accessToken = user.accessToken token.expiresIn = user.expiresIn token.refreshToken = user.refreshToken token.email = user.email } const decodedToken = jwtDecode(token.idToken) const currentTime = Math.floor(Date.now() / 1000) if (decodedToken.exp && decodedToken.exp < currentTime) { try { // 有効期限が切れている場合、リフレッシュトークンを使用してトークンを更新 const refreshedTokens = await refreshAccessToken(token.refreshToken) if (refreshedTokens?.idToken && refreshedTokens?.accessToken) { token.idToken = refreshedTokens.idToken token.accessToken = refreshedTokens.accessToken } else { throw new Error() } } catch (error) { // リフレッシュトークンの期限が切れたorリフレッシュできなかった場合 token.error = 'RefreshTokenError' } } return token }, async session({ session, token }) { // 必要に応じてトークンを暗号化させてください session.idToken = token.idToken session.accessToken = token.accessToken session.error = token.error return session }, }, secret: process.env.NEXTAUTH_SECRET, } export default NextAuth(authOptions)
- next-authの型定義を拡張
デフォルトの型定義だと、上記の設定でエラーが出るため、型定義を拡張させます
types/next-auth.d.tsを作成next-auth.d.tsimport 'next-auth' declare module 'next-auth' { interface Session { idToken?: string accessToken?: string error?: string } interface User { id?: string idToken?: string accessToken?: string refreshToken?: string expiresIn?: number } } declare module 'next-auth/jwt' { interface JWT { idToken?: string accessToken?: string refreshToken?: string expiresIn?: number error?: string } }
4. ログイン、ログアウト処理の実装
import { signIn, signOut } from 'next-auth/react'
export type LoginParams = {
email: string
password: string
}
export const login = async (params: LoginParams) => {
const { email, password } = params
const result = await signIn('credentials', {
email,
password,
redirect: false,
})
return result
}
export const logout = async () => {
await signOut({ redirect: false })
}
※エラーハンドリング、その他の処理は任意で記述してください
5. IDトークンを取得しAPIを叩く
import axios from 'axios'
import { getSession } from 'next-auth/react'
export const getAxiosInstanceWithToken = async () => {
const session = await getSession()
if (!session) {
throw new Error()
}
if (session?.error === 'RefreshTokenError') {
// リフレッシュトークンの期限切れ時の処理
}
return axios.create({
headers: {
Authorization: `Bearer ${session.idToken}`,
},
})
}
// APIを叩く処理
export const getPosts = async () => {
const axiosInstance = await getAxiosInstanceWithToken()
const response = await axiosInstance.get('任意のエンドポイント')
}
※エラーハンドリング、その他の処理は任意で記述してください
Discussion