nextauthjs / nextauthjs/next-auth

getServerSession fails to fallback to Authorization header if no token is found in cookies

Open
#9,010 4 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug triage
Dominant language
TypeScript
Stars
28.4k
Forks
4k
PR merge metrics
No merged PRs in 30d

Description

Environment

System:
OS: macOS 14.0
CPU: (12) x64 Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
Memory: 1.55 GB / 16.00 GB
Shell: 5.0.16 - /usr/local/bin/bash
Binaries:
Node: 20.5.0 - /usr/local/bin/node
npm: 9.8.0 - /usr/local/bin/npm
Browsers:
Chrome: 118.0.5993.117
Safari: 17.0

npmPackages:
next: ^13.5.5 => 13.5.6
next-auth: ^4.22.1 => 4.23.2
react: 18.2.0 => 18.2.0

Reproduction URL

https://github.com/nextauthjs/next-auth-example/commit/8f5899691fe3fdddf5194c85e2c72e2f7a4e144e

Describe the issue

When attempting to drop-in the getServerSession method to replace getToken for backend calls from Next13 pages/layouts, the method fails to read from the "Authorization: Bearer *" value as the getToken method used to. I've also verified that this appears to happen on the experimental branch's universal auth method

Note that this becomes a problem primarily when attempting to use a page from the Next13 App Router which never exposes the underlying Request object. The original Pages Router was able to get around this by using getServerSideProps to inject the current Request into getToken

When reviewing the implementation, it appears to be a manual edit to the getToken method under ~/packages/next-auth/src/jwt/index.ts:92 to fallback to the Authorization header

Looks like this can be more universal if implemented directly in the SessionStore class and have the same expected result in all areas of the app

export class SessionStore {
  #chunks: Chunks = {}
  #option: CookieOption
  #logger: LoggerInstance | Console
  #token: string

  constructor(
    option: CookieOption,
    req: Partial<{
      cookies: NextRequest["cookies"] | NextApiRequest["cookies"]
      headers: NextRequest["headers"] | NextApiRequest["headers"]
    }>,
    logger: LoggerInstance | Console
  ) {
    this.#logger = logger
    this.#option = option

    const { cookies, headers } = req
    const { name: cookieName } = option

   // extract the token from the standard auth header
    const authorizationHeader =
      headers instanceof Headers
       ? headers.get("authorization")
       : headers?.authorization;

    if (authorizationHeader?.split(" ")[0] === "Bearer") {
      const urlEncodedToken = authorizationHeader.split(" ")[1];

      this.#token = decodeURIComponent(urlEncodedToken);
    }

    if (typeof cookies?.getAll === "function") {
      // Next.js ^v13.0.1 (Edge Env)
      for (const { name, value } of cookies.getAll()) {
        if (name.startsWith(cookieName)) {
          this.#chunks[name] = value
        }
      }
    } else if (cookies instanceof Map) {
      for (const name of cookies.keys()) {
        if (name.startsWith(cookieName)) this.#chunks[name] = cookies.get(name)
      }
    } else {
      for (const name in cookies) {
        if (name.startsWith(cookieName)) this.#chunks[name] = cookies[name]
      }
    }
  }

  /**
   * The JWT Session or database Session ID
   * constructed from the cookie chunks or from authorization header (fallback)
   */
  get value() {
    // Sort the chunks by their keys before joining
    const sortedKeys = Object.keys(this.#chunks).sort((a, b) => {
      const aSuffix = parseInt(a.split(".").pop() ?? "0")
      const bSuffix = parseInt(b.split(".").pop() ?? "0")

      return aSuffix - bSuffix
    })

    // Use the sorted keys to join the chunks in the correct order
    return sortedKeys.map((key) => this.#chunks[key]).join("") || this.#token;
  }

  /** Given a cookie, return a list of cookies, chunked to fit the allowed cookie size. */
  #chunk(cookie: Cookie): Cookie[] {
    const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE)

    if (chunkCount === 1) {
      this.#chunks[cookie.name] = cookie.value
      return [cookie]
    }

    const cookies: Cookie[] = []
    for (let i = 0; i < chunkCount; i++) {
      const name = `${cookie.name}.${i}`
      const value = cookie.value.substr(i * CHUNK_SIZE, CHUNK_SIZE)
      cookies.push({ ...cookie, name, value })
      this.#chunks[name] = value
    }

    this.#logger.debug("CHUNKING_SESSION_COOKIE", {
      message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
      emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
      valueSize: cookie.value.length,
      chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
    })

    return cookies
  }

  /** Returns cleaned cookie chunks. */
  #clean(): Record<string, Cookie> {
    const cleanedChunks: Record<string, Cookie> = {}
    for (const name in this.#chunks) {
      delete this.#chunks?.[name]
      cleanedChunks[name] = {
        name,
        value: "",
        options: { ...this.#option.options, maxAge: 0 },
      }
    }
    return cleanedChunks
  }

  /**
   * Given a cookie value, return new cookies, chunked, to fit the allowed cookie size.
   * If the cookie has changed from chunked to unchunked or vice versa,
   * it deletes the old cookies as well.
   */
  chunk(value: string, options: Partial<Cookie["options"]>): Cookie[] {
    // Assume all cookies should be cleaned by default
    const cookies: Record<string, Cookie> = this.#clean()

    // Calculate new chunks
    const chunked = this.#chunk({
      name: this.#option.name,
      value,
      options: { ...this.#option.options, ...options },
    })

    // Update stored chunks / cookies
    for (const chunk of chunked) {
      cookies[chunk.name] = chunk
    }

    return Object.values(cookies)
  }

  /** Returns a list of cookies that should be cleaned. */
  clean(): Cookie[] {
    return Object.values(this.#clean())
  }
}
How to reproduce
  • Authenticate
  • Copy JWT token created
  • Attempt to call page using token in "Authorization: Bearer {Token}" using cURL, Postman or puppeteer
Expected behavior

All auth methods universally support reading the token from the Authorization Bearer header.

Enables remote testing of API endpoints and using tools such as puppeteer for exporting pages to PDF on the server

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/next-auth/src/jwt/index.ts and the SessionStore implementation described in the issue, then inspect the linked reproduction commit. Verify completion by reproducing a request with only an Authorization: Bearer token and confirming that the relevant auth methods consistently read it when no cookie token is present.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, typescript
Domain
api, authentication
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.