nextauthjs / nextauthjs/next-auth
getServerSession fails to fallback to Authorization header if no token is found in cookies
Nobody has claimed this yet.
- 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,Postmanorpuppeteer
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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