diff --git a/packages/next-auth/src/core/lib/oauth/checks.ts b/packages/next-auth/src/core/lib/oauth/checks.ts index f4970126a6..19196b5b59 100644 --- a/packages/next-auth/src/core/lib/oauth/checks.ts +++ b/packages/next-auth/src/core/lib/oauth/checks.ts @@ -21,11 +21,17 @@ export async function signCookie( logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge }) + const { name } = cookies[type] const expires = new Date() expires.setTime(expires.getTime() + maxAge * 1000) return { - name: cookies[type].name, - value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }), + name, + value: await jwt.encode({ + ...options.jwt, + maxAge, + token: { value }, + salt: name, + }), options: { ...cookies[type].options, expires }, } } @@ -71,16 +77,18 @@ export const pkce = { if (!codeVerifier) throw new TypeError("PKCE code_verifier cookie was missing.") + const { name } = options.cookies.pkceCodeVerifier const value = (await jwt.decode({ ...options.jwt, token: codeVerifier, + salt: name, })) as any if (!value?.value) throw new TypeError("PKCE code_verifier value could not be parsed.") resCookies.push({ - name: options.cookies.pkceCodeVerifier.name, + name, value: "", options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 }, }) @@ -121,12 +129,17 @@ export const state = { if (!state) throw new TypeError("State cookie was missing.") - const value = (await jwt.decode({ ...options.jwt, token: state })) as any + const { name } = options.cookies.state + const value = (await jwt.decode({ + ...options.jwt, + token: state, + salt: name, + })) as any if (!value?.value) throw new TypeError("State value could not be parsed.") resCookies.push({ - name: options.cookies.state.name, + name, value: "", options: { ...options.cookies.state.options, maxAge: 0 }, }) @@ -166,12 +179,17 @@ export const nonce = { const nonce = cookies?.[options.cookies.nonce.name] if (!nonce) throw new TypeError("Nonce cookie was missing.") - const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any + const { name } = options.cookies.nonce + const value = (await jwt.decode({ + ...options.jwt, + token: nonce, + salt: name, + })) as any if (!value?.value) throw new TypeError("Nonce value could not be parsed.") resCookies.push({ - name: options.cookies.nonce.name, + name, value: "", options: { ...options.cookies.nonce.options, maxAge: 0 }, }) diff --git a/packages/next-auth/src/jwt/index.ts b/packages/next-auth/src/jwt/index.ts index aec2e0492a..2fcc51ab62 100644 --- a/packages/next-auth/src/jwt/index.ts +++ b/packages/next-auth/src/jwt/index.ts @@ -15,8 +15,9 @@ const now = () => (Date.now() / 1000) | 0 /** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */ export async function encode(params: JWTEncodeParams) { - const { token = {}, secret, maxAge = DEFAULT_MAX_AGE } = params - const encryptionSecret = await getDerivedEncryptionKey(secret) + /** @note empty `salt` means 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() @@ -27,9 +28,10 @@ export async function encode(params: JWTEncodeParams) { /** Decodes a NextAuth.js issued JWT. */ export async function decode(params: JWTDecodeParams): Promise { - const { token, secret } = params + /** @note empty `salt` means a session token. See {@link JWTDecodeParams.salt}. */ + const { token, secret, salt = "" } = params if (!token) return null - const encryptionSecret = await getDerivedEncryptionKey(secret) + const encryptionSecret = await getDerivedEncryptionKey(secret, salt) const { payload } = await jwtDecrypt(token, encryptionSecret, { clockTolerance: 15, }) @@ -116,12 +118,15 @@ export async function getToken( } } -async function getDerivedEncryptionKey(secret: string | Buffer) { +async function getDerivedEncryptionKey( + keyMaterial: string | Buffer, + salt: string +) { return await hkdf( "sha256", - secret, - "", - "NextAuth.js Generated Encryption Key", + keyMaterial, + salt, + `NextAuth.js Generated Encryption Key${salt ? ` (${salt})` : ""}`, 32 ) } diff --git a/packages/next-auth/src/jwt/types.ts b/packages/next-auth/src/jwt/types.ts index 6d4fa45bbd..6a5a32ddc7 100644 --- a/packages/next-auth/src/jwt/types.ts +++ b/packages/next-auth/src/jwt/types.ts @@ -17,7 +17,13 @@ export interface JWT extends Record, DefaultJWT {} export interface JWTEncodeParams { /** The JWT payload. */ token?: JWT - /** The secret used to encode the NextAuth.js issued JWT. */ + /** + * Used in combination with `secret` when deriving the encryption secret for the various NextAuth.js-issued JWTs. + * @note When no `salt` is passed, we assume this is a session token. + * This is for backwards-compatibility with currently active sessions, so they won't be invalidated when upgrading the package. + */ + salt?: string + /** The key material used to encode the NextAuth.js issued JWTs. Defaults to `NEXTAUTH_SECRET`. */ secret: string | Buffer /** * The maximum age of the NextAuth.js issued JWT in seconds. @@ -29,7 +35,13 @@ export interface JWTEncodeParams { export interface JWTDecodeParams { /** The NextAuth.js issued JWT to be decoded */ token?: string - /** The secret used to decode the NextAuth.js issued JWT. */ + /** + * Used in combination with `secret` when deriving the encryption secret for the various NextAuth.js-issued JWTs. + * @note When no `salt` is passed, we assume this is a session token. + * This is for backwards-compatibility with currently active sessions, so they won't be invalidated when upgrading the package. + */ + salt?: string + /** The key material used to decode the NextAuth.js issued JWTs. Defaults to `NEXTAUTH_SECRET`. */ secret: string | Buffer }