Leapcell: The Best of Serverless Web Hosting
Next-Authのソースコード解析:強力で柔軟な認証ソリューション
はじめに
Next-Authは、強力で柔軟な認証ライブラリであり、Next.jsアプリケーションやその他のReactプロジェクトに便利な認証機能を提供します。複数の認証方法をサポートしており、ソースコード構造も合理的に分割されており、開発者が理解しやすく、拡張しやすいものとなっています。この記事では、Next-Authのソースコード構造と重要な機能について詳細に分析します。
ディレクトリの紹介
Next-Authのコアソースコードは、packages/next-auth/src
ディレクトリに格納されています。以下は、このディレクトリの主な構造です:
-
client
:主にfetch
メソッドをカプセル化し、localStorage
の変化を監視するためのブロードキャストイベント機構を実装しています。 -
core
:主なビジネスロジックを含み、/api/auth/xxx
のAPIとページがここで定義されています。 -
jwt
:JWT(JSON Web Token)の暗号化と復号化メソッドを提供し、認証におけるトークン関連の操作を処理するために使用されます。 -
next
:Next.jsのミドルウェアを定義し、Next.jsアプリケーションに対して特定のサポートを提供します。 -
providers
:各種の認証方法のデフォルト設定を提供し、開発者が異なる認証プロバイダーを統合しやすくしています。 -
react
:Reactアプリケーションに対してuseSession
やgetToken
などのフロントエンドメソッドを提供し、ユーザーセッション情報の取得と更新に使用されます。 -
utils
:ルートの解析やデータのマージなど、いくつかの補助メソッドを定義し、一般的なタスクの処理を支援します。
また、index.ts
と middleware.ts
ファイルは、ライブラリ全体の初期化とミドルウェア処理において重要な役割を果たしています。
clientディレクトリの解析
client
ディレクトリは主に2つの重要な機能を提供しています:
-
Fetch
のカプセル化:ネットワークリクエストのfetch
メソッドをカプセル化しています。ネットワークリクエストに関するすべての操作は、このカプセル化されたメソッドを呼び出します。これにより、ネットワークリクエスト関連のロジックを一括で管理し、処理することができます。 -
ブロードキャストイベント機構:
localStorage
の変化を監視することで、異なるタブやウィンドウ間での通信を実現しています。コードは以下の通りです:
export function BroadcastChannel(name = "nextauth.message") {
return {
/** 他のタブ/ウィンドウからの通知を受け取る */
receive(onReceive: (message: BroadcastMessage) => void) {
const handler = (event: StorageEvent) => {
if (event.key!== name) return
const message: BroadcastMessage = JSON.parse(event.newValue?? "{}")
if (message?.event!== "session" ||!message?.data) return
onReceive(message)
}
window.addEventListener("storage", handler)
return () => window.removeEventListener("storage", handler)
},
/** 他のタブ/ウィンドウに通知する */
post(message: Record<string, unknown>) {
if (typeof window === "undefined") return
try {
localStorage.setItem(
name,
JSON.stringify({...message, timestamp: now() })
)
} catch {
/**
* localStorage APIは必ずしも利用可能ではありません。例えば、Safari 11以前のプライベートモードでは機能しません。
* エラーが発生した場合は、通知を単に破棄します。
*/
}
},
}
}
export interface BroadcastMessage {
event?: "session"
data?: { trigger?: "signout" | "getSession" }
clientId: string
timestamp: number
}
現在、このブロードキャストイベントの主なリスナーは、Reactの SessionProvider
です。localStorage
の変化が検出されると、SessionProvider
に定義されている __NEXTAUTH._getSession()
メソッドがトリガーされます。このメソッドは、/api/auth/session
APIをリクエストして、ユーザーセッションオブジェクトを取得するために使用されます。__NEXTAUTH._getSession()
メソッドには以下のような呼び出し方法があります:
-
__NEXTAUTH._getSession()
:最初に呼び出されるときに、セッション情報の初期取得に使用されます。 -
__NEXTAUTH._getSession({ event: "storage" })
:他のタブやウィンドウからメッセージが送信されてセッションを更新するときに呼び出され、循環的な更新を回避するために使用されます。 -
__NEXTAUTH._getSession({ event: "visibilitychange" })
:タブがアクティブになったときにトリガーされ、セッションを更新する必要があるかどうかをチェックするために使用されます。 -
__NEXTAUTH._getSession({ event: "poll" })
:セッションを定期的に更新し、セッション情報のリアルタイム性を保つために使用されます。
以下は、__NEXTAUTH._getSession()
メソッドの具体的な実装です:
React.useEffect(() => {
__NEXTAUTH._getSession = async ({ event } = {}) => {
try {
const storageEvent = event === "storage"
// まだクライアントセッションが存在しない場合、または他のタブ/ウィンドウからのイベントがある場合、常に更新する必要がある
if (storageEvent || __NEXTAUTH._session === undefined) {
__NEXTAUTH._lastSync = now()
__NEXTAUTH._session = await getSession({
broadcast:!storageEvent,
})
setSession(__NEXTAUTH._session)
return
}
if (
// セッションの有効期限が定義されていない場合、イベントがトリガーされるまでは既存の値を使用しても問題ない
!event ||
// クライアントにセッションが存在しない場合、サーバーにチェックを依頼する必要はない(他のタブ/ウィンドウでログインした場合は、"storage" イベントの形式で到来する)
__NEXTAUTH._session === null ||
// クライアントセッションがまだ有効期限内である場合、早期に終了する
now() < __NEXTAUTH._lastSync
) {
return
}
// イベントが発生したか、セッションが期限切れになったため、クライアントセッションを更新する
__NEXTAUTH._lastSync = now()
__NEXTAUTH._session = await getSession()
setSession(__NEXTAUTH._session)
} catch (error) {
logger.error("CLIENT_SESSION_ERROR", error as Error)
} finally {
setLoading(false)
}
}
__NEXTAUTH._getSession()
return () => {
__NEXTAUTH._lastSync = 0
__NEXTAUTH._session = undefined
__NEXTAUTH._getSession = () => {}
}
}, [])
reactディレクトリの解析
react
ディレクトリは、フロントエンドのReactプロジェクトに対して一連の実用的なメソッドとコンポーネントを提供しています:
-
SessionProvider
コンポーネント:通常、アプリケーション全体の最外層にラップされ、アプリケーションにセッションオブジェクトを提供し、アプリケーション全体でセッション情報にアクセスできるようにします。 -
useSession()
フック関数:SessionProvider
が提供するセッションオブジェクトを消費するために使用されます。セッションオブジェクトの型定義は以下の通りです:
export type SessionContextValue<R extends boolean = false> = R extends true
?
| { update: UpdateSession; data: Session; status: "authenticated" }
| { update: UpdateSession; data: null; status: "loading" }
:
| { update: UpdateSession; data: Session; status: "authenticated" }
| {
update: UpdateSession
data: null
status: "unauthenticated" | "loading"
}
-
signIn()
関数:ログインプロセスをトリガーします。OAuthログインの場合、/auth/signin/{provider}
にPOSTリクエストを送信して認証を行います。 -
signOut()
関数:/auth/signout
エンドポイントにアクセスし、また、他のブラウザタブにイベントをブロードキャストして、同時にログアウトする機能を実現します。 -
getSession()
関数:ユーザーセッションオブジェクトを取得するために使用され、フロントエンドアプリケーションでユーザーの認証情報を取得しやすくします。 -
getCsrfToken()
関数:クロスサイトリクエストフォージェリ(XSS)トークンを取得し、signIn
、signOut
、SessionProvider
のリクエストボディに追加する必要があり、セキュリティを強化するために使用されます。
nextディレクトリの解析
next
パッケージは、Next.jsアプリケーション向けに特別に設計されており、パッケージ全体のエントリポイントである NextAuth()
メソッドを含んでいます。ここでは、NextAuthApiHandler()
ブランチのコードに焦点を当てます:
-
リクエストの変換:Next.jsのリクエストを内部のリクエストデータ構造に変換します。主に
action
、cookie
、httpmethod
などの情報を解析し、toInternalRequest()
メソッドを使用して実装されています。 -
初期化操作:
init()
メソッドを呼び出します。これには、オプションの初期化、CSRFトークンの処理(作成または検証)、コールバックURLの処理(クエリパラメータまたはクッキーから読み取り)が含まれます。コールバックURLを処理するとき、callbacks.redirect()
メソッドを呼び出して、異なるシナリオ(最初のエントリまたはコールバックの戻り)に応じて対応する処理を行います。 -
SessionStoreの構築:
SessionStore
オブジェクトを構築します。これは、SessionToken
クッキーを管理するために使用されます。クッキーのサイズが大きすぎる場合があるため、複数のクッキーに分割して保存されます。例えば、xx.0
、xx.1
、xx.2
などです。 -
リクエスト処理ブランチ:
httpmethod
に応じて、get
とpost
の2つのブランチに分かれます:-
get
リクエスト:signIn
、signOut
、error
、veryrequest
などの静的ページを定義します。カスタムページがある場合は、そのカスタムページにリダイレクトされます。また、providers
、session
、csrf
、callback
などのインターフェイスがあり、フロントエンドのJavaScriptにセッションとトークン情報を提供するため、またはトークンとクッキーを更新するために使用されます。 -
post
リクエスト:まず、リクエストボディ内のトークンとクッキー内のトークンを比較することでCSRFトークンを検証し、クロスサイト攻撃を防ぎます。signIn
とsignOut
の操作では、クッキーを準備してOAuthサイトにジャンプします。callback
は、OAuth認証が成功した後のコールバックを処理するために使用されます。Session
インターフェイスは、フロントエンドのJavaScriptがセッションオブジェクトを取得または更新するために使用されます。
-
jwtディレクトリの解析
Next-Authにおけるクッキー暗号化に関連するコードは、jwt
ディレクトリに格納されています。暗号化キーは process.env.NEXTAUTH_SECRET
環境変数から取得され、以下のメソッドを使用して暗号化が行われます(ソルトは空文字列です):
import hkdf from "@panva/hkdf"
async function getDerivedEncryptionKey(
keyMaterial: string | Buffer,
salt: string
) {
return await hkdf(
"sha256",
keyMaterial,
salt,
`NextAuth.js Generated Encryption Key${salt? ` (${salt})` : ""}`,
32
)
}
暗号化されたキーは、トークンを暗号化して署名し、クッキーを生成するために使用されます。具体的なエンコードとデコードのメソッドは以下の通りです:
import { EncryptJWT, jwtDecrypt } from "jose";
export async function encode(params: JWTEncodeParams) {
/** @note An empty `salt` indicates a session token. See {@link JWTEncodeParams.salt}. */
const { token = {}, secret, maxAge = DEFAULT_MAX_AGE, salt = "" } = params
const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
return await new EncryptJWT(token)
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
.setIssuedAt()
.setExpirationTime(now() + maxAge)
.setJti(uuid())
.encrypt(encryptionSecret)
}
/** Decode the JWT issued by Next-Auth.js. */
export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
/** @note An empty `salt` indicates a session token. See {@link JWTDecodeParams.salt}. */
const { token, secret, salt = "" } = params
if (!token) return null
const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
const { payload } = await jwtDecrypt(token, encryptionSecret, {
clockTolerance: 15,
})
return payload
}
結論
Next-Authは、合理的なソースコード構造の分割を通じて、強力で柔軟な認証機能を提供しています。ネットワークリクエストのカプセル化、セッション管理、複数の認証方法のサポート、セキュリティ面の配慮(CSRF保護やJWT暗号化など)のいずれも、その設計の優れた点を反映しています。開発者は、自身のニーズに応じてNext-Authのソースコードを深く理解し、拡張することができ、異なるプロジェクトの認証要件を満たすことができます。
Leapcell: The Best of Serverless Web Hosting
最後に、サービスをデプロイするのに最適なプラットフォームをおすすめします:Leapcell
🚀 好きな言語で開発
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無料で無制限のプロジェクトをデプロイ
使用する分だけ支払います—リクエストがなければ、課金はありません。
⚡ 使った分だけ支払い、隠れた費用はありません
アイドル料金はなく、シームレスなスケーラビリティを実現しています。
🔹 Twitterでフォローしてください:@LeapcellHQ