Skip to content

Commit d237059

Browse files
committed
fix: differentiate between issued JWTs
1 parent 0f0c444 commit d237059

File tree

3 files changed

+52
-17
lines changed

3 files changed

+52
-17
lines changed

packages/next-auth/src/core/lib/oauth/checks.ts

+25-7
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ export async function signCookie(
2121

2222
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
2323

24+
const { name } = cookies[type]
2425
const expires = new Date()
2526
expires.setTime(expires.getTime() + maxAge * 1000)
2627
return {
27-
name: cookies[type].name,
28-
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
28+
name,
29+
value: await jwt.encode({
30+
...options.jwt,
31+
maxAge,
32+
token: { value },
33+
salt: name,
34+
}),
2935
options: { ...cookies[type].options, expires },
3036
}
3137
}
@@ -71,16 +77,18 @@ export const pkce = {
7177
if (!codeVerifier)
7278
throw new TypeError("PKCE code_verifier cookie was missing.")
7379

80+
const { name } = options.cookies.pkceCodeVerifier
7481
const value = (await jwt.decode({
7582
...options.jwt,
7683
token: codeVerifier,
84+
salt: name,
7785
})) as any
7886

7987
if (!value?.value)
8088
throw new TypeError("PKCE code_verifier value could not be parsed.")
8189

8290
resCookies.push({
83-
name: options.cookies.pkceCodeVerifier.name,
91+
name,
8492
value: "",
8593
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
8694
})
@@ -121,12 +129,17 @@ export const state = {
121129

122130
if (!state) throw new TypeError("State cookie was missing.")
123131

124-
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
132+
const { name } = options.cookies.state
133+
const value = (await jwt.decode({
134+
...options.jwt,
135+
token: state,
136+
salt: name,
137+
})) as any
125138

126139
if (!value?.value) throw new TypeError("State value could not be parsed.")
127140

128141
resCookies.push({
129-
name: options.cookies.state.name,
142+
name,
130143
value: "",
131144
options: { ...options.cookies.state.options, maxAge: 0 },
132145
})
@@ -166,12 +179,17 @@ export const nonce = {
166179
const nonce = cookies?.[options.cookies.nonce.name]
167180
if (!nonce) throw new TypeError("Nonce cookie was missing.")
168181

169-
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
182+
const { name } = options.cookies.nonce
183+
const value = (await jwt.decode({
184+
...options.jwt,
185+
token: nonce,
186+
salt: name,
187+
})) as any
170188

171189
if (!value?.value) throw new TypeError("Nonce value could not be parsed.")
172190

173191
resCookies.push({
174-
name: options.cookies.nonce.name,
192+
name,
175193
value: "",
176194
options: { ...options.cookies.nonce.options, maxAge: 0 },
177195
})

packages/next-auth/src/jwt/index.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ const now = () => (Date.now() / 1000) | 0
1515

1616
/** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
1717
export async function encode(params: JWTEncodeParams) {
18-
const { token = {}, secret, maxAge = DEFAULT_MAX_AGE } = params
19-
const encryptionSecret = await getDerivedEncryptionKey(secret)
18+
/** @note empty `salt` means a session token. See {@link JWTEncodeParams.salt}. */
19+
const { token = {}, secret, maxAge = DEFAULT_MAX_AGE, salt = "" } = params
20+
const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
2021
return await new EncryptJWT(token)
2122
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
2223
.setIssuedAt()
@@ -27,9 +28,10 @@ export async function encode(params: JWTEncodeParams) {
2728

2829
/** Decodes a NextAuth.js issued JWT. */
2930
export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
30-
const { token, secret } = params
31+
/** @note empty `salt` means a session token. See {@link JWTDecodeParams.salt}. */
32+
const { token, secret, salt = "" } = params
3133
if (!token) return null
32-
const encryptionSecret = await getDerivedEncryptionKey(secret)
34+
const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
3335
const { payload } = await jwtDecrypt(token, encryptionSecret, {
3436
clockTolerance: 15,
3537
})
@@ -116,12 +118,15 @@ export async function getToken<R extends boolean = false>(
116118
}
117119
}
118120

119-
async function getDerivedEncryptionKey(secret: string | Buffer) {
121+
async function getDerivedEncryptionKey(
122+
keyMaterial: string | Buffer,
123+
salt: string
124+
) {
120125
return await hkdf(
121126
"sha256",
122-
secret,
123-
"",
124-
"NextAuth.js Generated Encryption Key",
127+
keyMaterial,
128+
salt,
129+
`NextAuth.js Generated Encryption Key${salt ? ` (${salt})` : ""}`,
125130
32
126131
)
127132
}

packages/next-auth/src/jwt/types.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export interface JWT extends Record<string, unknown>, DefaultJWT {}
1717
export interface JWTEncodeParams {
1818
/** The JWT payload. */
1919
token?: JWT
20-
/** The secret used to encode the NextAuth.js issued JWT. */
20+
/**
21+
* Used in combination with `secret` when deriving the encryption secret for the various NextAuth.js-issued JWTs.
22+
* @note When no `salt` is passed, we assume this is a session token.
23+
* This is for backwards-compatibility with currently active sessions, so they won't be invalidated when upgrading the package.
24+
*/
25+
salt?: string
26+
/** The key material used to encode the NextAuth.js issued JWTs. Defaults to `NEXTAUTH_SECRET`. */
2127
secret: string | Buffer
2228
/**
2329
* The maximum age of the NextAuth.js issued JWT in seconds.
@@ -29,7 +35,13 @@ export interface JWTEncodeParams {
2935
export interface JWTDecodeParams {
3036
/** The NextAuth.js issued JWT to be decoded */
3137
token?: string
32-
/** The secret used to decode the NextAuth.js issued JWT. */
38+
/**
39+
* Used in combination with `secret` when deriving the encryption secret for the various NextAuth.js-issued JWTs.
40+
* @note When no `salt` is passed, we assume this is a session token.
41+
* This is for backwards-compatibility with currently active sessions, so they won't be invalidated when upgrading the package.
42+
*/
43+
salt?: string
44+
/** The key material used to decode the NextAuth.js issued JWTs. Defaults to `NEXTAUTH_SECRET`. */
3345
secret: string | Buffer
3446
}
3547

0 commit comments

Comments
 (0)