diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md index 591e68adc..97f2f4b4e 100644 --- a/V2_MIGRATION_GUIDE.md +++ b/V2_MIGRATION_GUIDE.md @@ -2,11 +2,38 @@ Guide to migrating from `1.x` to `2.x` +- [`getSession` now returns a `Promise`](#getsession-now-returns-a-promise) - [`updateUser` has been added](#updateuser-has-been-added) - [`getServerSidePropsWrapper` has been removed](#getserversidepropswrapper-has-been-removed) - [Profile API route no longer returns a 401](#profile-api-route-no-longer-returns-a-401) - [The ID token is no longer stored by default](#the-id-token-is-no-longer-stored-by-default) +## `getSession` now returns a `Promise` + +### Before + +```js +// /pages/api/my-api +import { getSession } from '@auth0/nextjs-auth0'; + +function myApiRoute(req, res) { + const session = getSession(req, res); + // ... +} +``` + +### After + +```js +// /pages/api/my-api +import { getSession } from '@auth0/nextjs-auth0'; + +async function myApiRoute(req, res) { + const session = await getSession(req, res); + // ... +} +``` + ## `updateUser` has been added ### Before @@ -30,17 +57,17 @@ function myApiRoute(req, res) { We've introduced a new `updateUser` method which must be explicitly invoked in order to update the session's user. -This will immediately serialise the session and write it to the cookie. +This will immediately serialise the session, write it to the cookie and return a `Promise`. ```js // /pages/api/update-user import { getSession, updateUser } from '@auth0/nextjs-auth0'; -function myApiRoute(req, res) { - const { user } = getSession(req, res); +async function myApiRoute(req, res) { + const { user } = await getSession(req, res); // The session is updated, serialized and the cookie is updated // everytime you call `updateUser`. - updateUser(req, res, { ...user, foo: 'bar' }); + await updateUser(req, res, { ...user, foo: 'bar' }); res.json({ success: true }); } ``` @@ -52,7 +79,7 @@ Because the process of modifying the session is now explicit, you no longer have ### Before ```js -export const getServerSideProps = getServerSidePropsWrapper(async (ctx) => { +export const getServerSideProps = getServerSidePropsWrapper((ctx) => { const session = getSession(ctx.req, ctx.res); if (session) { // User is authenticated @@ -66,7 +93,7 @@ export const getServerSideProps = getServerSidePropsWrapper(async (ctx) => { ```js export const getServerSideProps = async (ctx) => { - const session = getSession(ctx.req, ctx.res); + const session = await getSession(ctx.req, ctx.res); if (session) { // User is authenticated } else { diff --git a/package-lock.json b/package-lock.json index ce08d5b8c..f8cf29f5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,12 @@ "version": "1.9.1", "license": "MIT", "dependencies": { - "base64url": "^3.0.1", + "@panva/hkdf": "^1.0.2", "cookie": "^0.5.0", "debug": "^4.3.4", - "futoin-hkdf": "^1.5.0", "http-errors": "^1.8.1", "joi": "^17.6.0", - "jose": "^2.0.5", + "jose": "^4.9.2", "openid-client": "^4.9.1", "tslib": "^2.4.0", "url-join": "^4.0.1" @@ -1803,6 +1802,14 @@ "node": ">=10.13.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz", + "integrity": "sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@sideway/address": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", @@ -3323,14 +3330,6 @@ } ] }, - "node_modules/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -6285,14 +6284,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/futoin-hkdf": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.1.tgz", - "integrity": "sha512-g5d0Qp7ks55hYmYmfqn4Nz18XH49lcCR+vvIvHT92xXnsJaGZmY1EtWQWilJ6BQp57heCIXM/rRo+AFep8hGgg==", - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9252,15 +9243,9 @@ } }, "node_modules/jose": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", - "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", - "dependencies": { - "@panva/asn1.js": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0 < 13 || >=13.7.0" - }, + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.9.2.tgz", + "integrity": "sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -10377,15 +10362,6 @@ "node": ">= 0.6" } }, - "node_modules/oidc-provider/node_modules/jose": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.8.1.tgz", - "integrity": "sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/oidc-provider/node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -10497,6 +10473,20 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/openid-client/node_modules/jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -14800,6 +14790,11 @@ "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" }, + "@panva/hkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz", + "integrity": "sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==" + }, "@sideway/address": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", @@ -16000,11 +15995,6 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, - "base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" - }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -18304,11 +18294,6 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, - "futoin-hkdf": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.1.tgz", - "integrity": "sha512-g5d0Qp7ks55hYmYmfqn4Nz18XH49lcCR+vvIvHT92xXnsJaGZmY1EtWQWilJ6BQp57heCIXM/rRo+AFep8hGgg==" - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -20557,12 +20542,9 @@ } }, "jose": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", - "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", - "requires": { - "@panva/asn1.js": "^1.0.0" - } + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.9.2.tgz", + "integrity": "sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw==" }, "js-tokens": { "version": "4.0.0", @@ -21420,12 +21402,6 @@ "toidentifier": "1.0.0" } }, - "jose": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.8.1.tgz", - "integrity": "sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw==", - "dev": true - }, "jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -21507,6 +21483,16 @@ "make-error": "^1.3.6", "object-hash": "^2.0.1", "oidc-token-hash": "^5.0.1" + }, + "dependencies": { + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + } } }, "optionator": { diff --git a/package.json b/package.json index 694a89479..60dd26d2d 100644 --- a/package.json +++ b/package.json @@ -107,13 +107,12 @@ "typescript": "^4.1.3" }, "dependencies": { - "base64url": "^3.0.1", + "@panva/hkdf": "^1.0.2", "cookie": "^0.5.0", "debug": "^4.3.4", - "futoin-hkdf": "^1.5.0", "http-errors": "^1.8.1", "joi": "^17.6.0", - "jose": "^2.0.5", + "jose": "^4.9.2", "openid-client": "^4.9.1", "tslib": "^2.4.0", "url-join": "^4.0.1" diff --git a/src/auth0-session/cookie-store.ts b/src/auth0-session/cookie-store.ts index 529503416..f04a958d7 100644 --- a/src/auth0-session/cookie-store.ts +++ b/src/auth0-session/cookie-store.ts @@ -1,11 +1,11 @@ import { IncomingMessage, ServerResponse } from 'http'; import { strict as assert, AssertionError } from 'assert'; -import { JWE, JWK, JWKS, errors } from 'jose'; +import * as jose from 'jose'; +import { CookieSerializeOptions, serialize } from 'cookie'; import { encryption as deriveKey } from './utils/hkdf'; import createDebug from './utils/debug'; import Cookies from './utils/cookies'; import { Config } from './config'; -import { CookieSerializeOptions, serialize } from 'cookie'; const debug = createDebug('cookie-store'); const epoch = (): number => (Date.now() / 1000) | 0; // eslint-disable-line no-bitwise @@ -13,27 +13,15 @@ const MAX_COOKIE_SIZE = 4096; const alg = 'dir'; const enc = 'A256GCM'; +type Header = { iat: number; uat: number; exp: number }; const notNull = (value: T | null): value is T => value !== null; export default class CookieStore { - private keystore: JWKS.KeyStore; - - private currentKey: JWK.OctKey | undefined; + private keys?: Uint8Array[]; private chunkSize: number; constructor(public config: Config) { - const secrets = Array.isArray(config.secret) ? config.secret : [config.secret]; - this.keystore = new JWKS.KeyStore(); - - secrets.forEach((secretString: string, i: number) => { - const key = JWK.asKey(deriveKey(secretString)); - if (i === 0) { - this.currentKey = key as JWK.OctKey; - } - this.keystore.add(key); - }); - const { cookie: { transient, ...cookieConfig }, name: sessionName @@ -49,20 +37,31 @@ export default class CookieStore { this.chunkSize = MAX_COOKIE_SIZE - emptyCookie.length; } - private encrypt(payload: string, headers: { [key: string]: any }): string { - return JWE.encrypt(payload, this.currentKey as JWK.OctKey, { - alg, - enc, - ...headers - }); + private async getKeys(): Promise { + if (!this.keys) { + const secret = this.config.secret; + const secrets = Array.isArray(secret) ? secret : [secret]; + this.keys = await Promise.all(secrets.map(deriveKey)); + } + return this.keys; + } + + private async encrypt(payload: jose.JWTPayload, { iat, uat, exp }: Header): Promise { + const [key] = await this.getKeys(); + return await new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg, enc, uat, iat, exp }).encrypt(key); } - private decrypt(jwe: string): JWE.completeDecrypt { - return JWE.decrypt(jwe, this.keystore, { - complete: true, - contentEncryptionAlgorithms: [enc], - keyManagementAlgorithms: [alg] - }); + private async decrypt(jwe: string): Promise { + const keys = await this.getKeys(); + let err; + for (let key of keys) { + try { + return await jose.jwtDecrypt(jwe, key); + } catch (e) { + err = e; + } + } + throw err; } private calculateExp(iat: number, uat: number): number { @@ -78,13 +77,13 @@ export default class CookieStore { return Math.min(uat + (rollingDuration as number), iat + absoluteDuration); } - public read(req: IncomingMessage): [{ [key: string]: any }?, number?] { + public async read(req: any): Promise<[{ [key: string]: any }?, number?]> { const cookies = Cookies.getAll(req); const { name: sessionName, rollingDuration, absoluteDuration } = this.config.session; - let iat; - let uat; - let exp; + let iat: number; + let uat: number; + let exp: number; let existingSessionValue; try { @@ -118,8 +117,8 @@ export default class CookieStore { } if (existingSessionValue) { - const { protected: header, cleartext } = this.decrypt(existingSessionValue); - ({ iat, uat, exp } = header as { iat: number; uat: number; exp: number }); + const { protectedHeader: header, payload } = await this.decrypt(existingSessionValue); + ({ iat, uat, exp } = header as unknown as Header); // check that the existing session isn't expired based on options when it was established assert(exp > epoch(), 'it is expired based on options when it was established'); @@ -134,13 +133,13 @@ export default class CookieStore { assert(iat + absoluteDuration > epoch(), 'it is expired based on current absoluteDuration rules'); } - return [JSON.parse(cleartext.toString()), iat]; + return [payload, iat]; } } catch (err) { /* istanbul ignore else */ if (err instanceof AssertionError) { debug('existing session was rejected because', err.message); - } else if (err instanceof errors.JOSEError) { + } else if (err instanceof jose.errors.JOSEError) { debug('existing session was rejected because it could not be decrypted', err); } else { debug('unexpected error handling session', err); @@ -150,12 +149,12 @@ export default class CookieStore { return []; } - public save( + public async save( req: IncomingMessage, res: ServerResponse, session: { [key: string]: any } | undefined | null, createdAt?: number - ): void { + ): Promise { const { cookie: { transient, ...cookieConfig }, name: sessionName @@ -186,7 +185,7 @@ export default class CookieStore { } debug('found session, creating signed session cookie(s) with name %o(.i)', sessionName); - const value = this.encrypt(JSON.stringify(session), { iat, uat, exp }); + const value = await this.encrypt(session, { iat, uat, exp }); const chunkCount = Math.ceil(value.length / this.chunkSize); if (chunkCount > 1) { diff --git a/src/auth0-session/handlers/callback.ts b/src/auth0-session/handlers/callback.ts index 9e0e91682..606c933fc 100644 --- a/src/auth0-session/handlers/callback.ts +++ b/src/auth0-session/handlers/callback.ts @@ -40,10 +40,10 @@ export default function callbackHandlerFactory( let tokenSet; try { const callbackParams = client.callbackParams(req); - expectedState = transientCookieHandler.read('state', req, res); - const max_age = transientCookieHandler.read('max_age', req, res); - const code_verifier = transientCookieHandler.read('code_verifier', req, res); - const nonce = transientCookieHandler.read('nonce', req, res); + expectedState = await transientCookieHandler.read('state', req, res); + const max_age = await transientCookieHandler.read('max_age', req, res); + const code_verifier = await transientCookieHandler.read('code_verifier', req, res); + const nonce = await transientCookieHandler.read('nonce', req, res); tokenSet = await client.callback( redirectUri, @@ -65,13 +65,13 @@ export default function callbackHandlerFactory( } const openidState: { returnTo?: string } = decodeState(expectedState as string) as ValidState; - let session = sessionCache.fromTokenSet(tokenSet); + let session = await sessionCache.fromTokenSet(tokenSet); if (options?.afterCallback) { session = await options.afterCallback(req as any, res as any, session, openidState); } - sessionCache.create(req, res, session); + await sessionCache.create(req, res, session); res.writeHead(302, { Location: openidState.returnTo || config.baseURL diff --git a/src/auth0-session/handlers/login.ts b/src/auth0-session/handlers/login.ts index 630702609..bb605f590 100644 --- a/src/auth0-session/handlers/login.ts +++ b/src/auth0-session/handlers/login.ts @@ -58,15 +58,15 @@ export default function loginHandlerFactory( const authParams = { ...opts.authorizationParams, - nonce: transientHandler.save('nonce', req, res, transientOpts), - state: transientHandler.save('state', req, res, { + nonce: await transientHandler.save('nonce', req, res, transientOpts), + state: await transientHandler.save('state', req, res, { ...transientOpts, value: encodeState(stateValue) }), ...(usePKCE ? { code_challenge: transientHandler.calculateCodeChallenge( - transientHandler.save('code_verifier', req, res, transientOpts) + await transientHandler.save('code_verifier', req, res, transientOpts) ), code_challenge_method: 'S256' } @@ -81,7 +81,7 @@ export default function loginHandlerFactory( assert(/\bopenid\b/.test(authParams.scope as string), 'scope should contain "openid"'); if (authParams.max_age) { - transientHandler.save('max_age', req, res, { + await transientHandler.save('max_age', req, res, { ...transientOpts, value: authParams.max_age.toString() }); diff --git a/src/auth0-session/handlers/logout.ts b/src/auth0-session/handlers/logout.ts index 5e25691eb..41f142cc1 100644 --- a/src/auth0-session/handlers/logout.ts +++ b/src/auth0-session/handlers/logout.ts @@ -24,7 +24,8 @@ export default function logoutHandlerFactory( returnURL = urlJoin(config.baseURL, returnURL); } - if (!sessionCache.isAuthenticated(req, res)) { + const isAuthenticated = await sessionCache.isAuthenticated(req, res); + if (!isAuthenticated) { debug('end-user already logged out, redirecting to %s', returnURL); res.writeHead(302, { Location: returnURL @@ -33,8 +34,8 @@ export default function logoutHandlerFactory( return; } - const idToken = sessionCache.getIdToken(req, res); - sessionCache.delete(req, res); + const idToken = await sessionCache.getIdToken(req, res); + await sessionCache.delete(req, res); if (!config.idpLogout) { debug('performing a local only logout, redirecting to %s', returnURL); diff --git a/src/auth0-session/hooks/get-login-state.ts b/src/auth0-session/hooks/get-login-state.ts index 130e58d8a..07df627b3 100644 --- a/src/auth0-session/hooks/get-login-state.ts +++ b/src/auth0-session/hooks/get-login-state.ts @@ -1,6 +1,7 @@ -import base64url from 'base64url'; +import * as jose from 'jose'; import createDebug from '../utils/debug'; import { GetLoginState } from '../config'; +import { TextDecoder } from 'util'; const debug = createDebug('get-login-state'); @@ -32,7 +33,7 @@ export function encodeState(stateObject: { [key: string]: any }): string { // only stored in its dedicated transient cookie. // eslint-disable-next-line @typescript-eslint/no-unused-vars const { nonce, code_verifier, max_age, ...filteredState } = stateObject; - return base64url.encode(JSON.stringify(filteredState)); + return jose.base64url.encode(JSON.stringify(filteredState)); } /** @@ -44,7 +45,7 @@ export function encodeState(stateObject: { [key: string]: any }): string { */ export function decodeState(stateValue?: string): { [key: string]: any } | undefined { try { - return JSON.parse(base64url.decode(stateValue as string)); + return JSON.parse(new TextDecoder().decode(jose.base64url.decode(stateValue as string))); } catch (e) { return undefined; } diff --git a/src/auth0-session/session-cache.ts b/src/auth0-session/session-cache.ts index a935f2aa8..1f0723903 100644 --- a/src/auth0-session/session-cache.ts +++ b/src/auth0-session/session-cache.ts @@ -2,9 +2,9 @@ import { IncomingMessage, ServerResponse } from 'http'; import { TokenSet } from 'openid-client'; export interface SessionCache { - create(req: IncomingMessage, res: ServerResponse, session: { [key: string]: any }): void; - delete(req: IncomingMessage, res: ServerResponse): void; - isAuthenticated(req: IncomingMessage, res: ServerResponse): boolean; - getIdToken(req: IncomingMessage, res: ServerResponse): string | undefined; + create(req: IncomingMessage, res: ServerResponse, session: { [key: string]: any }): Promise; + delete(req: IncomingMessage, res: ServerResponse): Promise; + isAuthenticated(req: IncomingMessage, res: ServerResponse): Promise; + getIdToken(req: IncomingMessage, res: ServerResponse): Promise; fromTokenSet(tokenSet: TokenSet): { [key: string]: any }; } diff --git a/src/auth0-session/transient-store.ts b/src/auth0-session/transient-store.ts index fefbebdf7..16caa44f1 100644 --- a/src/auth0-session/transient-store.ts +++ b/src/auth0-session/transient-store.ts @@ -1,6 +1,6 @@ import { IncomingMessage, ServerResponse } from 'http'; import { generators } from 'openid-client'; -import { JWKS, JWS, JWK } from 'jose'; +import * as jose from 'jose'; import { signing as deriveKey } from './utils/hkdf'; import Cookies from './utils/cookies'; import { Config } from './config'; @@ -10,70 +10,46 @@ export interface StoreOptions { value?: string; } -const header = { alg: 'HS256', b64: false, crit: ['b64'] }; -const getPayload = (cookie: string, value: string): Buffer => Buffer.from(`${cookie}=${value}`); -const flattenedJWSFromCookie = (cookie: string, value: string, signature: string): JWS.FlattenedJWS => ({ - protected: Buffer.from(JSON.stringify(header)) - .toString('base64') - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'), - payload: getPayload(cookie, value), - signature -}); -const generateSignature = (cookie: string, value: string, key: JWK.Key): string => { - const payload = getPayload(cookie, value); - return JWS.sign.flattened(payload, key, header).signature; -}; -const verifySignature = (cookie: string, value: string, signature: string, keystore: JWKS.KeyStore): boolean => { - try { - return !!JWS.verify(flattenedJWSFromCookie(cookie, value, signature), keystore, { - algorithms: ['HS256'], - crit: ['b64'] - }); - } catch (err) { - return false; - } -}; -const getCookieValue = (cookie: string, value: string, keystore: JWKS.KeyStore): string | undefined => { - if (!value) { +const getCookieValue = async (k: string, v: string, keys: Uint8Array[]): Promise => { + if (!v) { return undefined; } - let signature; - [value, signature] = value.split('.'); - if (verifySignature(cookie, value, signature, keystore)) { - return value; + const [value, signature] = v.split('.'); + const flattenedJWS = { + protected: jose.base64url.encode(JSON.stringify({ alg: 'HS256', b64: false, crit: ['b64'] })), + payload: `${k}=${value}`, + signature + }; + for (let key of keys) { + try { + await jose.flattenedVerify(flattenedJWS, key, { + algorithms: ['HS256'] + }); + return value; + } catch (e) {} } - - return undefined; + return; }; -export const generateCookieValue = (cookie: string, value: string, key: JWK.Key): string => { - const signature = generateSignature(cookie, value, key); +export const generateCookieValue = async (cookie: string, value: string, key: Uint8Array): Promise => { + const { signature } = await new jose.FlattenedSign(new TextEncoder().encode(`${cookie}=${value}`)) + .setProtectedHeader({ alg: 'HS256', b64: false, crit: ['b64'] }) + .sign(key); return `${value}.${signature}`; }; export default class TransientStore { - private currentKey: JWK.Key | undefined; - - private keyStore: JWKS.KeyStore; + private keys?: Uint8Array[]; - constructor(private config: Config) { - let current; + constructor(private config: Config) {} - const secret = config.secret; - const secrets = Array.isArray(secret) ? secret : [secret]; - const keystore = new JWKS.KeyStore(); - secrets.forEach((secretString, i) => { - const key = JWK.asKey(deriveKey(secretString)); - if (i === 0) { - current = key; - } - keystore.add(key); - }); - - this.currentKey = current; - this.keyStore = keystore; + private async getKeys(): Promise { + if (!this.keys) { + const secret = this.config.secret; + const secrets = Array.isArray(secret) ? secret : [secret]; + this.keys = await Promise.all(secrets.map(deriveKey)); + } + return this.keys; } /** @@ -88,12 +64,12 @@ export default class TransientStore { * * @return {String} Cookie value that was set. */ - save( + async save( key: string, _req: IncomingMessage, res: ServerResponse, { sameSite = 'none', value = this.generateNonce() }: StoreOptions - ): string { + ): Promise { const isSameSiteNone = sameSite === 'none'; const { domain, path, secure } = this.config.session.cookie; const basicAttr = { @@ -102,11 +78,12 @@ export default class TransientStore { domain, path }; + const [signingKey] = await this.getKeys(); const cookieSetter = new Cookies(); { - const cookieValue = generateCookieValue(key, value, this.currentKey as JWK.Key); - // Set the cookie with the SameSite attribute and, if needed, the Secure flag + const cookieValue = await generateCookieValue(key, value, signingKey); + // Set the cookie with the SameSite attribute and, if needed, the Secure flag. cookieSetter.set(key, cookieValue, { ...basicAttr, sameSite, @@ -115,8 +92,8 @@ export default class TransientStore { } if (isSameSiteNone && this.config.legacySameSiteCookie) { - const cookieValue = generateCookieValue(`_${key}`, value, this.currentKey as JWK.Key); - // Set the fallback cookie with no SameSite or Secure attributes + const cookieValue = await generateCookieValue(`_${key}`, value, signingKey); + // Set the fallback cookie with no SameSite or Secure attributes. cookieSetter.set(`_${key}`, cookieValue, basicAttr); } @@ -133,20 +110,21 @@ export default class TransientStore { * * @return {String|undefined} Cookie value or undefined if cookie was not found. */ - read(key: string, req: IncomingMessage, res: ServerResponse): string | undefined { + async read(key: string, req: IncomingMessage, res: ServerResponse): Promise { const cookies = Cookies.getAll(req); const cookie = cookies[key]; const cookieConfig = this.config.session.cookie; const cookieSetter = new Cookies(); - let value = getCookieValue(key, cookie, this.keyStore); + const verifyingKeys = await this.getKeys(); + let value = await getCookieValue(key, cookie, verifyingKeys); cookieSetter.clear(key, cookieConfig); if (this.config.legacySameSiteCookie) { const fallbackKey = `_${key}`; if (!value) { const fallbackCookie = cookies[fallbackKey]; - value = getCookieValue(fallbackKey, fallbackCookie, this.keyStore); + value = await getCookieValue(fallbackKey, fallbackCookie, verifyingKeys); } cookieSetter.clear(fallbackKey, cookieConfig); } diff --git a/src/auth0-session/utils/hkdf.ts b/src/auth0-session/utils/hkdf.ts index 2e8973f56..6d9cd6a37 100644 --- a/src/auth0-session/utils/hkdf.ts +++ b/src/auth0-session/utils/hkdf.ts @@ -1,9 +1,9 @@ -import hkdf from 'futoin-hkdf'; +import hkdf from '@panva/hkdf'; const BYTE_LENGTH = 32; const ENCRYPTION_INFO = 'JWE CEK'; const SIGNING_INFO = 'JWS Cookie Signing'; -const options = { hash: 'SHA-256' }; +const digest = 'sha256'; /** * @@ -13,5 +13,6 @@ const options = { hash: 'SHA-256' }; * @see https://tools.ietf.org/html/rfc5869 * */ -export const encryption = (secret: string): Buffer => hkdf(secret, BYTE_LENGTH, { info: ENCRYPTION_INFO, ...options }); -export const signing = (secret: string): Buffer => hkdf(secret, BYTE_LENGTH, { info: SIGNING_INFO, ...options }); +export const encryption = (secret: string): Promise => + hkdf(digest, secret, '', ENCRYPTION_INFO, BYTE_LENGTH); +export const signing = (secret: string): Promise => hkdf(digest, secret, '', SIGNING_INFO, BYTE_LENGTH); diff --git a/src/config.ts b/src/config.ts index da92a9507..bf089241a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,8 @@ export interface BaseConfig { /** * The secret(s) used to derive an encryption key for the user identity in a session cookie and * to sign the transient cookies used by the login callback. - * Use a single string key or array of keys for an encrypted session cookie. + * Provide a single string secret, but if you want to rotate the secret you can provide an array putting + * the new secret first. * You can also use the `AUTH0_SECRET` environment variable. */ secret: string | Array; diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts index 3b797d9f0..7d7fbf28d 100644 --- a/src/handlers/profile.ts +++ b/src/handlers/profile.ts @@ -49,12 +49,12 @@ export default function profileHandler( try { assertReqRes(req, res); - if (!sessionCache.isAuthenticated(req, res)) { + if (!(await sessionCache.isAuthenticated(req, res))) { res.status(204).end(); return; } - const session = sessionCache.get(req, res) as Session; + const session = (await sessionCache.get(req, res)) as Session; res.setHeader('Cache-Control', 'no-store'); if (options?.refetch) { @@ -78,7 +78,7 @@ export default function profileHandler( newSession = await options.afterRefetch(req, res, newSession); } - sessionCache.set(req, res, newSession); + await sessionCache.set(req, res, newSession); res.json(newSession.user); return; diff --git a/src/helpers/with-api-auth-required.ts b/src/helpers/with-api-auth-required.ts index 0b7368b51..76597cfb7 100644 --- a/src/helpers/with-api-auth-required.ts +++ b/src/helpers/with-api-auth-required.ts @@ -26,18 +26,19 @@ export type WithApiAuthRequired = (apiRoute: NextApiHandler) => NextApiHandler; * @ignore */ export default function withApiAuthFactory(sessionCache: SessionCache): WithApiAuthRequired { - return (apiRoute) => async (req: NextApiRequest, res: NextApiResponse): Promise => { - assertReqRes(req, res); + return (apiRoute) => + async (req: NextApiRequest, res: NextApiResponse): Promise => { + assertReqRes(req, res); - const session = sessionCache.get(req, res); - if (!session || !session.user) { - res.status(401).json({ - error: 'not_authenticated', - description: 'The user does not have an active session or is not authenticated' - }); - return; - } + const session = await sessionCache.get(req, res); + if (!session || !session.user) { + res.status(401).json({ + error: 'not_authenticated', + description: 'The user does not have an active session or is not authenticated' + }); + return; + } - await apiRoute(req, res); - }; + await apiRoute(req, res); + }; } diff --git a/src/helpers/with-page-auth-required.ts b/src/helpers/with-page-auth-required.ts index 1e813b136..ac588f0d2 100644 --- a/src/helpers/with-page-auth-required.ts +++ b/src/helpers/with-page-auth-required.ts @@ -116,7 +116,7 @@ export default function withPageAuthRequiredFactory( return async (ctx: GetServerSidePropsContext): Promise => { assertCtx(ctx); const sessionCache = getSessionCache(); - const session = sessionCache.get(ctx.req, ctx.res); + const session = await sessionCache.get(ctx.req, ctx.res); if (!session?.user) { return { redirect: { diff --git a/src/session/cache.ts b/src/session/cache.ts index 773508ef0..6cc8a69f9 100644 --- a/src/session/cache.ts +++ b/src/session/cache.ts @@ -16,52 +16,52 @@ export default class SessionCache implements ISessionCache { this.iatCache = new WeakMap(); } - init(req: NextApiOrPageRequest, res: NextApiOrPageResponse, autoSave = true): void { + async init(req: NextApiOrPageRequest, res: NextApiOrPageResponse, autoSave = true): Promise { if (!this.cache.has(req)) { - const [json, iat] = this.cookieStore.read(req); + const [json, iat] = await this.cookieStore.read(req); this.iatCache.set(req, iat); this.cache.set(req, fromJson(json)); if (this.config.session.rolling && autoSave) { - this.save(req, res); + await this.save(req, res); } } } - save(req: NextApiOrPageRequest, res: NextApiOrPageResponse): void { - this.cookieStore.save(req, res, this.cache.get(req), this.iatCache.get(req)); + async save(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Promise { + await this.cookieStore.save(req, res, this.cache.get(req), this.iatCache.get(req)); } - create(req: NextApiOrPageRequest, res: NextApiOrPageResponse, session: Session): void { + async create(req: NextApiOrPageRequest, res: NextApiOrPageResponse, session: Session): Promise { this.cache.set(req, session); - this.save(req, res); + await this.save(req, res); } - delete(req: NextApiOrPageRequest, res: NextApiOrPageResponse): void { - this.init(req, res, false); + async delete(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Promise { + await this.init(req, res, false); this.cache.set(req, null); - this.save(req, res); + await this.save(req, res); } - isAuthenticated(req: NextApiOrPageRequest, res: NextApiOrPageResponse): boolean { - this.init(req, res); + async isAuthenticated(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Promise { + await this.init(req, res); const session = this.cache.get(req); return !!session?.user; } - getIdToken(req: NextApiOrPageRequest, res: NextApiOrPageResponse): string | undefined { - this.init(req, res); + async getIdToken(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Promise { + await this.init(req, res); const session = this.cache.get(req); return session?.idToken; } - set(req: NextApiOrPageRequest, res: NextApiOrPageResponse, session: Session | null): void { - this.init(req, res, false); + async set(req: NextApiOrPageRequest, res: NextApiOrPageResponse, session: Session | null): Promise { + await this.init(req, res, false); this.cache.set(req, session); - this.save(req, res); + await this.save(req, res); } - get(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Session | null | undefined { - this.init(req, res); + async get(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Promise { + await this.init(req, res); return this.cache.get(req); } diff --git a/src/session/get-access-token.ts b/src/session/get-access-token.ts index 4b85dee27..85221f34d 100644 --- a/src/session/get-access-token.ts +++ b/src/session/get-access-token.ts @@ -97,7 +97,7 @@ export default function accessTokenFactory( sessionCache: SessionCache ): GetAccessToken { return async (req, res, accessTokenRequest): Promise => { - let session = sessionCache.get(req, res); + let session = await sessionCache.get(req, res); if (!session) { throw new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, 'The user does not have a valid session.'); } @@ -177,7 +177,7 @@ export default function accessTokenFactory( session = await accessTokenRequest.afterRefresh(req as NextApiRequest, res as NextApiResponse, session); } - sessionCache.set(req, res, session); + await sessionCache.set(req, res, session); // Return the new access token. return { diff --git a/src/session/get-session.ts b/src/session/get-session.ts index 34f92982c..f119ee7b1 100644 --- a/src/session/get-session.ts +++ b/src/session/get-session.ts @@ -10,13 +10,13 @@ import { SessionCache, Session } from '../session'; export type GetSession = ( req: IncomingMessage | NextApiRequest, res: ServerResponse | NextApiResponse -) => Session | null | undefined; +) => Promise; /** * @ignore */ export default function sessionFactory(sessionCache: SessionCache): GetSession { - return (req, res): Session | null | undefined => { + return (req, res) => { return sessionCache.get(req, res); }; } diff --git a/src/session/update-user.ts b/src/session/update-user.ts index 73a8b40ff..b8a065587 100644 --- a/src/session/update-user.ts +++ b/src/session/update-user.ts @@ -26,18 +26,18 @@ export type UpdateUser = ( req: IncomingMessage | NextApiRequest, res: ServerResponse | NextApiResponse, user: Claims -) => void; +) => Promise; /** * @ignore */ export default function updateUserFactory(sessionCache: SessionCache): UpdateUser { - return (req, res, user): void => { - sessionCache.init(req, res, false); - const session = sessionCache.get(req, res); + return async (req, res, user) => { + await sessionCache.init(req, res, false); + const session = await sessionCache.get(req, res); if (!session || !user) { return; } - sessionCache.set(req, res, { ...session, user }); + await sessionCache.set(req, res, { ...session, user }); }; } diff --git a/tests/auth0-session/cookie-store.test.ts b/tests/auth0-session/cookie-store.test.ts index cba683d60..6b88ed2bf 100644 --- a/tests/auth0-session/cookie-store.test.ts +++ b/tests/auth0-session/cookie-store.test.ts @@ -1,5 +1,5 @@ import { randomBytes } from 'crypto'; -import { JWK, JWE } from 'jose'; +import * as jose from 'jose'; import { IdTokenClaims } from 'openid-client'; import { setup, teardown } from './fixtures/server'; import { defaultConfig, fromCookieJar, get, toCookieJar } from './fixtures/helpers'; @@ -8,28 +8,27 @@ import { makeIdToken } from './fixtures/cert'; const hr = 60 * 60 * 1000; const day = 24 * hr; -const key = JWK.asKey(deriveKey(defaultConfig.secret as string)); -const encrypted = (payload: Partial = { sub: '__test_sub__' }): string => { +const encrypted = async (claims: Partial = { sub: '__test_sub__' }): Promise => { + const key = await deriveKey(defaultConfig.secret as string); const epochNow = (Date.now() / 1000) | 0; const weekInSeconds = 7 * 24 * 60 * 60; - return JWE.encrypt( - JSON.stringify({ - access_token: '__test_access_token__', - token_type: 'Bearer', - id_token: makeIdToken(payload), - refresh_token: '__test_access_token__', - expires_at: epochNow + weekInSeconds - }), - key, - { + const payload = { + access_token: '__test_access_token__', + token_type: 'Bearer', + id_token: await makeIdToken(claims), + refresh_token: '__test_access_token__', + expires_at: epochNow + weekInSeconds + }; + return new jose.EncryptJWT({ ...payload }) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM', uat: epochNow, iat: epochNow, exp: epochNow + weekInSeconds - } - ); + }) + .encrypt(key); }; describe('CookieStore', () => { @@ -53,13 +52,13 @@ describe('CookieStore', () => { it('should not error with JWEDecryptionFailed when using old secrets', async () => { const baseURL = await setup({ ...defaultConfig, secret: ['__invalid_secret__', '__also_invalid__'] }); - const cookieJar = toCookieJar({ appSession: encrypted() }, baseURL); + const cookieJar = toCookieJar({ appSession: await encrypted() }, baseURL); await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized'); }); it('should get an existing session', async () => { const baseURL = await setup(defaultConfig); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); const session = await get(baseURL, '/session', { cookieJar }); expect(session).toMatchObject({ @@ -82,7 +81,7 @@ describe('CookieStore', () => { it('should chunk and accept chunked cookies over 4kb', async () => { const baseURL = await setup(defaultConfig); - const appSession = encrypted({ + const appSession = await encrypted({ big_claim: randomBytes(2000).toString('base64') }); expect(appSession.length).toBeGreaterThan(4000); @@ -105,7 +104,7 @@ describe('CookieStore', () => { const path = '/some-really-really-really-really-really-really-really-really-really-really-really-really-really-long-path'; const baseURL = await setup({ ...defaultConfig, session: { cookie: { path } } }); - const appSession = encrypted({ + const appSession = await encrypted({ big_claim: randomBytes(5000).toString('base64') }); expect(appSession.length).toBeGreaterThan(4096); @@ -117,12 +116,12 @@ describe('CookieStore', () => { expect(cookies['appSession.0']).toHaveLength(4096); expect(cookies['appSession.1']).toHaveLength(4096); expect(cookies['appSession.2']).toHaveLength(4096); - expect(cookies['appSession.3']).toHaveLength(1568); + expect(cookies['appSession.3'].length).toBeLessThan(4096); }); it('should handle unordered chunked cookies', async () => { const baseURL = await setup(defaultConfig); - const appSession = encrypted({ sub: '__chunked_sub__' }); + const appSession = await encrypted({ sub: '__chunked_sub__' }); const cookieJar = toCookieJar( { 'appSession.2': appSession.slice(20), @@ -150,7 +149,7 @@ describe('CookieStore', () => { it('should clean up single cookie when switching to chunked', async () => { const baseURL = await setup(defaultConfig); - const appSession = encrypted({ + const appSession = await encrypted({ big_claim: randomBytes(2000).toString('base64') }); expect(appSession.length).toBeGreaterThan(4000); @@ -164,7 +163,7 @@ describe('CookieStore', () => { it('should clean up chunked cookies when switching to a single cookie', async () => { const baseURL = await setup(defaultConfig); - const appSession = encrypted({ sub: 'foo' }); + const appSession = await encrypted({ sub: 'foo' }); const cookieJar = toCookieJar( { 'appSession.0': appSession.slice(0, 100), @@ -181,7 +180,7 @@ describe('CookieStore', () => { it('should set the default cookie options on http', async () => { const baseURL = await setup(defaultConfig); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -198,7 +197,7 @@ describe('CookieStore', () => { it('should set custom cookie options on http', async () => { const baseURL = await setup({ ...defaultConfig, session: { cookie: { httpOnly: false } } }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -209,7 +208,7 @@ describe('CookieStore', () => { it('should set the default cookie options on https', async () => { const baseURL = await setup(defaultConfig, { https: true }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -226,7 +225,7 @@ describe('CookieStore', () => { it('should set custom secure option on https', async () => { const baseURL = await setup({ ...defaultConfig, session: { cookie: { secure: false } } }, { https: true }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -238,7 +237,7 @@ describe('CookieStore', () => { it('should set custom sameSite option on https', async () => { const baseURL = await setup({ ...defaultConfig, session: { cookie: { sameSite: 'none' } } }, { https: true }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -250,7 +249,7 @@ describe('CookieStore', () => { it('should use a custom cookie name', async () => { const baseURL = await setup({ ...defaultConfig, session: { name: 'myCookie' } }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ myCookie: appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -261,7 +260,7 @@ describe('CookieStore', () => { it('should set an ephemeral cookie', async () => { const baseURL = await setup({ ...defaultConfig, session: { cookie: { transient: true } } }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await get(baseURL, '/session', { cookieJar }); const [cookie] = cookieJar.getCookiesSync(baseURL); @@ -274,12 +273,13 @@ describe('CookieStore', () => { const clock = jest.useFakeTimers('modern'); const baseURL = await setup(defaultConfig); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await expect(get(baseURL, '/session', { cookieJar })).resolves.not.toThrow(); jest.advanceTimersByTime(25 * hr); await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized'); clock.restoreAllMocks(); + jest.useRealTimers(); }); it('should expire after 7 days regardless of activity by default', async () => { @@ -287,7 +287,7 @@ describe('CookieStore', () => { let days = 7; const baseURL = await setup(defaultConfig); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); while (days--) { jest.advanceTimersByTime(23 * hr); @@ -296,6 +296,7 @@ describe('CookieStore', () => { jest.advanceTimersByTime(23 * hr); await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized'); clock.restoreAllMocks(); + jest.useRealTimers(); }); it('should expire only after custom absoluteDuration', async () => { @@ -308,7 +309,7 @@ describe('CookieStore', () => { absoluteDuration: (10 * day) / 1000 } }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); await expect(get(baseURL, '/session', { cookieJar })).resolves.not.toThrow(); jest.advanceTimersByTime(9 * day); @@ -316,6 +317,7 @@ describe('CookieStore', () => { jest.advanceTimersByTime(2 * day); await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized'); clock.restoreAllMocks(); + jest.useRealTimers(); }); it('should expire only after defined rollingDuration period of inactivty', async () => { @@ -331,7 +333,7 @@ describe('CookieStore', () => { } } }); - const appSession = encrypted(); + const appSession = await encrypted(); const cookieJar = toCookieJar({ appSession }, baseURL); let days = 30; while (days--) { @@ -341,5 +343,35 @@ describe('CookieStore', () => { jest.advanceTimersByTime(25 * hr); await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized'); clock.restoreAllMocks(); + jest.useRealTimers(); + }); + + it('should not logout v1 users', async () => { + // Cookie generated with v1 cookie store tests with v long absolute exp + const V1_COOKIE = + 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidWF0IjoxNjYyNDY2OTc2LCJpYXQiOjE2NjI0NjY5NzYsImV4cCI6NDgxODIyNjk3Nn0..3H_btn3Vk4SQhA0v.1tA8Olxj_1QTXRJYgY3FUtq1it-PunBKn2YKiO5cMCyf4ARF6sry4jfkq36aavaUYTh6w9mvAQawhcduOTzWOWtSbvRMOIlrOZTzUNohuLakKZA6ME2EgdLx1sMhuhtRdA1qACSDqly4qPw9IcOo1IUsYRzhtyI8MaYncjLzRHpo1Lvq_F5vtg5PIDTkYVnrhRX-SPsx6jbCr0rXxU3Cp9X8YYt-tl5yW-TLBPAeBy0TR-iiYJWMNyTMPE00o2LqsC2NQN7AySNtaaURb_a0cSpkF2X1fAb_iAKw-bg1wTruKUulErXkwTKPzZW6L0sGtnWN4qTg8gfxnoZxxrf7s-x2xCzKefiR0_8qpdfo0zhtE-PTYCFZxTU46yIkGZbJgVaH-tavoe1G3YhKMLEau49KV29agjVlN6eB5beEK2H70BgbaSPM4rcOhfqVeB5dku9olKCppI4UAtahaQqwnQrf1vd0W-qbslN_KO84QaBf8YlzGDbnfOAgXobqNnMu_-BoEInODK4azk_d9BquukhEm0g47XNYZuVCmgqNLo3Nul15lmHzZPGQ2ITivG7Dfb7sCLrKM6omioUjVCs6K6TCp100ndxQZuKUXYF2JkQoJhEge6MBSZDMF0cwIZlg1w8ArbPKl16zdZl8MYqDR_Vtwx7feT8sOvqST5he7oXp0yH2SvcG0dQbJLgPrmOOfDZIbjae11mcoIKa5oVVf4O_h-yHSVYyky8zLX-r7QP_H8CwMi19SysQa7S0b5BDlc5fn4ndf75TR7Zgg7r8PxzQiQghYoJXMZgzDpsaq8i33z2KMrwiGZPiDTuLmeOoV2BKNAVpBpad86BN_d2K7wAmPGx5ysWTc4mxSTv0b1E4G5_ZGDF59wl1m4o1zCSgMqZ56VCqb5qksPPhjpWjnbLnLw_6R_i4aqAxwHkdHOzbbSAGfwpBQF8PRDlmkIlZRQ9QRoLuWVdc3lJfX_Xp_ZKY9j8rKtOOC8BGq2yAZDIv0ezJPwLYEgi8_zdfufyogTLPOs0tIcImJIMasba5MqpHzOcKCsjnptUt2OF83Vyinw.NLZleTxnLwai5TN2wvcXhg'; + const baseURL = await setup({ + ...defaultConfig, + session: { rolling: false, absoluteDuration: day * 365 * 100 } + }); + const appSession = V1_COOKIE; + const cookieJar = toCookieJar({ appSession }, baseURL); + const session = await get(baseURL, '/session', { cookieJar }); + expect(session).toMatchObject({ + access_token: '__test_access_token__', + token_type: 'Bearer', + id_token: expect.any(String), + refresh_token: '__test_access_token__', + expires_at: expect.any(Number), + claims: { + nickname: '__test_nickname__', + sub: '__test_sub__', + iss: 'https://op.example.com/', + aud: '__test_client_id__', + iat: expect.any(Number), + exp: expect.any(Number), + nonce: '__test_nonce__' + } + }); }); }); diff --git a/tests/auth0-session/fixtures/cert.ts b/tests/auth0-session/fixtures/cert.ts index 2820d7f77..0fd225698 100644 --- a/tests/auth0-session/fixtures/cert.ts +++ b/tests/auth0-session/fixtures/cert.ts @@ -1,13 +1,19 @@ -import { JWK, JWKS, JWT } from 'jose'; +import * as jose from 'jose'; import { IdTokenClaims } from 'openid-client'; -const k = JWK.asKey({ +const publicKey = { e: 'AQAB', n: 'wQrThQ9HKf8ksCQEzqOu0ofF8DtLJgexeFSQBNnMQetACzt4TbHPpjhTWUIlD8bFCkyx88d2_QV3TewMtfS649Pn5hV6adeYW2TxweAA8HVJxskc' + 'qTSa_ktojQ-cD43HIStsbqJhHoFv0UY6z5pwJrVPT-yt38ciKo9Oc9IhEl6TSw-zAnuNW0zPOhKjuiIqpAk1lT3e6cYv83ahx82vpx3ZnV83dT9u' + 'RbIbcgIpK4W64YnYb5uDH7hGI8-4GnalZDfdApTu-9Y8lg_1v5ul-eQDsLCkUCPkqBaNiCG3gfZUAKp9rrFRE_cJTv_MJn-y_XSTMWILvTY7vdSM' + 'RMo4kQ', + kty: 'RSA', + use: 'sig', + alg: 'RS256' +}; + +const privateKey = { d: 'EMHY1K8b1VhxndyykiGBVoM0uoLbJiT60eA9VD53za0XNSJncg8iYGJ5UcE9KF5v0lIQDIJfIN2tmpUIEW96HbbSZZWtt6xgbGaZ2eOREU6NJfVl' + 'SIbpgXOYUs5tFKiRBZ8YXY448gX4Z-k5x7W3UJTimqSH_2nw3FLuU32FI2vtf4ToUKEcoUdrIqoAwZ1et19E7Q_NCG2y1nez0LpD8PKgfeX1OVHd' + @@ -28,17 +34,12 @@ const k = JWK.asKey({ qi: '8hAW25CmPjLAXpzkMpXpXsvJKdgql0Zjt-OeSVwzQN5dLYmu-Q98Xl5n8H-Nfr8aOmPfHBQ8M9FOMpxbgg8gbqixpkrxcTIGjpuH8RFYXj_0TYSB' + 'kCSOoc7tAP7YjOUOGJMqFHDYZVD-gmsCuRwWx3jKFxRrWLS5b8kWzkON0bM', - kty: 'RSA', - use: 'sig', - alg: 'RS256' -}); - -export const jwks = new JWKS.KeyStore([k]).toJWKS(false); + ...publicKey +}; -export const key = k.toPEM(true); -export const kid = k.kid; +export const jwks = { keys: [publicKey] }; -export const makeIdToken = (payload?: Partial): string => { +export const makeIdToken = async (payload?: Partial): Promise => { payload = Object.assign( { nickname: '__test_nickname__', @@ -52,8 +53,7 @@ export const makeIdToken = (payload?: Partial): string => { payload ); - return JWT.sign(payload, k.toPEM(true), { - algorithm: 'RS256', - header: { kid: k.kid } - }); + return new jose.SignJWT(payload as IdTokenClaims) + .setProtectedHeader({ alg: 'RS256' }) + .sign(await jose.importJWK(privateKey)); }; diff --git a/tests/auth0-session/fixtures/helpers.ts b/tests/auth0-session/fixtures/helpers.ts index 8eb01c9be..7f7443163 100644 --- a/tests/auth0-session/fixtures/helpers.ts +++ b/tests/auth0-session/fixtures/helpers.ts @@ -1,5 +1,4 @@ import { Cookie, CookieJar } from 'tough-cookie'; -import { JWK } from 'jose'; import { signing as deriveKey } from '../../../src/auth0-session/utils/hkdf'; import { generateCookieValue } from '../../../src/auth0-session/transient-store'; import { IncomingMessage, request as nodeHttpRequest } from 'http'; @@ -17,11 +16,11 @@ export const defaultConfig: Omit = { } }; -export const toSignedCookieJar = (cookies: { [key: string]: string }, url: string): CookieJar => { +export const toSignedCookieJar = async (cookies: { [key: string]: string }, url: string): Promise => { const cookieJar = new CookieJar(); - const jwk = JWK.asKey(deriveKey(secret)); + const signingKey = await deriveKey(secret); for (const [key, value] of Object.entries(cookies)) { - cookieJar.setCookieSync(`${key}=${generateCookieValue(key, value, jwk)}`, url); + cookieJar.setCookieSync(`${key}=${await generateCookieValue(key, value, signingKey)}`, url); } return cookieJar; }; diff --git a/tests/auth0-session/fixtures/server.ts b/tests/auth0-session/fixtures/server.ts index 665dffed8..1469bfcb5 100644 --- a/tests/auth0-session/fixtures/server.ts +++ b/tests/auth0-session/fixtures/server.ts @@ -4,7 +4,6 @@ import { createServer as createHttpsServer, Server as HttpsServer } from 'https' import url from 'url'; import nock from 'nock'; import { TokenSet, TokenSetParameters } from 'openid-client'; -import onHeaders from 'on-headers'; import bodyParser from 'body-parser'; import { loginHandler, @@ -29,21 +28,20 @@ import version from '../../../src/version'; export type SessionResponse = TokenSetParameters & { claims: Claims }; class TestSessionCache implements SessionCache { - public cache: WeakMap; - constructor() { - this.cache = new WeakMap(); + constructor(private cookieStore: CookieStore) {} + async create(req: IncomingMessage, res: ServerResponse, tokenSet: TokenSet): Promise { + await this.cookieStore.save(req, res, tokenSet); } - create(req: IncomingMessage, _res: ServerResponse, tokenSet: TokenSet): void { - this.cache.set(req, tokenSet); + async delete(req: IncomingMessage, res: ServerResponse): Promise { + await this.cookieStore.save(req, res, null); } - delete(req: IncomingMessage): void { - this.cache.delete(req); + async isAuthenticated(req: IncomingMessage): Promise { + const [session] = await this.cookieStore.read(req); + return !!session?.id_token; } - isAuthenticated(req: IncomingMessage): boolean { - return !!this.cache.get(req)?.id_token; - } - getIdToken(req: IncomingMessage): string | undefined { - return this.cache.get(req)?.id_token; + async getIdToken(req: IncomingMessage): Promise { + const [session] = await this.cookieStore.read(req); + return session?.id_token; } fromTokenSet(tokenSet: TokenSet): { [p: string]: any } { return tokenSet; @@ -62,31 +60,24 @@ const createHandlers = (params: ConfigParameters): Handlers => { const getClient = clientFactory(config, { name: 'nextjs-auth0', version }); const transientStore = new TransientStore(config); const cookieStore = new CookieStore(config); - const sessionCache = new TestSessionCache(); - - const applyCookies = (fn: Function) => (req: IncomingMessage, res: ServerResponse, ...args: []): any => { - if (!sessionCache.cache.has(req)) { - const [json, iat] = cookieStore.read(req); - sessionCache.cache.set(req, new TokenSet(json)); - onHeaders(res, () => cookieStore.save(req, res, sessionCache.cache.get(req), iat)); - } - return fn(req, res, ...args); - }; + const sessionCache = new TestSessionCache(cookieStore); return { - handleLogin: applyCookies(loginHandler(config, getClient, transientStore)), - handleLogout: applyCookies(logoutHandler(config, getClient, sessionCache)), - handleCallback: applyCookies(callbackHandler(config, getClient, sessionCache, transientStore)), - handleSession: applyCookies((req: IncomingMessage, res: ServerResponse) => { - if (!sessionCache.isAuthenticated(req)) { + handleLogin: loginHandler(config, getClient, transientStore), + handleLogout: logoutHandler(config, getClient, sessionCache), + handleCallback: callbackHandler(config, getClient, sessionCache, transientStore), + handleSession: async (req: IncomingMessage, res: ServerResponse) => { + const [json, iat] = await cookieStore.read(req); + if (!json?.id_token) { res.writeHead(401); res.end(); return; } - const session = sessionCache.cache.get(req); + const session = new TokenSet(json); + await cookieStore.save(req, res, session, iat); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ...session, claims: session?.claims() } as SessionResponse)); - }) + } }; }; @@ -102,36 +93,38 @@ const parseJson = (req: IncomingMessage, res: ServerResponse): Promise async (req: IncomingMessage, res: ServerResponse): Promise => { - const { pathname } = url.parse(req.url as string, true); - const parsedReq = await parseJson(req, res); - - try { - switch (pathname) { - case '/login': - return await handlers.handleLogin(parsedReq, res, loginOptions); - case '/logout': - return await handlers.handleLogout(parsedReq, res, logoutOptions); - case '/callback': - return await handlers.handleCallback(parsedReq, res, callbackOptions); - case '/session': - return await handlers.handleSession(parsedReq, res); - default: - res.writeHead(404); - res.end(); +const requestListener = + ( + handlers: Handlers, + { + callbackOptions, + loginOptions, + logoutOptions + }: { callbackOptions?: CallbackOptions; loginOptions?: LoginOptions; logoutOptions?: LogoutOptions } + ) => + async (req: IncomingMessage, res: ServerResponse): Promise => { + const { pathname } = url.parse(req.url as string, true); + const parsedReq = await parseJson(req, res); + + try { + switch (pathname) { + case '/login': + return await handlers.handleLogin(parsedReq, res, loginOptions); + case '/logout': + return await handlers.handleLogout(parsedReq, res, logoutOptions); + case '/callback': + return await handlers.handleCallback(parsedReq, res, callbackOptions); + case '/session': + return await handlers.handleSession(parsedReq, res); + default: + res.writeHead(404); + res.end(); + } + } catch (e) { + res.writeHead(e.statusCode || 500, e.message); + res.end(); } - } catch (e) { - res.writeHead(e.statusCode || 500, e.message); - res.end(); - } -}; + }; let server: HttpServer | HttpsServer; diff --git a/tests/auth0-session/handlers/callback.test.ts b/tests/auth0-session/handlers/callback.test.ts index ff90fef44..03929018c 100644 --- a/tests/auth0-session/handlers/callback.test.ts +++ b/tests/auth0-session/handlers/callback.test.ts @@ -1,6 +1,7 @@ import nock from 'nock'; import { CookieJar } from 'tough-cookie'; -import { JWT } from 'jose'; +import * as jose from 'jose'; +import { signing as deriveKey } from '../../../src/auth0-session/utils/hkdf'; import { encodeState } from '../../../src/auth0-session/hooks/get-login-state'; import { SessionResponse, setup, teardown } from '../fixtures/server'; import { makeIdToken } from '../fixtures/cert'; @@ -14,7 +15,7 @@ describe('callback', () => { it('should error when the body is empty', async () => { const baseURL = await setup(defaultConfig); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { nonce: '__test_nonce__', state: '__test_state__' @@ -44,7 +45,7 @@ describe('callback', () => { it("should error when state doesn't match", async () => { const baseURL = await setup(defaultConfig); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { nonce: '__valid_nonce__', state: '__valid_state__' @@ -66,7 +67,7 @@ describe('callback', () => { it("should error when id_token can't be parsed", async () => { const baseURL = await setup(defaultConfig); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { nonce: '__valid_nonce__', state: '__valid_state__' @@ -88,7 +89,7 @@ describe('callback', () => { it('should error when id_token has invalid alg', async () => { const baseURL = await setup(defaultConfig); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { nonce: '__valid_nonce__', state: '__valid_state__' @@ -100,9 +101,9 @@ describe('callback', () => { post(baseURL, '/callback', { body: { state: '__valid_state__', - id_token: JWT.sign({ sub: '__test_sub__' }, 'secret', { - algorithm: 'HS256' - }) + id_token: await new jose.SignJWT({ sub: '__test_sub__' }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(await deriveKey('secret')) }, cookieJar }) @@ -112,7 +113,7 @@ describe('callback', () => { it('should error when id_token is missing issuer', async () => { const baseURL = await setup(defaultConfig); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { nonce: '__valid_nonce__', state: '__valid_state__' @@ -124,7 +125,7 @@ describe('callback', () => { post(baseURL, '/callback', { body: { state: '__valid_state__', - id_token: makeIdToken({ iss: undefined }) + id_token: await makeIdToken({ iss: undefined }) }, cookieJar }) @@ -134,7 +135,7 @@ describe('callback', () => { it('should error when nonce is missing from cookies', async () => { const baseURL = await setup(defaultConfig); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: '__valid_state__' }, @@ -145,7 +146,7 @@ describe('callback', () => { post(baseURL, '/callback', { body: { state: '__valid_state__', - id_token: makeIdToken({ nonce: '__test_nonce__' }) + id_token: await makeIdToken({ nonce: '__test_nonce__' }) }, cookieJar }) @@ -155,7 +156,7 @@ describe('callback', () => { it('should error when legacy samesite fallback is off', async () => { const baseURL = await setup({ ...defaultConfig, legacySameSiteCookie: false }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { _state: '__valid_state__' }, @@ -166,7 +167,7 @@ describe('callback', () => { post(baseURL, '/callback', { body: { state: '__valid_state__', - id_token: makeIdToken() + id_token: await makeIdToken() }, cookieJar }) @@ -185,7 +186,7 @@ describe('callback', () => { auth_time: 10 }; - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: expectedDefaultState, nonce: '__test_nonce__', @@ -198,7 +199,7 @@ describe('callback', () => { post(baseURL, '/callback', { body: { state: expectedDefaultState, - id_token: makeIdToken(expected) + id_token: await makeIdToken(expected) }, cookieJar }) @@ -216,7 +217,7 @@ describe('callback', () => { nonce: '__test_nonce__' }; - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: expectedDefaultState, nonce: '__test_nonce__' @@ -227,7 +228,7 @@ describe('callback', () => { const { res } = await post(baseURL, '/callback', { body: { state: expectedDefaultState, - id_token: makeIdToken(expected) + id_token: await makeIdToken(expected) }, cookieJar, fullResponse: true @@ -250,7 +251,7 @@ describe('callback', () => { } }); - const idToken = makeIdToken({ + const idToken = await makeIdToken({ c_hash: '77QmUPtjPfzWtF2AnpK9RQ' }); @@ -264,7 +265,7 @@ describe('callback', () => { expires_in: 86400 })); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: expectedDefaultState, nonce: '__test_nonce__' @@ -294,7 +295,7 @@ describe('callback', () => { }); it('should use basic auth on token endpoint when using code flow', async () => { - const idToken = makeIdToken({ + const idToken = await makeIdToken({ c_hash: '77QmUPtjPfzWtF2AnpK9RQ' }); @@ -324,7 +325,7 @@ describe('callback', () => { }; }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: expectedDefaultState, nonce: '__test_nonce__' @@ -352,7 +353,7 @@ describe('callback', () => { const baseURL = await setup(defaultConfig); const state = encodeState({ foo: 'bar' }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: state, nonce: '__test_nonce__' @@ -363,7 +364,7 @@ describe('callback', () => { const { res } = await post(baseURL, '/callback', { body: { state: state, - id_token: makeIdToken() + id_token: await makeIdToken() }, cookieJar, fullResponse: true @@ -377,11 +378,11 @@ describe('callback', () => { const redirectUri = 'http://messi:3000/api/auth/callback/runtime'; const baseURL = await setup(defaultConfig, { callbackOptions: { redirectUri } }); const state = encodeState({ foo: 'bar' }); - const cookieJar = toSignedCookieJar({ state, nonce: '__test_nonce__' }, baseURL); + const cookieJar = await toSignedCookieJar({ state, nonce: '__test_nonce__' }, baseURL); const { res } = await post(baseURL, '/callback', { body: { state: state, - id_token: makeIdToken() + id_token: await makeIdToken() }, cookieJar, fullResponse: true diff --git a/tests/auth0-session/handlers/login.test.ts b/tests/auth0-session/handlers/login.test.ts index 6d2ed956b..c6d0fa6a7 100644 --- a/tests/auth0-session/handlers/login.test.ts +++ b/tests/auth0-session/handlers/login.test.ts @@ -34,7 +34,6 @@ describe('login', () => { }); expect(fromCookieJar(cookieJar, baseURL)).toMatchObject({ - appSession: expect.any(String), _state: parsed.query.state, _nonce: parsed.query.nonce }); @@ -72,7 +71,6 @@ describe('login', () => { }); expect(fromCookieJar(cookieJar, baseURL)).toMatchObject({ - appSession: expect.any(String), code_verifier: expect.any(String), state: parsed.query.state, nonce: parsed.query.nonce @@ -111,7 +109,6 @@ describe('login', () => { }); expect(fromCookieJar(cookieJar, baseURL)).toMatchObject({ - appSession: expect.any(String), _code_verifier: expect.any(String), _state: parsed.query.state, _nonce: parsed.query.nonce diff --git a/tests/auth0-session/handlers/logout.test.ts b/tests/auth0-session/handlers/logout.test.ts index 74fafc459..3aa2c5f80 100644 --- a/tests/auth0-session/handlers/logout.test.ts +++ b/tests/auth0-session/handlers/logout.test.ts @@ -8,11 +8,11 @@ import { encodeState } from '../../../src/auth0-session/hooks/get-login-state'; const login = async (baseURL: string): Promise => { const nonce = '__test_nonce__'; const state = encodeState({ returnTo: 'https://example.org' }); - const cookieJar = toSignedCookieJar({ state, nonce }, baseURL); + const cookieJar = await toSignedCookieJar({ state, nonce }, baseURL); await post(baseURL, '/callback', { body: { state, - id_token: makeIdToken({ nonce }) + id_token: await makeIdToken({ nonce }) }, cookieJar }); @@ -87,11 +87,11 @@ describe('logout route', () => { }); const nonce = '__test_nonce__'; const state = encodeState({ returnTo: 'https://example.org' }); - const cookieJar = toSignedCookieJar({ state, nonce }, baseURL); + const cookieJar = await toSignedCookieJar({ state, nonce }, baseURL); await post(baseURL, '/callback', { body: { state, - id_token: makeIdToken({ nonce, iss: 'https://test.eu.auth0.com/' }) + id_token: await makeIdToken({ nonce, iss: 'https://test.eu.auth0.com/' }) }, cookieJar }); diff --git a/tests/auth0-session/transient-store.test.ts b/tests/auth0-session/transient-store.test.ts index de0d8ea01..3863116b2 100644 --- a/tests/auth0-session/transient-store.test.ts +++ b/tests/auth0-session/transient-store.test.ts @@ -1,24 +1,24 @@ import { IncomingMessage, ServerResponse } from 'http'; -import { JWK, JWS } from 'jose'; +import * as jose from 'jose'; import { CookieJar } from 'tough-cookie'; -import { getConfig, TransientStore } from '../../src/auth0-session'; +import { getConfig, TransientStore } from '../../src/auth0-session/'; import { signing as deriveKey } from '../../src/auth0-session/utils/hkdf'; import { defaultConfig, fromCookieJar, get, getCookie, toSignedCookieJar } from './fixtures/helpers'; import { setup as createServer, teardown } from './fixtures/server'; -const generateSignature = (cookie: string, value: string): string => { - const key = JWK.asKey(deriveKey(defaultConfig.secret as string)); - return JWS.sign.flattened(Buffer.from(`${cookie}=${value}`), key, { - alg: 'HS256', - b64: false, - crit: ['b64'] - }).signature; +const generateSignature = async (cookie: string, value: string): Promise => { + const key = await deriveKey(defaultConfig.secret as string); + const { signature } = await new jose.FlattenedSign(new TextEncoder().encode(`${cookie}=${value}`)) + .setProtectedHeader({ alg: 'HS256', b64: false, crit: ['b64'] }) + .sign(key); + return signature; }; const setup = async (params = defaultConfig, cb: Function, https = true): Promise => createServer(params, { - customListener: (req, res) => { - res.end(JSON.stringify({ value: cb(req, res) })); + customListener: async (req, res) => { + const value = await cb(req, res); + res.end(JSON.stringify({ value })); }, https }); @@ -27,8 +27,10 @@ describe('TransientStore', () => { afterEach(teardown); it('should use the passed-in key to set the cookies', async () => { - const baseURL = await setup(defaultConfig, (req: IncomingMessage, res: ServerResponse) => - transientStore.save('test_key', req, res, { value: 'foo' }) + const baseURL = await setup( + defaultConfig, + async (req: IncomingMessage, res: ServerResponse) => + await transientStore.save('test_key', req, res, { value: 'foo' }) ); const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL })); const cookieJar = new CookieJar(); @@ -40,11 +42,11 @@ describe('TransientStore', () => { }); it('should accept list of secrets', async () => { - const baseURL = await setup( - { ...defaultConfig, secret: ['__old_secret__', defaultConfig.secret as string] }, - (req: IncomingMessage, res: ServerResponse) => transientStore.save('test_key', req, res, { value: 'foo' }) + const config = { ...defaultConfig, secret: ['__old_secret__', defaultConfig.secret as string] }; + const baseURL = await setup(config, (req: IncomingMessage, res: ServerResponse) => + transientStore.save('test_key', req, res, { value: 'foo' }) ); - const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL })); + const transientStore = new TransientStore(getConfig({ ...config, baseURL })); const cookieJar = new CookieJar(); const { value } = await get(baseURL, '/', { cookieJar }); const cookies = fromCookieJar(cookieJar, baseURL); @@ -157,10 +159,10 @@ describe('TransientStore', () => { transientStore.read('test_key', req, res) ); const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL })); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { - test_key: `foo.${generateSignature('test_key', 'foo')}`, - _test_key: `foo.${generateSignature('_test_key', 'foo')}` + test_key: `foo.${await generateSignature('test_key', 'foo')}`, + _test_key: `foo.${await generateSignature('_test_key', 'foo')}` }, baseURL ); @@ -177,9 +179,9 @@ describe('TransientStore', () => { transientStore.read('test_key', req, res) ); const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL })); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { - _test_key: `foo.${generateSignature('_test_key', 'foo')}` + _test_key: `foo.${await generateSignature('_test_key', 'foo')}` }, baseURL ); @@ -196,9 +198,9 @@ describe('TransientStore', () => { (req: IncomingMessage, res: ServerResponse) => transientStore.read('test_key', req, res) ); const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL, legacySameSiteCookie: false })); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { - _test_key: `foo.${generateSignature('_test_key', 'foo')}` + _test_key: `foo.${await generateSignature('_test_key', 'foo')}` }, baseURL ); @@ -213,7 +215,7 @@ describe('TransientStore', () => { transientStore.read('test_key', req, res) ); const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL })); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { test_key: 'foo.bar', _test_key: 'foo.bar' diff --git a/tests/fixtures/frontend.tsx b/tests/fixtures/frontend.tsx index f10ff8da3..4c9a96b4d 100644 --- a/tests/fixtures/frontend.tsx +++ b/tests/fixtures/frontend.tsx @@ -1,7 +1,13 @@ import React from 'react'; -import { UserProvider, UserProviderProps, UserProfile } from '../../src'; -import { ConfigProvider, ConfigProviderProps, RequestError } from '../../src/frontend'; +import { + ConfigProvider, + ConfigProviderProps, + RequestError, + UserProvider, + UserProviderProps, + UserProfile +} from '../../src/frontend'; type FetchUserMock = { ok: boolean; diff --git a/tests/fixtures/oidc-nocks.ts b/tests/fixtures/oidc-nocks.ts index 2b19cf96d..d8c445854 100644 --- a/tests/fixtures/oidc-nocks.ts +++ b/tests/fixtures/oidc-nocks.ts @@ -97,13 +97,13 @@ export function codeExchange(params: ConfigParameters, idToken: string, code = ' }); } -export function refreshTokenExchange( +export async function refreshTokenExchange( params: ConfigParameters, refreshToken: string, payload: Record, newToken?: string -): nock.Scope { - const idToken = makeIdToken({ +): Promise { + const idToken = await makeIdToken({ iss: `${params.issuerBaseURL}/`, aud: params.clientID, ...payload @@ -120,14 +120,14 @@ export function refreshTokenExchange( }); } -export function refreshTokenRotationExchange( +export async function refreshTokenRotationExchange( params: ConfigParameters, refreshToken: string, payload: Record, newToken?: string, newrefreshToken?: string -): nock.Scope { - const idToken = makeIdToken({ +): Promise { + const idToken = await makeIdToken({ iss: `${params.issuerBaseURL}/`, aud: params.clientID, ...payload diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts index eca230423..8c1635751 100644 --- a/tests/fixtures/setup.ts +++ b/tests/fixtures/setup.ts @@ -51,7 +51,7 @@ export const setup = async ( ): Promise => { discovery(config, discoveryOptions); jwksEndpoint(config, jwks); - codeExchange(config, makeIdToken({ iss: 'https://acme.auth0.local/', ...idTokenClaims })); + codeExchange(config, await makeIdToken({ iss: 'https://acme.auth0.local/', ...idTokenClaims })); userInfo(config, userInfoToken, userInfoPayload); const { handleAuth, @@ -126,7 +126,7 @@ export const teardown = async (): Promise => { export const login = async (baseUrl: string): Promise => { const nonce = '__test_nonce__'; const state = encodeState({ returnTo: '/' }); - const cookieJar = toSignedCookieJar({ state, nonce }, baseUrl); + const cookieJar = await toSignedCookieJar({ state, nonce }, baseUrl); await post(baseUrl, '/api/auth/callback', { fullResponse: true, body: { diff --git a/tests/fixtures/test-app/pages/api/session.ts b/tests/fixtures/test-app/pages/api/session.ts index d356905af..e451f9e84 100644 --- a/tests/fixtures/test-app/pages/api/session.ts +++ b/tests/fixtures/test-app/pages/api/session.ts @@ -1,6 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; -export default function sessionHandler(req: NextApiRequest, res: NextApiResponse): void { - const json = (global as any).getSession(req, res); +export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse) { + const json = await (global as any).getSession(req, res); res.status(200).json(json); } diff --git a/tests/fixtures/test-app/pages/api/update-user.ts b/tests/fixtures/test-app/pages/api/update-user.ts index a33bf15a0..ce8f82ac2 100644 --- a/tests/fixtures/test-app/pages/api/update-user.ts +++ b/tests/fixtures/test-app/pages/api/update-user.ts @@ -1,8 +1,8 @@ import { NextApiRequest, NextApiResponse } from 'next'; -export default function sessionHandler(req: NextApiRequest, res: NextApiResponse): void { - const session = (global as any).getSession(req, res); +export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse) { + const session = await (global as any).getSession(req, res); const updated = { ...session?.user, ...req.body?.user }; - (global as any).updateUser(req, res, updated); + await (global as any).updateUser(req, res, updated); res.status(200).json(updated); } diff --git a/tests/handlers/callback.test.ts b/tests/handlers/callback.test.ts index 2d2df3926..37f48370d 100644 --- a/tests/handlers/callback.test.ts +++ b/tests/handlers/callback.test.ts @@ -29,7 +29,7 @@ describe('callback handler', () => { test('should validate the state', async () => { const baseUrl = await setup(withoutApi); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state: '__other_state__' }, @@ -49,7 +49,7 @@ describe('callback handler', () => { test('should validate the audience', async () => { const baseUrl = await setup(withoutApi, { idTokenClaims: { aud: 'bar' } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -71,7 +71,7 @@ describe('callback handler', () => { test('should validate the issuer', async () => { const baseUrl = await setup(withoutApi, { idTokenClaims: { aud: 'bar', iss: 'other-issuer' } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -100,7 +100,7 @@ describe('callback handler', () => { test('should create the session without OIDC claims', async () => { const baseUrl = await setup(withoutApi); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -117,7 +117,6 @@ describe('callback handler', () => { ); expect(res.statusCode).toBe(302); const body = await get(baseUrl, `/api/session`, { cookieJar }); - expect(body.user).toStrictEqual({ nickname: '__test_nickname__', sub: '__test_sub__' @@ -128,7 +127,7 @@ describe('callback handler', () => { timekeeper.freeze(0); const baseUrl = await setup(withoutApi); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -155,7 +154,7 @@ describe('callback handler', () => { timekeeper.freeze(0); const baseUrl = await setup(withApi); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -196,7 +195,7 @@ describe('callback handler', () => { }; const baseUrl = await setup(withApi, { callbackOptions: { afterCallback } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -234,7 +233,7 @@ describe('callback handler', () => { }; const baseUrl = await setup(withApi, { callbackOptions: { afterCallback } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -268,7 +267,7 @@ describe('callback handler', () => { }; const baseUrl = await setup(withApi, { callbackOptions: { afterCallback } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -290,7 +289,7 @@ describe('callback handler', () => { test('throws for missing org_id claim', async () => { const baseUrl = await setup({ ...withApi, organization: 'foo' }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -312,7 +311,7 @@ describe('callback handler', () => { test('throws for org_id claim mismatch', async () => { const baseUrl = await setup({ ...withApi, organization: 'foo' }, { idTokenClaims: { org_id: 'bar' } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -339,7 +338,7 @@ describe('callback handler', () => { callbackOptions: { organization: 'foo' } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -368,7 +367,7 @@ describe('callback handler', () => { } }); const state = encodeState({ returnTo: baseUrl }); - const cookieJar = toSignedCookieJar( + const cookieJar = await toSignedCookieJar( { state, nonce: '__test_nonce__' @@ -379,14 +378,14 @@ describe('callback handler', () => { nock(`${withoutApi.issuerBaseURL}`) .post('/oauth/token', /grant_type=authorization_code/) - .reply(200, (_, body) => { + .reply(200, async (_, body) => { spy(body); return { access_token: 'eyJz93a...k4laUWw', expires_in: 750, scope: 'read:foo delete:foo', refresh_token: 'GEbRxBN...edjnXbL', - id_token: makeIdToken({ iss: `${withoutApi.issuerBaseURL}/` }), + id_token: await makeIdToken({ iss: `${withoutApi.issuerBaseURL}/` }), token_type: 'Bearer' }; }); diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts index 8813b3d64..4a980097e 100644 --- a/tests/handlers/logout.test.ts +++ b/tests/handlers/logout.test.ts @@ -1,6 +1,7 @@ import { parse } from 'cookie'; import { parse as parseUrl, URL } from 'url'; import { withoutApi } from '../fixtures/default-settings'; +import { get } from '../auth0-session/fixtures/helpers'; import { setup, teardown, login } from '../fixtures/setup'; import { IncomingMessage } from 'http'; @@ -20,15 +21,15 @@ describe('logout handler', () => { const baseUrl = await setup(withoutApi); const cookieJar = await login(baseUrl); - const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, { - redirect: 'manual', - headers: { - cookie: cookieJar.getCookieStringSync(baseUrl) - } + const { + res: { statusCode, headers } + } = await get(baseUrl, '/api/auth/logout', { + cookieJar, + fullResponse: true }); - expect(status).toBe(302); - expect(parseUrl(headers.get('location') as string, true)).toMatchObject({ + expect(statusCode).toBe(302); + expect(parseUrl(headers['location'], true)).toMatchObject({ protocol: 'https:', host: 'acme.auth0.local', query: { @@ -46,15 +47,15 @@ describe('logout handler', () => { }); const cookieJar = await login(baseUrl); - const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, { - redirect: 'manual', - headers: { - cookie: cookieJar.getCookieStringSync(baseUrl) - } + const { + res: { statusCode, headers } + } = await get(baseUrl, '/api/auth/logout', { + cookieJar, + fullResponse: true }); - expect(status).toBe(302); - expect(parseUrl(headers.get('location') as string, true).query).toMatchObject({ + expect(statusCode).toBe(302); + expect(parseUrl(headers['location'], true).query).toMatchObject({ returnTo: 'https://www.foo.bar' }); }); @@ -65,15 +66,15 @@ describe('logout handler', () => { }); const cookieJar = await login(baseUrl); - const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, { - redirect: 'manual', - headers: { - cookie: cookieJar.getCookieStringSync(baseUrl) - } + const { + res: { statusCode, headers } + } = await get(baseUrl, '/api/auth/logout', { + cookieJar, + fullResponse: true }); - expect(status).toBe(302); - expect(parseUrl(headers.get('location') as string)).toMatchObject({ + expect(statusCode).toBe(302); + expect(parseUrl(headers['location'])).toMatchObject({ host: 'my-end-session-endpoint', pathname: '/logout' }); @@ -85,14 +86,14 @@ describe('logout handler', () => { }); const cookieJar = await login(baseUrl); - const res = await fetch(`${baseUrl}/api/auth/logout`, { - redirect: 'manual', - headers: { - cookie: cookieJar.getCookieStringSync(baseUrl) - } + const { + res: { headers } + } = await get(baseUrl, '/api/auth/logout', { + cookieJar, + fullResponse: true }); - expect(parse(res.headers.get('set-cookie') as string)).toMatchObject({ + expect(parse(headers['set-cookie'][0])).toMatchObject({ appSession: '', 'Max-Age': '0', Path: '/' diff --git a/tests/handlers/profile.test.ts b/tests/handlers/profile.test.ts index 7f15cf326..504951a69 100644 --- a/tests/handlers/profile.test.ts +++ b/tests/handlers/profile.test.ts @@ -92,7 +92,7 @@ describe('profile handler', () => { nock(`${withoutApi.issuerBaseURL}`) .post('/oauth/token', `grant_type=refresh_token&refresh_token=GEbRxBN...edjnXbL`) .reply(200, { - id_token: makeIdToken({ iss: 'https://acme.auth0.local/' }), + id_token: await makeIdToken({ iss: 'https://acme.auth0.local/' }), token_type: 'Bearer', expires_in: 750, scope: 'read:foo write:foo' @@ -115,7 +115,7 @@ describe('profile handler', () => { }, userInfoToken: 'new-access-token' }); - refreshTokenRotationExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-access-token', 'new-refresh-token'); + await refreshTokenRotationExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-access-token', 'new-refresh-token'); const cookieJar = await login(baseUrl); const profile = await get(baseUrl, '/api/auth/me', { cookieJar }); expect(profile).toMatchObject({ foo: 'bar' }); diff --git a/tests/session/cache.test.ts b/tests/session/cache.test.ts index 8efb9bafe..bccc3f7cb 100644 --- a/tests/session/cache.test.ts +++ b/tests/session/cache.test.ts @@ -31,54 +31,54 @@ describe('SessionCache', () => { expect(cache).toBeInstanceOf(SessionCache); }); - test('should create the session entry', () => { - cache.create(req, res, session); - expect(cache.get(req, res)).toEqual(session); + test('should create the session entry', async () => { + await cache.create(req, res, session); + expect(await cache.get(req, res)).toEqual(session); expect(cookieStore.save).toHaveBeenCalledWith(req, res, session, undefined); }); - test('should delete the session entry', () => { - cache.create(req, res, session); - expect(cache.get(req, res)).toEqual(session); - cache.delete(req, res); - expect(cache.get(req, res)).toBeNull(); + test('should delete the session entry', async () => { + await cache.create(req, res, session); + expect(await cache.get(req, res)).toEqual(session); + await cache.delete(req, res); + expect(await cache.get(req, res)).toBeNull(); }); - test('should set authenticated for authenticated user', () => { - cache.create(req, res, session); - expect(cache.isAuthenticated(req, res)).toEqual(true); + test('should set authenticated for authenticated user', async () => { + await cache.create(req, res, session); + expect(await cache.isAuthenticated(req, res)).toEqual(true); }); - test('should set unauthenticated for anonymous user', () => { - expect(cache.isAuthenticated(req, res)).toEqual(false); + test('should set unauthenticated for anonymous user', async () => { + expect(await cache.isAuthenticated(req, res)).toEqual(false); }); - test('should get an id token for authenticated user', () => { - cache.create(req, res, session); - expect(cache.getIdToken(req, res)).toEqual('__test_id_token__'); + test('should get an id token for authenticated user', async () => { + await cache.create(req, res, session); + expect(await cache.getIdToken(req, res)).toEqual('__test_id_token__'); }); - test('should get no id token for anonymous user', () => { - expect(cache.getIdToken(req, res)).toBeUndefined(); + test('should get no id token for anonymous user', async () => { + expect(await cache.getIdToken(req, res)).toBeUndefined(); }); - test('should save the session on read and update with a rolling session', () => { - cookieStore.read = jest.fn().mockReturnValue([{ user: { sub: '__test_user__' } }, 500]); - expect(cache.isAuthenticated(req, res)).toEqual(true); - expect(cache.get(req, res)?.user).toEqual({ sub: '__test_user__' }); - cache.set(req, res, new Session({ sub: '__new_user__' })); - expect(cache.get(req, res)?.user).toEqual({ sub: '__new_user__' }); + test('should save the session on read and update with a rolling session', async () => { + cookieStore.read = jest.fn().mockResolvedValue([{ user: { sub: '__test_user__' } }, 500]); + expect(await cache.isAuthenticated(req, res)).toEqual(true); + expect((await cache.get(req, res))?.user).toEqual({ sub: '__test_user__' }); + await cache.set(req, res, new Session({ sub: '__new_user__' })); + expect((await cache.get(req, res))?.user).toEqual({ sub: '__new_user__' }); expect(cookieStore.read).toHaveBeenCalledTimes(1); expect(cookieStore.save).toHaveBeenCalledTimes(2); }); - test('should save the session only on update without a rolling session', () => { + test('should save the session only on update without a rolling session', async () => { setup({ ...withoutApi, session: { rolling: false } }); - cookieStore.read = jest.fn().mockReturnValue([{ user: { sub: '__test_user__' } }, 500]); - expect(cache.isAuthenticated(req, res)).toEqual(true); - expect(cache.get(req, res)?.user).toEqual({ sub: '__test_user__' }); + cookieStore.read = jest.fn().mockResolvedValue([{ user: { sub: '__test_user__' } }, 500]); + expect(await cache.isAuthenticated(req, res)).toEqual(true); + expect((await cache.get(req, res))?.user).toEqual({ sub: '__test_user__' }); cache.set(req, res, new Session({ sub: '__new_user__' })); - expect(cache.get(req, res)?.user).toEqual({ sub: '__new_user__' }); + expect((await cache.get(req, res))?.user).toEqual({ sub: '__new_user__' }); expect(cookieStore.read).toHaveBeenCalledTimes(1); expect(cookieStore.save).toHaveBeenCalledTimes(1); }); diff --git a/tests/session/get-access-token.test.ts b/tests/session/get-access-token.test.ts index d91d6bea9..819309b78 100644 --- a/tests/session/get-access-token.test.ts +++ b/tests/session/get-access-token.test.ts @@ -122,7 +122,7 @@ describe('get access token', () => { }); test('should retrieve a new access token if the old one is expired and update the profile', async () => { - refreshTokenExchange( + await refreshTokenExchange( withApi, 'GEbRxBN...edjnXbL', { @@ -148,7 +148,7 @@ describe('get access token', () => { }); test('should retrieve a new access token if force refresh is set', async () => { - refreshTokenExchange( + await refreshTokenExchange( withApi, 'GEbRxBN...edjnXbL', { @@ -167,7 +167,7 @@ describe('get access token', () => { }); test('should retrieve a new access token and rotate the refresh token', async () => { - refreshTokenRotationExchange( + await refreshTokenRotationExchange( withApi, 'GEbRxBN...edjnXbL', { @@ -194,7 +194,7 @@ describe('get access token', () => { }); test('should not overwrite custom session properties when applying a new access token', async () => { - refreshTokenExchange( + await refreshTokenExchange( withApi, 'GEbRxBN...edjnXbL', { @@ -233,7 +233,7 @@ describe('get access token', () => { }); test('should retrieve a new access token and update the session based on afterRefresh', async () => { - refreshTokenExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-token'); + await refreshTokenExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-token'); const baseUrl = await setup(withApi, { getAccessTokenOptions: { refresh: true, @@ -255,7 +255,7 @@ describe('get access token', () => { }); test('should pass custom auth params in refresh grant request body', async () => { - const idToken = makeIdToken({ + const idToken = await makeIdToken({ iss: `${withApi.issuerBaseURL}/`, aud: withApi.clientID, email: 'john@test.com', diff --git a/tests/session/session.test.ts b/tests/session/session.test.ts index 4396c7e2f..49121cca6 100644 --- a/tests/session/session.test.ts +++ b/tests/session/session.test.ts @@ -9,9 +9,9 @@ describe('session', () => { }); describe('from tokenSet', () => { - test('should construct a session from a tokenSet', () => { + test('should construct a session from a tokenSet', async () => { expect( - fromTokenSet(new TokenSet({ id_token: makeIdToken({ foo: 'bar', bax: 'qux' }) }), { + fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar', bax: 'qux' }) }), { identityClaimFilter: ['baz'], routes: { login: '', callback: '', postLogoutRedirect: '' } }).user @@ -28,18 +28,18 @@ describe('session', () => { }); }); - test('should not store the ID Token by default', () => { + test('should not store the ID Token by default', async () => { expect( - fromTokenSet(new TokenSet({ id_token: makeIdToken({ foo: 'bar' }) }), { + fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), { identityClaimFilter: ['baz'], routes: { login: '', callback: '', postLogoutRedirect: '' } }).idToken ).toBeUndefined(); }); - test('should store the ID Token', () => { + test('should store the ID Token', async () => { expect( - fromTokenSet(new TokenSet({ id_token: makeIdToken({ foo: 'bar' }) }), { + fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), { session: { storeIDToken: true, name: '', diff --git a/tests/session/update-user.test.ts b/tests/session/update-user.test.ts index e659a543b..1441129a6 100644 --- a/tests/session/update-user.test.ts +++ b/tests/session/update-user.test.ts @@ -1,6 +1,7 @@ import { login, setup, teardown } from '../fixtures/setup'; import { withoutApi } from '../fixtures/default-settings'; import { get, post } from '../auth0-session/fixtures/helpers'; +import { CookieJar } from 'tough-cookie'; describe('update-user', () => { afterEach(teardown); @@ -27,8 +28,9 @@ describe('update-user', () => { test('should ignore updates if user is not logged in', async () => { const baseUrl = await setup(withoutApi); - await expect(get(baseUrl, '/api/auth/me')).resolves.toBe(''); - await post(baseUrl, '/api/update-user', { body: { user: { sub: 'foo' } } }); - await expect(get(baseUrl, '/api/auth/me')).resolves.toBe(''); + const cookieJar = new CookieJar(); + await expect(get(baseUrl, '/api/auth/me', { cookieJar })).resolves.toBe(''); + await post(baseUrl, '/api/update-user', { body: { user: { sub: 'foo' } }, cookieJar }); + await expect(get(baseUrl, '/api/auth/me', { cookieJar })).resolves.toBe(''); }); });