JWT 関連の RFC (by ChatGPT4)
- RFC 7519 - JSON Web Token (JWT): JWT の基本概念、データ構造、エンコーディング手順を定義しています。これは JWT の基本的な仕様を提供するドキュメントです。
- RFC 7520 - Examples of Protecting Content Using JSON Object Signing and Encryption (JOSE): JSON Web Signature (JWS)と JSON Web Encryption (JWE)を使用して、JWT と他の JOSE オブジェクトを保護する方法に関する例を示しています。
- RFC 7515 - JSON Web Signature (JWS): JWT の署名部分を担当する JSON Web Signature (JWS)の仕様を定義しています。
- RFC 7516 - JSON Web Encryption (JWE): JWT の暗号化部分を担当する JSON Web Encryption (JWE)の仕様を定義しています。
- RFC 7517 - JSON Web Key (JWK): JSON 形式で表現される暗号鍵の仕様を定義しています。JWK は、JWT の署名と暗号化に使用される鍵を表現するために使用されます。
- RFC 7518 - JSON Web Algorithms (JWA): JWT、JWS、JWE で使用される暗号アルゴリズムを定義しています。これには、デジタル署名や暗号化アルゴリズム、鍵管理アルゴリズムが含まれます。
JWT の仕組み概要
- Header, Payload, Signature の 3 つをドットでつなげたものが JWT。
- Header, Payload は Json オブジェクトを Base64 Encode した結果。
- Signature は Encode 済みの Header と Encode 済み Payload をドットでつなげたものを、秘密鍵でデジタル署名した結果。
- Payload は Base64 Decode すれば内容が確認可能。
- ただし、Payload 自体を暗号化する方式もある。
- ただし、秘密鍵に対応する公開鍵で Signature を検証し、Encode 済みの Header と Encode 済み Payload をドットでつなげたものと内容が合致するか確認することで、改ざんされていないかの確認が可能。
JWT による認証フローの概要
登場人物
- Client ... A
- ID Provider ... B
- API ... C
A は認証に必要な情報と共に、B に JWT をリクエストします。A は Cookie に JWT を保存します。A は C に JWT と共に、必要な情報をリクエストします。C は JWT を検証し、認証・認可チェックを行い、問題なければ A が必要とする情報をレスポンスします。
B は JWT の生成の際に秘密鍵を使って署名します。秘密鍵は、対称キーと非対称キーがあります。非対称キーは公開鍵・秘密鍵に分かれます。B は秘密鍵を使い、C は公開鍵で検証します。対称キーの場合、B の JWT 署名も C の検証も同一のキーを使います。(漏洩時のリスクは対称キーの方が大きいです)
JWT の検証
署名の検証により、検証に使う鍵が不正であったり、JWT の内容が改ざんされていたりといったことをチェックする。また、署名の検証に問題がなければ、有効期限が切れていないかをチェックする。
その他、sub に user_id などが入っているので、認可チェック等も合わせて行う。
注意点として、JWT は署名の方式を選択でき、非常に弱い署名だったり、署名しないといった選択が仕様として可能。よって、JWT の Header に記載されている署名方式に沿う形で検証しようとすると、署名の有効性チェックをスルーするような事態が起こりうる。基本的には署名方式を決めて、チェックする側も決められた方式でチェックするとよさそう。
NextAuth の仕組み概要
NextAuth で Github 認証等の OAuth 連携をする場合の仕組み概要
登場人物
- Client … A (Next.js を想定)
- NextAuth … N (A の Next.js 内の NextAuth を想定)
- Github … B (Github 認証を使う場合を想定)
- API … C (NestJS で作った API を想定)
NextAuth で OAuth 認証をする場合、B でやるのは、ログインしてアクセストークンをもらい、それを使ってユーザ情報を取得するだけ。B で JWT を作成・発行してもらうわけではない。
B で認証 OK となりユーザ情報をゲットしたら、その情報を元に N 自体が ID Provider となり、セッショントークンを作成する。セッショントークンは DB 保存する形式も、JWT にすることも可能。
NextAuth のデフォルトは、NEXTAUTH_SECRET を対称キーとして署名する形式です。Payload もデフォルトで暗号化されます。よって、Payload が途中で読み取られるリスクが低いですが、非対称キーと比べるとキー漏洩時のリスクが高いです。今回はデフォルトの状態でコード例を作成します。非対称キーを使う場合等は、下記の[…nextauth].ts 内で jwt の encode 関数と decode 関数を上書きします。(参考)
サンプルコードの Github リポジトリ
https://github.com/edo1z/nestjs-nextjs-nextauth-rest-example
Next.js(A)と NextAuth のコード例
下記で 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; }
NestJS(C)のコード例
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; } } }