Skip to content

Commit 72408ab

Browse files
authored
feat: update jose and openid-client (#3039)
Updates the `jose` and `openid-client` packages. BREAKING CHANGE: The `jwt` option has been simplified and the NextAuth.js issued JWT is now encrypted by default. If you want to override the defaults, you can still use the `encode` and `decode` functions. These are advanced options and they should only be used if you know what you are doing. The default secret generation has been removed in this PR, which will be added back in a separate one. Remember, that is only for developer convenience, it is **highly** recommended to always create your own secret for production.
1 parent eb33c9d commit 72408ab

File tree

9 files changed

+171
-718
lines changed

9 files changed

+171
-718
lines changed

package-lock.json

Lines changed: 60 additions & 540 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@
6262
"dependencies": {
6363
"@babel/runtime": "^7.15.4",
6464
"futoin-hkdf": "^1.4.2",
65-
"jose": "^1.27.2",
65+
"jose": "^4.1.2",
6666
"oauth": "^0.9.15",
67-
"openid-client": "^4.9.0",
67+
"openid-client": "^5.0.1",
6868
"preact": "^10.5.14",
69-
"preact-render-to-string": "^5.1.19"
69+
"preact-render-to-string": "^5.1.19",
70+
"uuid": "^8.3.2"
7071
},
7172
"peerDependencies": {
7273
"nodemailer": "^6.6.5",
@@ -91,6 +92,7 @@
9192
"@testing-library/react": "^12.1.2",
9293
"@testing-library/react-hooks": "^7.0.2",
9394
"@testing-library/user-event": "^13.2.1",
95+
"@types/node": "^16.11.6",
9496
"@types/nodemailer": "^6.4.4",
9597
"@types/oauth": "^0.9.1",
9698
"@types/react": "^17.0.27",
@@ -124,6 +126,9 @@
124126
"typescript": "^4.4.3",
125127
"whatwg-fetch": "^3.6.2"
126128
},
129+
"engines": {
130+
"node": "^12.19.0 || ^14.15.0 || ^16.13.0"
131+
},
127132
"prettier": {
128133
"semi": false
129134
},

src/core/lib/oauth/callback.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ export default async function oAuthCallback(params: {
6464
try {
6565
const client = await openidClient(options)
6666

67-
/** @type {import("openid-client").TokenSet} */
68-
let tokens
67+
let tokens: TokenSet
6968

7069
const pkce = await usePKCECodeVerifier({
7170
options,

src/core/lib/oauth/pkce-handler.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as jwt from "../../../jwt"
33
import { generators } from "openid-client"
44
import { InternalOptions } from "src/lib/types"
55

6-
const PKCE_LENGTH = 64
76
const PKCE_CODE_CHALLENGE_METHOD = "S256"
87
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
98

@@ -25,15 +24,14 @@ export async function createPKCE(options) {
2524
// Provider does not support PKCE, return nothing.
2625
return
2726
}
28-
const codeVerifier = generators.codeVerifier(PKCE_LENGTH)
27+
const codeVerifier = generators.codeVerifier()
2928
const codeChallenge = generators.codeChallenge(codeVerifier)
3029

3130
// Encrypt code_verifier and save it to an encrypted cookie
3231
const encryptedCodeVerifier = await jwt.encode({
3332
maxAge: PKCE_MAX_AGE,
3433
...options.jwt,
3534
token: { code_verifier: codeVerifier },
36-
encryption: true,
3735
})
3836

3937
const cookieExpires = new Date()
@@ -44,7 +42,6 @@ export async function createPKCE(options) {
4442
code_challenge: codeChallenge,
4543
code_verifier: codeVerifier,
4644
},
47-
pkceLength: PKCE_LENGTH,
4845
method: PKCE_CODE_CHALLENGE_METHOD,
4946
})
5047
return {
@@ -85,8 +82,6 @@ export async function usePKCECodeVerifier(params: {
8582
const pkce = await jwt.decode({
8683
...options.jwt,
8784
token: codeVerifier,
88-
maxAge: PKCE_MAX_AGE,
89-
encryption: true,
9085
})
9186

9287
// remove PKCE cookie after it has been used up

src/core/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Adapter } from "../adapters"
22
import { Provider, CredentialInput, ProviderType } from "../providers"
3-
import { TokenSetParameters } from "openid-client"
3+
import type { TokenSetParameters } from "openid-client"
44
import { JWT, JWTOptions } from "../jwt"
55
import { LoggerInstance } from "../lib/logger"
66

src/jwt/index.ts

Lines changed: 52 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,85 @@
11
import crypto from "crypto"
2-
import jose from "jose"
3-
import logger from "../lib/logger"
2+
import { EncryptJWT, jwtDecrypt } from "jose"
3+
import uuid from "uuid"
44
import { NextApiRequest } from "next"
5-
import type { JWT, JWTDecodeParams, JWTEncodeParams } from "./types"
5+
import type { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "./types"
66

77
export * from "./types"
88

9-
// Set default algorithm to use for auto-generated signing key
10-
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
11-
12-
// Set default algorithm for auto-generated symmetric encryption key
13-
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
14-
15-
// Use encryption or not by default
16-
const DEFAULT_ENCRYPTION_ENABLED = false
17-
189
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
1910

11+
const now = () => (Date.now() / 1000) | 0
12+
13+
/** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
2014
export async function encode({
2115
token = {},
22-
maxAge = DEFAULT_MAX_AGE,
2316
secret,
24-
signingKey,
25-
signingOptions = {
26-
expiresIn: `${maxAge}s`,
27-
},
28-
encryptionKey,
29-
encryptionOptions = {
30-
alg: "dir",
31-
enc: DEFAULT_ENCRYPTION_ALGORITHM,
32-
zip: "DEF",
33-
},
34-
encryption = DEFAULT_ENCRYPTION_ENABLED,
17+
maxAge = DEFAULT_MAX_AGE,
3518
}: JWTEncodeParams) {
36-
// Signing Key
37-
const _signingKey = signingKey
38-
? jose.JWK.asKey(JSON.parse(signingKey))
39-
: getDerivedSigningKey(secret)
40-
41-
// Sign token
42-
const signedToken = jose.JWT.sign(token, _signingKey, signingOptions)
43-
44-
if (encryption) {
45-
// Encryption Key
46-
const _encryptionKey = encryptionKey
47-
? jose.JWK.asKey(JSON.parse(encryptionKey))
48-
: getDerivedEncryptionKey(secret)
49-
50-
// Encrypt token
51-
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
52-
}
53-
return signedToken
19+
const encryptionSecret = await getDerivedEncryptionKey(secret)
20+
return await new EncryptJWT(token)
21+
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
22+
.setIssuedAt()
23+
.setExpirationTime(now() + maxAge)
24+
.setJti(crypto.randomUUID ? crypto.randomUUID() : uuid())
25+
.encrypt(encryptionSecret)
5426
}
5527

28+
/** Decodes a NextAuth.js issued JWT. */
5629
export async function decode({
57-
secret,
5830
token,
59-
maxAge = DEFAULT_MAX_AGE,
60-
signingKey,
61-
verificationKey = signingKey, // Optional (defaults to encryptionKey)
62-
verificationOptions = {
63-
maxTokenAge: `${maxAge}s`,
64-
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
65-
},
66-
encryptionKey,
67-
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
68-
decryptionOptions = {
69-
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
70-
},
71-
encryption = DEFAULT_ENCRYPTION_ENABLED,
31+
secret,
7232
}: JWTDecodeParams): Promise<JWT | null> {
7333
if (!token) return null
74-
75-
let tokenToVerify = token
76-
77-
if (encryption) {
78-
// Encryption Key
79-
const _encryptionKey = decryptionKey
80-
? jose.JWK.asKey(JSON.parse(decryptionKey))
81-
: getDerivedEncryptionKey(secret)
82-
83-
// Decrypt token
84-
const decryptedToken = jose.JWE.decrypt(
85-
token,
86-
_encryptionKey,
87-
decryptionOptions
88-
)
89-
tokenToVerify = decryptedToken.toString("utf8")
90-
}
91-
92-
// Signing Key
93-
const _signingKey = verificationKey
94-
? jose.JWK.asKey(JSON.parse(verificationKey))
95-
: getDerivedSigningKey(secret)
96-
97-
// Verify token
98-
return jose.JWT.verify(
99-
tokenToVerify,
100-
_signingKey,
101-
verificationOptions
102-
) as JWT | null
34+
const encryptionSecret = await getDerivedEncryptionKey(secret)
35+
const { payload } = await jwtDecrypt(token, encryptionSecret, {
36+
clockTolerance: 15,
37+
})
38+
return payload
10339
}
10440

10541
export type GetTokenParams<R extends boolean = false> = {
42+
/** The request containing the JWT either in the cookies or in the `Authorization` header. */
10643
req: NextApiRequest
44+
/**
45+
* Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http://
46+
* or not set (e.g. development or test instance) case use unprefixed name
47+
*/
10748
secureCookie?: boolean
49+
/** If the JWT is in the cookie, what name `getToken()` should look for. */
10850
cookieName?: string
51+
/**
52+
* `getToken()` will return the raw JWT if this is set to `true`
53+
* @default false
54+
*/
10955
raw?: R
110-
decode?: typeof decode
111-
secret?: string
112-
} & Omit<JWTDecodeParams, "secret">
56+
} & Pick<JWTOptions, "decode" | "secret">
11357

114-
/** [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken) */
58+
/**
59+
* Takes a NextAuth.js request (`req`) and returns either the NextAuth.js issued JWT's payload,
60+
* or the raw JWT string. We look for the JWT in the either the cookies, or the `Authorization` header.
61+
* [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken)
62+
*/
11563
export async function getToken<R extends boolean = false>(
11664
params?: GetTokenParams<R>
11765
): Promise<R extends true ? string : JWT | null> {
11866
const {
11967
req,
120-
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
121-
// or not set (e.g. development or test instance) case use unprefixed name
12268
secureCookie = !(
12369
!process.env.NEXTAUTH_URL ||
12470
process.env.NEXTAUTH_URL.startsWith("http://")
12571
),
12672
cookieName = secureCookie
12773
? "__Secure-next-auth.session-token"
12874
: "next-auth.session-token",
129-
raw = false,
75+
raw,
13076
decode: _decode = decode,
13177
} = params ?? {}
78+
13279
if (!req) throw new Error("Must pass `req` to JWT getToken()")
13380

134-
// Try to get token from cookie
13581
let token = req.cookies[cookieName]
13682

137-
// If cookie not found in cookie look for bearer token in authorization header.
138-
// This allows clients that pass through tokens in headers rather than as
139-
// cookies to use this helper function.
14083
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
14184
const urlEncodedToken = req.headers.authorization.split(" ")[1]
14285
token = decodeURIComponent(urlEncodedToken)
@@ -156,22 +99,22 @@ export async function getToken<R extends boolean = false>(
15699
}
157100
}
158101

159-
// Generate warning (but only once at startup) when auto-generated keys are used
160-
let DERIVED_SIGNING_KEY_WARNING = false
161-
let DERIVED_ENCRYPTION_KEY_WARNING = false
162-
163-
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
164-
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
165-
if (crypto.hkdfSync) {
166-
return Buffer.from(
167-
crypto.hkdfSync(
102+
/** Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one */
103+
async function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
104+
if (crypto.hkdf) {
105+
return await new Promise((resolve, reject) => {
106+
crypto.hkdf(
168107
digest,
169108
secret,
170109
Buffer.alloc(0),
171110
encryptionInfo,
172-
byteLength
111+
byteLength,
112+
(err, derivedKey) => {
113+
if (err) reject(err)
114+
else resolve(Buffer.from(derivedKey))
115+
}
173116
)
174-
)
117+
})
175118
}
176119
// eslint-disable-next-line @typescript-eslint/no-var-requires
177120
return require("futoin-hkdf")(secret, byteLength, {
@@ -180,38 +123,9 @@ function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
180123
})
181124
}
182125

183-
function getDerivedSigningKey(secret) {
184-
if (!DERIVED_SIGNING_KEY_WARNING) {
185-
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
186-
DERIVED_SIGNING_KEY_WARNING = true
187-
}
188-
189-
const buffer = hkdf(secret, {
190-
byteLength: 64,
191-
encryptionInfo: "NextAuth.js Generated Signing Key",
192-
})
193-
const key = jose.JWK.asKey(buffer, {
194-
alg: DEFAULT_SIGNATURE_ALGORITHM,
195-
use: "sig",
196-
kid: "nextauth-auto-generated-signing-key",
197-
})
198-
return key
199-
}
200-
201-
function getDerivedEncryptionKey(secret) {
202-
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
203-
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
204-
DERIVED_ENCRYPTION_KEY_WARNING = true
205-
}
206-
207-
const buffer = hkdf(secret, {
126+
async function getDerivedEncryptionKey(secret) {
127+
return await hkdf(secret, {
208128
byteLength: 32,
209129
encryptionInfo: "NextAuth.js Generated Encryption Key",
210130
})
211-
const key = jose.JWK.asKey(buffer, {
212-
alg: DEFAULT_ENCRYPTION_ALGORITHM,
213-
use: "enc",
214-
kid: "nextauth-auto-generated-encryption-key",
215-
})
216-
return key
217131
}

0 commit comments

Comments
 (0)