Image description

Leapcell: The Best of Serverless Web Hosting

Source Code Analysis of Next-Auth: A Powerful and Flexible Authentication Solution

Introduction

Next-Auth is a powerful and flexible authentication library that provides convenient authentication functions for Next.js applications and other React projects. It supports multiple authentication methods and has a reasonable division of the source code structure, making it easy for developers to understand and expand. This article will deeply analyze the source code structure and key functions of Next-Auth.

Introduction to the Directory

The core source code of Next-Auth is located in the packages/next-auth/src directory. The following is the main structure of this directory:

  • client: It mainly encapsulates the fetch method and implements a broadcast event mechanism to listen for changes in localStorage.
  • core: It contains the main business logic, and the APIs and pages of /api/auth/xxx are defined here.
  • jwt: It provides encryption and decryption methods for JWT (JSON Web Token) and is used to handle token-related operations in authentication.
  • next: It defines the middleware in Next.js and provides specific support for Next.js applications.
  • providers: It provides the default configurations of various authentication methods, making it convenient for developers to integrate different authentication providers.
  • react: It provides front-end methods such as useSession and getToken for React applications, which are used to obtain and update user session information.
  • utils: It defines some auxiliary methods, such as parsing routes and merging data, to help handle some common tasks.

In addition, there are index.ts and middleware.ts files, which play important roles in the initialization of the entire library and middleware processing.

Analysis of the client Directory

The client directory mainly provides two key functions:

  1. Fetch Encapsulation: It encapsulates the fetch method of network requests. All operations involving network requests will call this encapsulated method, which is convenient for unified management and processing of network request-related logic.
  2. Broadcast Event Mechanism: It realizes communication between different tabs or windows by listening to changes in localStorage. The code is as follows:
export function BroadcastChannel(name = "nextauth.message") {
  return {
    /** Get notifications from other tabs/windows */
    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)
    },
    /** Notify other tabs/windows */
    post(message: Record<string, unknown>) {
      if (typeof window === "undefined") return
      try {
        localStorage.setItem(
          name,
          JSON.stringify({...message, timestamp: now() })
        )
      } catch {
        /**
         * The localStorage API is not always available. For example, it does not work in private mode before Safari 11.
         * If an error occurs, the notification will be simply discarded.
         */
      }
    },
  }
}

export interface BroadcastMessage {
  event?: "session"
  data?: { trigger?: "signout" | "getSession" }
  clientId: string
  timestamp: number
}

Currently, the main listener of this broadcast event is the SessionProvider in React. When a change in localStorage is detected, it will trigger the __NEXTAUTH._getSession() method defined in the SessionProvider. This method is used to request the /api/auth/session API to obtain the user session object. The __NEXTAUTH._getSession() method has the following calling methods:

  • __NEXTAUTH._getSession(): It is used when called for the first time to initialize the retrieval of session information.
  • __NEXTAUTH._getSession({ event: "storage" }): It is called when other tabs or windows send messages to update the session to avoid circular updates.
  • __NEXTAUTH._getSession({ event: "visibilitychange" }): It is triggered when the tab is activated to check whether the session needs to be updated.
  • __NEXTAUTH._getSession({ event: "poll" }): It is used to poll and refresh the session to ensure the real-time nature of the session information.

The following is the specific implementation of the __NEXTAUTH._getSession() method:

React.useEffect(() => {
    __NEXTAUTH._getSession = async ({ event } = {}) => {
      try {
        const storageEvent = event === "storage"
        // If there is no client session yet, or there is an event from other tabs/windows, we should always update
        if (storageEvent || __NEXTAUTH._session === undefined) {
          __NEXTAUTH._lastSync = now()
          __NEXTAUTH._session = await getSession({
            broadcast:!storageEvent,
          })
          setSession(__NEXTAUTH._session)
          return
        }

        if (
          // If the session expiration time is not defined, it is okay to use the existing value before the event triggers an update
         !event ||
          // If there is no session on the client, we don't need to call the server to check (if logged in through other tabs/windows, it will come in the form of a "storage" event)
          __NEXTAUTH._session === null ||
          // If the client session has not expired yet, exit early
          now() < __NEXTAUTH._lastSync
        ) {
          return
        }

        // An event has occurred or the session has expired, update the client session
        __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 = () => {}
    }
  }, [])

Analysis of the react Directory

The react directory provides a series of practical methods and components for front-end React projects:

  • SessionProvider Component: It is usually wrapped around the outermost layer of the entire application to provide a session object for the application, ensuring that session information can be accessed throughout the application.
  • useSession() hook Function: It is used to consume the session object provided by the SessionProvider. The type definition of the session object is as follows:
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() Function: It triggers the login process. If it is an OAuth login, it will send a POST request to /auth/signin/{provider} for authentication.
  • signOut() Function: It accesses the /auth/signout endpoint and will also broadcast an event to notify other browser tabs to achieve the function of logging out simultaneously.
  • getSession() Function: It is used to obtain the user session object, making it convenient to obtain the user's authentication information in the front-end application.
  • getCsrfToken() Function: It obtains the anti-cross-site request forgery (XSS) token, which needs to be added to the request body in signIn, signOut, and SessionProvider to enhance security.

Analysis of the next Directory

The next package is specifically designed for Next.js applications, which contains the entry NextAuth() method of the entire package. We focus on the code of the NextAuthApiHandler() branch:

  1. Request Conversion: It converts the requests of Next.js into the internal request data structure, mainly parses out information such as action, cookie, and httpmethod, and is implemented using the toInternalRequest() method.
  2. Initialization Operations: It calls the init() method, which includes initializing options, handling CSRF tokens (creating or validating), and handling callback URLs (reading from query parameters or cookies). When handling callback URLs, it will call the callbacks.redirect() method to perform corresponding processing according to different scenarios (first entry or callback return).
  3. SessionStore Construction: It constructs the SessionStore object, which is used to manage the SessionToken cookie. Since the size of the cookie may be too large, it will be divided into multiple cookies for storage, such as xx.0, xx.1, xx.2, etc.
  4. Request Processing Branches: According to the httpmethod, it is divided into two branches, get and post:
    • get Request: It defines static pages such as signIn, signOut, error, and veryrequest. If there are custom pages, it will be redirected to the custom pages. In addition, there are interfaces such as providers, session, csrf, and callback, which are used to provide session and token information to the front-end JavaScript, or update tokens and cookies.
    • post Request: First, it validates the CSRF token by comparing the token in the request body with the token in the cookie to prevent cross-site attacks. For signIn and signOut operations, it will prepare cookies and jump to the OAuth site; callback is used to handle the callback after OAuth authentication is successful; the Session interface is used for the front-end JavaScript to obtain or update the session object.

Analysis of the jwt Directory

The code related to cookie encryption in Next-Auth is located in the jwt directory. The encryption key comes from the process.env.NEXTAUTH_SECRET environment variable, and the following method is used for encryption (the salt is an empty string):

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
  )
}

The encrypted key is used to encrypt and sign the token to generate a cookie. The specific encoding and decoding methods are as follows:

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
}

Conclusion

Next-Auth provides powerful and flexible authentication functions through a reasonable division of the source code structure. Whether it is the encapsulation of network requests, session management, support for multiple authentication methods, or considerations for security (such as CSRF protection and JWT encryption), it reflects the excellence of its design. Developers can deeply understand and expand the source code of Next-Auth according to their own needs to meet the authentication requirements of different projects.

Leapcell: The Best of Serverless Web Hosting

Finally, I would like to recommend a platform that is most suitable for deploying services: Leapcell

Image description

🚀 Build with Your Favorite Language

Develop effortlessly in JavaScript, Python, Go, or Rust.

🌍 Deploy Unlimited Projects for Free

Only pay for what you use—no requests, no charges.

⚡ Pay-as-You-Go, No Hidden Costs

No idle fees, just seamless scalability.

Image description

📖 Explore Our Documentation

🔹 Follow us on Twitter: @LeapcellHQ