登場人物
A は認証に必要な情報と共に、B に JWT をリクエストします。A は Cookie に JWT を保存します。A は C に JWT と共に、必要な情報をリクエストします。C は JWT を検証し、認証・認可チェックを行い、問題なければ A が必要とする情報をレスポンスします。
B は JWT の生成の際に秘密鍵を使って署名します。秘密鍵は、対称キーと非対称キーがあります。非対称キーは公開鍵・秘密鍵に分かれます。B は秘密鍵を使い、C は公開鍵で検証します。対称キーの場合、B の JWT 署名も C の検証も同一のキーを使います。(漏洩時のリスクは対称キーの方が大きいです)
署名の検証により、検証に使う鍵が不正であったり、JWT の内容が改ざんされていたりといったことをチェックする。また、署名の検証に問題がなければ、有効期限が切れていないかをチェックする。
その他、sub に user_id などが入っているので、認可チェック等も合わせて行う。
注意点として、JWT は署名の方式を選択でき、非常に弱い署名だったり、署名しないといった選択が仕様として可能。よって、JWT の Header に記載されている署名方式に沿う形で検証しようとすると、署名の有効性チェックをスルーするような事態が起こりうる。基本的には署名方式を決めて、チェックする側も決められた方式でチェックするとよさそう。
NextAuth で Github 認証等の OAuth 連携をする場合の仕組み概要
登場人物
NextAuth で OAuth 認証をする場合、B でやるのは、ログインしてアクセストークンをもらい、それを使ってユーザ情報を取得するだけ。B で JWT を作成・発行してもらうわけではない。
B で認証 OK となりユーザ情報をゲットしたら、その情報を元に N 自体が ID Provider となり、セッショントークンを作成する。セッショントークンは DB 保存する形式も、JWT にすることも可能。
NextAuth のデフォルトは、NEXTAUTH_SECRET を対称キーとして署名する形式です。Payload もデフォルトで暗号化されます。よって、Payload が途中で読み取られるリスクが低いですが、非対称キーと比べるとキー漏洩時のリスクが高いです。今回はデフォルトの状態でコード例を作成します。非対称キーを使う場合等は、下記の[…nextauth].ts 内で jwt の encode 関数と decode 関数を上書きします。(参考 )
https://github.com/edo1z/nestjs-nextjs-nextauth-rest-example
下記で NextAuth の各種設定をしています。Prisma アダプタ等で DB 連携するとデフォルトは DB を利用したらセッション管理になるようですが、session.strategy に jwt を設定することで、JWT で管理します。
import NextAuth from 'next-auth';
import type { NextAuthOptions } from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { prisma } from '@mypj/database';
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID ?? '',
clientSecret: process.env.GITHUB_SECRET ?? ''
})
],
session: {
strategy: 'jwt',
maxAge: 60 * 60 * 24
},
jwt: {
maxAge: 60 * 3
}
};
export default NextAuth(authOptions);
下記は自分の最新の記事を取得するコード例です。記事一覧取得 API にリクエストする際に JWT を Authorization ヘッダに付与しています。
import { getJwt } from '@/utils/auth/getJwt';
import { ApiError } from '@/errors/apiError';
import { NextApiRequest } from 'next';
export async function getLatestPosts(req: NextApiRequest) {
const token = await getJwt(req);
const baseurl = process.env.API_URL ?? '';
const url = `${baseurl}/posts`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`
}
});
if (!res.ok) {
const error = await res.json();
const message = error.message || res.statusText;
throw new ApiError(res.status, message);
}
return await res.json();
}
Authorization ヘッダに付与するための JWT を取得するコード例です。next-auth/jwt のgetToken 関数 を使っています。getToken 関数の引数には secret も追加できます。secret 未指定の場合はデフォルトで NEXTAUTH_SECRET を利用します。raw を true にすると encode 済みの状態の JWT を取得できます。
import type { NextApiRequest } from 'next';
import { getToken } from 'next-auth/jwt';
import { ApiError } from '@/errors/apiError';
export async function getJwt(req: NextApiRequest): Promise<string> {
const token = await getToken({ req, raw: true });
if (!token) throw new ApiError(401, 'jwt is none');
return token;
}
NextAuth のデフォルトは Payload も暗号化しています。next-auth/jwt の decode を使うと簡単に複合と署名の検証ができます。改竄や不正 secret が原因で署名の検証に失敗するとエラーになります。署名の検証に成功すると有効期限のチェックもします。request.user に payload をセットすることで、controlle 等で認証ユーザ情報を利用できます。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { decode } from 'next-auth/jwt';
@Injectable()
export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authorization = request.headers?.authorization;
if (!authorization) return false;
const token = authorization.split(' ')[1];
if (!token) return false;
const secret = process.env.NEXTAUTH_SECRET ?? '';
if (!secret) return false;
try {
const decoded = await decode({ token, secret });
if (!decoded) return false;
request.user = decoded;
return true;
} catch (error) {
console.error(error);
return false;
}
}
}