Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
600 changes: 60 additions & 540 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@
"dependencies": {
"@babel/runtime": "^7.15.4",
"futoin-hkdf": "^1.4.2",
"jose": "^1.27.2",
"jose": "^4.1.2",
"oauth": "^0.9.15",
"openid-client": "^4.9.0",
"openid-client": "^5.0.1",
"preact": "^10.5.14",
"preact-render-to-string": "^5.1.19"
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
},
"peerDependencies": {
"nodemailer": "^6.6.5",
Expand All @@ -91,6 +92,7 @@
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.2.1",
"@types/node": "^16.11.6",
"@types/nodemailer": "^6.4.4",
"@types/oauth": "^0.9.1",
"@types/react": "^17.0.27",
Expand Down Expand Up @@ -124,6 +126,9 @@
"typescript": "^4.4.3",
"whatwg-fetch": "^3.6.2"
},
"engines": {
"node": "^12.19.0 || ^14.15.0 || ^16.13.0"
},
"prettier": {
"semi": false
},
Expand Down
3 changes: 1 addition & 2 deletions src/core/lib/oauth/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ export default async function oAuthCallback(params: {
try {
const client = await openidClient(options)

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

const pkce = await usePKCECodeVerifier({
options,
Expand Down
7 changes: 1 addition & 6 deletions src/core/lib/oauth/pkce-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as jwt from "../../../jwt"
import { generators } from "openid-client"
import { InternalOptions } from "src/lib/types"

const PKCE_LENGTH = 64
const PKCE_CODE_CHALLENGE_METHOD = "S256"
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds

Expand All @@ -25,15 +24,14 @@ export async function createPKCE(options) {
// Provider does not support PKCE, return nothing.
return
}
const codeVerifier = generators.codeVerifier(PKCE_LENGTH)
const codeVerifier = generators.codeVerifier()
const codeChallenge = generators.codeChallenge(codeVerifier)

// Encrypt code_verifier and save it to an encrypted cookie
const encryptedCodeVerifier = await jwt.encode({
maxAge: PKCE_MAX_AGE,
...options.jwt,
token: { code_verifier: codeVerifier },
encryption: true,
})

const cookieExpires = new Date()
Expand All @@ -44,7 +42,6 @@ export async function createPKCE(options) {
code_challenge: codeChallenge,
code_verifier: codeVerifier,
},
pkceLength: PKCE_LENGTH,
method: PKCE_CODE_CHALLENGE_METHOD,
})
return {
Expand Down Expand Up @@ -85,8 +82,6 @@ export async function usePKCECodeVerifier(params: {
const pkce = await jwt.decode({
...options.jwt,
token: codeVerifier,
maxAge: PKCE_MAX_AGE,
encryption: true,
})

// remove PKCE cookie after it has been used up
Expand Down
2 changes: 1 addition & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Adapter } from "../adapters"
import { Provider, CredentialInput, ProviderType } from "../providers"
import { TokenSetParameters } from "openid-client"
import type { TokenSetParameters } from "openid-client"
import { JWT, JWTOptions } from "../jwt"
import { LoggerInstance } from "../lib/logger"

Expand Down
190 changes: 52 additions & 138 deletions src/jwt/index.ts
Original file line number Diff line number Diff line change
@@ -1,142 +1,85 @@
import crypto from "crypto"
import jose from "jose"
import logger from "../lib/logger"
import { EncryptJWT, jwtDecrypt } from "jose"
import uuid from "uuid"
import { NextApiRequest } from "next"
import type { JWT, JWTDecodeParams, JWTEncodeParams } from "./types"
import type { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "./types"

export * from "./types"

// Set default algorithm to use for auto-generated signing key
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"

// Set default algorithm for auto-generated symmetric encryption key
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"

// Use encryption or not by default
const DEFAULT_ENCRYPTION_ENABLED = false

const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days

const now = () => (Date.now() / 1000) | 0

/** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
export async function encode({
token = {},
maxAge = DEFAULT_MAX_AGE,
secret,
signingKey,
signingOptions = {
expiresIn: `${maxAge}s`,
},
encryptionKey,
encryptionOptions = {
alg: "dir",
enc: DEFAULT_ENCRYPTION_ALGORITHM,
zip: "DEF",
},
encryption = DEFAULT_ENCRYPTION_ENABLED,
maxAge = DEFAULT_MAX_AGE,
}: JWTEncodeParams) {
// Signing Key
const _signingKey = signingKey
? jose.JWK.asKey(JSON.parse(signingKey))
: getDerivedSigningKey(secret)

// Sign token
const signedToken = jose.JWT.sign(token, _signingKey, signingOptions)

if (encryption) {
// Encryption Key
const _encryptionKey = encryptionKey
? jose.JWK.asKey(JSON.parse(encryptionKey))
: getDerivedEncryptionKey(secret)

// Encrypt token
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
}
return signedToken
const encryptionSecret = await getDerivedEncryptionKey(secret)
return await new EncryptJWT(token)
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
.setIssuedAt()
.setExpirationTime(now() + maxAge)
.setJti(crypto.randomUUID ? crypto.randomUUID() : uuid())
.encrypt(encryptionSecret)
}

/** Decodes a NextAuth.js issued JWT. */
export async function decode({
secret,
token,
maxAge = DEFAULT_MAX_AGE,
signingKey,
verificationKey = signingKey, // Optional (defaults to encryptionKey)
verificationOptions = {
maxTokenAge: `${maxAge}s`,
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
},
encryptionKey,
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
decryptionOptions = {
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
},
encryption = DEFAULT_ENCRYPTION_ENABLED,
secret,
}: JWTDecodeParams): Promise<JWT | null> {
if (!token) return null

let tokenToVerify = token

if (encryption) {
// Encryption Key
const _encryptionKey = decryptionKey
? jose.JWK.asKey(JSON.parse(decryptionKey))
: getDerivedEncryptionKey(secret)

// Decrypt token
const decryptedToken = jose.JWE.decrypt(
token,
_encryptionKey,
decryptionOptions
)
tokenToVerify = decryptedToken.toString("utf8")
}

// Signing Key
const _signingKey = verificationKey
? jose.JWK.asKey(JSON.parse(verificationKey))
: getDerivedSigningKey(secret)

// Verify token
return jose.JWT.verify(
tokenToVerify,
_signingKey,
verificationOptions
) as JWT | null
const encryptionSecret = await getDerivedEncryptionKey(secret)
const { payload } = await jwtDecrypt(token, encryptionSecret, {
clockTolerance: 15,
})
return payload
}

export type GetTokenParams<R extends boolean = false> = {
/** The request containing the JWT either in the cookies or in the `Authorization` header. */
req: NextApiRequest
/**
* Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http://
* or not set (e.g. development or test instance) case use unprefixed name
*/
secureCookie?: boolean
/** If the JWT is in the cookie, what name `getToken()` should look for. */
cookieName?: string
/**
* `getToken()` will return the raw JWT if this is set to `true`
* @default false
*/
raw?: R
decode?: typeof decode
secret?: string
} & Omit<JWTDecodeParams, "secret">
} & Pick<JWTOptions, "decode" | "secret">

/** [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken) */
/**
* Takes a NextAuth.js request (`req`) and returns either the NextAuth.js issued JWT's payload,
* or the raw JWT string. We look for the JWT in the either the cookies, or the `Authorization` header.
* [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken)
*/
export async function getToken<R extends boolean = false>(
params?: GetTokenParams<R>
): Promise<R extends true ? string : JWT | null> {
const {
req,
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
// or not set (e.g. development or test instance) case use unprefixed name
secureCookie = !(
!process.env.NEXTAUTH_URL ||
process.env.NEXTAUTH_URL.startsWith("http://")
),
cookieName = secureCookie
? "__Secure-next-auth.session-token"
: "next-auth.session-token",
raw = false,
raw,
decode: _decode = decode,
} = params ?? {}

if (!req) throw new Error("Must pass `req` to JWT getToken()")

// Try to get token from cookie
let token = req.cookies[cookieName]

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

// Generate warning (but only once at startup) when auto-generated keys are used
let DERIVED_SIGNING_KEY_WARNING = false
let DERIVED_ENCRYPTION_KEY_WARNING = false

// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
if (crypto.hkdfSync) {
return Buffer.from(
crypto.hkdfSync(
/** Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one */
async function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
if (crypto.hkdf) {
return await new Promise((resolve, reject) => {
crypto.hkdf(
digest,
secret,
Buffer.alloc(0),
encryptionInfo,
byteLength
byteLength,
(err, derivedKey) => {
if (err) reject(err)
else resolve(Buffer.from(derivedKey))
}
)
)
})
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require("futoin-hkdf")(secret, byteLength, {
Expand All @@ -180,38 +123,9 @@ function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
})
}

function getDerivedSigningKey(secret) {
if (!DERIVED_SIGNING_KEY_WARNING) {
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
DERIVED_SIGNING_KEY_WARNING = true
}

const buffer = hkdf(secret, {
byteLength: 64,
encryptionInfo: "NextAuth.js Generated Signing Key",
})
const key = jose.JWK.asKey(buffer, {
alg: DEFAULT_SIGNATURE_ALGORITHM,
use: "sig",
kid: "nextauth-auto-generated-signing-key",
})
return key
}

function getDerivedEncryptionKey(secret) {
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
DERIVED_ENCRYPTION_KEY_WARNING = true
}

const buffer = hkdf(secret, {
async function getDerivedEncryptionKey(secret) {
return await hkdf(secret, {
byteLength: 32,
encryptionInfo: "NextAuth.js Generated Encryption Key",
})
const key = jose.JWK.asKey(buffer, {
alg: DEFAULT_ENCRYPTION_ALGORITHM,
use: "enc",
kid: "nextauth-auto-generated-encryption-key",
})
return key
}
Loading