0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NextAuth.jsのソースコード徹底解説:強力かつ柔軟な認証ソリューション

Posted at

Group200.png

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アプリケーションに対して useSessiongetToken などのフロントエンドメソッドを提供し、ユーザーセッション情報の取得と更新に使用されます。
  • utils:ルートの解析やデータのマージなど、いくつかの補助メソッドを定義し、一般的なタスクの処理を支援します。

また、index.tsmiddleware.ts ファイルは、ライブラリ全体の初期化とミドルウェア処理において重要な役割を果たしています。

clientディレクトリの解析

client ディレクトリは主に2つの重要な機能を提供しています:

  1. Fetch のカプセル化:ネットワークリクエストの fetch メソッドをカプセル化しています。ネットワークリクエストに関するすべての操作は、このカプセル化されたメソッドを呼び出します。これにより、ネットワークリクエスト関連のロジックを一括で管理し、処理することができます。
  2. ブロードキャストイベント機構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)トークンを取得し、signInsignOutSessionProvider のリクエストボディに追加する必要があり、セキュリティを強化するために使用されます。

nextディレクトリの解析

next パッケージは、Next.jsアプリケーション向けに特別に設計されており、パッケージ全体のエントリポイントである NextAuth() メソッドを含んでいます。ここでは、NextAuthApiHandler() ブランチのコードに焦点を当てます:

  1. リクエストの変換:Next.jsのリクエストを内部のリクエストデータ構造に変換します。主に actioncookiehttpmethod などの情報を解析し、toInternalRequest() メソッドを使用して実装されています。
  2. 初期化操作init() メソッドを呼び出します。これには、オプションの初期化、CSRFトークンの処理(作成または検証)、コールバックURLの処理(クエリパラメータまたはクッキーから読み取り)が含まれます。コールバックURLを処理するとき、callbacks.redirect() メソッドを呼び出して、異なるシナリオ(最初のエントリまたはコールバックの戻り)に応じて対応する処理を行います。
  3. SessionStoreの構築SessionStore オブジェクトを構築します。これは、SessionToken クッキーを管理するために使用されます。クッキーのサイズが大きすぎる場合があるため、複数のクッキーに分割して保存されます。例えば、xx.0xx.1xx.2 などです。
  4. リクエスト処理ブランチhttpmethod に応じて、getpost の2つのブランチに分かれます:
    • get リクエストsignInsignOuterrorveryrequest などの静的ページを定義します。カスタムページがある場合は、そのカスタムページにリダイレクトされます。また、providerssessioncsrfcallback などのインターフェイスがあり、フロントエンドのJavaScriptにセッションとトークン情報を提供するため、またはトークンとクッキーを更新するために使用されます。
    • post リクエスト:まず、リクエストボディ内のトークンとクッキー内のトークンを比較することでCSRFトークンを検証し、クロスサイト攻撃を防ぎます。signInsignOut の操作では、クッキーを準備して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

brandpic7.png

🚀 好きな言語で開発

JavaScript、Python、Go、またはRustで簡単に開発できます。

🌍 無料で無制限のプロジェクトをデプロイ

使用する分だけ支払います—リクエストがなければ、課金はありません。

⚡ 使った分だけ支払い、隠れた費用はありません

アイドル料金はなく、シームレスなスケーラビリティを実現しています。

Frame3-withpadding2x.png

📖 ドキュメントを探索

🔹 Twitterでフォローしてください:@LeapcellHQ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?