diff --git a/src/auth0-session/config.ts b/src/auth0-session/config.ts index 1963d6125..9334bad76 100644 --- a/src/auth0-session/config.ts +++ b/src/auth0-session/config.ts @@ -188,6 +188,14 @@ export interface Config { * You can also use the `AUTH0_CLIENT_ASSERTION_SIGNING_ALG` environment variable. */ clientAssertionSigningAlg?: string; + + /** + * By default, the transaction cookie takes the same settings as the + * session cookie. But you may want to configure the session cookie to be more + * secure in a way that would break the OAuth flow's usage of the transaction + * cookie (Setting SameSite=Strict for example). + */ + transactionCookie: Omit & { name: string }; } /** diff --git a/src/auth0-session/get-config.ts b/src/auth0-session/get-config.ts index 16e9f19b0..1adc9e79c 100644 --- a/src/auth0-session/get-config.ts +++ b/src/auth0-session/get-config.ts @@ -166,7 +166,16 @@ const paramsSchema = Joi.object({ }), clientAssertionSigningAlg: Joi.string() .optional() - .valid('RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES256K', 'ES384', 'ES512', 'EdDSA') + .valid('RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES256K', 'ES384', 'ES512', 'EdDSA'), + transactionCookie: Joi.object({ + name: Joi.string().default('auth_verification'), + domain: Joi.string().default(Joi.ref('/session.cookie.domain')), + secure: Joi.boolean().default(Joi.ref('/session.cookie.secure')), + sameSite: Joi.string().valid('lax', 'strict', 'none').default(Joi.ref('/session.cookie.sameSite')), + path: Joi.string().uri({ relativeOnly: true }).default(Joi.ref('/session.cookie.transient')) + }) + .default() + .unknown(false) }); export type DeepPartial = { diff --git a/src/auth0-session/handlers/callback.ts b/src/auth0-session/handlers/callback.ts index efa4c85b1..6a094593c 100644 --- a/src/auth0-session/handlers/callback.ts +++ b/src/auth0-session/handlers/callback.ts @@ -36,7 +36,7 @@ export default function callbackHandlerFactory( let tokenResponse; let authVerification: AuthVerification; - const cookie = await transientCookieHandler.read('auth_verification', req, res); + const cookie = await transientCookieHandler.read(config.transactionCookie.name, req, res); if (!cookie) { throw new MissingStateCookieError(); diff --git a/src/auth0-session/handlers/login.ts b/src/auth0-session/handlers/login.ts index c8ea37ac5..bda12462f 100644 --- a/src/auth0-session/handlers/login.ts +++ b/src/auth0-session/handlers/login.ts @@ -86,8 +86,8 @@ export default function loginHandlerFactory( authVerification.response_type = responseType; } - await transientHandler.save('auth_verification', req, res, { - sameSite: authParams.response_mode === 'form_post' ? 'none' : config.session.cookie.sameSite, + await transientHandler.save(config.transactionCookie.name, req, res, { + sameSite: authParams.response_mode === 'form_post' ? 'none' : config.transactionCookie.sameSite, value: JSON.stringify(authVerification) }); diff --git a/src/auth0-session/transient-store.ts b/src/auth0-session/transient-store.ts index 0a70a327a..ee89457b2 100644 --- a/src/auth0-session/transient-store.ts +++ b/src/auth0-session/transient-store.ts @@ -41,7 +41,7 @@ export default class TransientStore { { sameSite = 'none', value }: StoreOptions ): Promise { const isSameSiteNone = sameSite === 'none'; - const { domain, path, secure } = this.config.session.cookie; + const { domain, path, secure } = this.config.transactionCookie; const basicAttr = { httpOnly: true, secure, @@ -81,7 +81,7 @@ export default class TransientStore { async read(key: string, req: Auth0Request, res: Auth0Response): Promise { const cookies = req.getCookies(); const cookie = cookies[key]; - const cookieConfig = this.config.session.cookie; + const cookieConfig = this.config.transactionCookie; const verifyingKeys = await this.getKeys(); let value = await getCookieValue(key, cookie, verifyingKeys); diff --git a/src/config.ts b/src/config.ts index 4dd659b69..7d0eaf648 100644 --- a/src/config.ts +++ b/src/config.ts @@ -192,6 +192,21 @@ export interface BaseConfig { * You can also use the `AUTH0_CLIENT_ASSERTION_SIGNING_ALG` environment variable. */ clientAssertionSigningAlg?: string; + + /** + * By default, the transaction cookie takes the same settings as the + * session cookie. But you may want to configure the session cookie to be more + * secure in a way that would break the OAuth flow's usage of the transaction + * cookie (Setting SameSite=Strict for example). + * + * You can also use: + * `AUTH0_TRANSACTION_COOKIE_NAME` + * `AUTH0_TRANSACTION_COOKIE_DOMAIN` + * `AUTH0_TRANSACTION_COOKIE_PATH` + * `AUTH0_TRANSACTION_COOKIE_SAME_SITE` + * `AUTH0_TRANSACTION_COOKIE_SECURE` + */ + transactionCookie: Omit & { name: string }; } /** @@ -417,6 +432,11 @@ export interface NextConfig extends Pick { * - `AUTH0_COOKIE_SAME_SITE`: See {@link CookieConfig.sameSite}. * - `AUTH0_CLIENT_ASSERTION_SIGNING_KEY`: See {@link BaseConfig.clientAssertionSigningKey} * - `AUTH0_CLIENT_ASSERTION_SIGNING_ALG`: See {@link BaseConfig.clientAssertionSigningAlg} + * - `AUTH0_TRANSACTION_COOKIE_NAME` See {@link BaseConfig.transactionCookie} + * - `AUTH0_TRANSACTION_COOKIE_DOMAIN` See {@link BaseConfig.transactionCookie} + * - `AUTH0_TRANSACTION_COOKIE_PATH` See {@link BaseConfig.transactionCookie} + * - `AUTH0_TRANSACTION_COOKIE_SAME_SITE` See {@link BaseConfig.transactionCookie} + * - `AUTH0_TRANSACTION_COOKIE_SECURE` See {@link BaseConfig.transactionCookie} * * ### 2. Create your own instance using {@link InitAuth0} * @@ -519,6 +539,11 @@ export const getConfig = (params: ConfigParameters = {}): { baseConfig: BaseConf const AUTH0_COOKIE_SAME_SITE = process.env.AUTH0_COOKIE_SAME_SITE; const AUTH0_CLIENT_ASSERTION_SIGNING_KEY = process.env.AUTH0_CLIENT_ASSERTION_SIGNING_KEY; const AUTH0_CLIENT_ASSERTION_SIGNING_ALG = process.env.AUTH0_CLIENT_ASSERTION_SIGNING_ALG; + const AUTH0_TRANSACTION_COOKIE_NAME = process.env.AUTH0_TRANSACTION_COOKIE_NAME; + const AUTH0_TRANSACTION_COOKIE_DOMAIN = process.env.AUTH0_TRANSACTION_COOKIE_DOMAIN; + const AUTH0_TRANSACTION_COOKIE_PATH = process.env.AUTH0_TRANSACTION_COOKIE_PATH; + const AUTH0_TRANSACTION_COOKIE_SAME_SITE = process.env.AUTH0_TRANSACTION_COOKIE_SAME_SITE; + const AUTH0_TRANSACTION_COOKIE_SECURE = process.env.AUTH0_TRANSACTION_COOKIE_SECURE; const baseURL = AUTH0_BASE_URL && !/^https?:\/\//.test(AUTH0_BASE_URL as string) ? `https://${AUTH0_BASE_URL}` : AUTH0_BASE_URL; @@ -575,7 +600,15 @@ export const getConfig = (params: ConfigParameters = {}): { baseConfig: BaseConf postLogoutRedirect: baseParams.routes?.postLogoutRedirect || AUTH0_POST_LOGOUT_REDIRECT }, clientAssertionSigningKey: AUTH0_CLIENT_ASSERTION_SIGNING_KEY, - clientAssertionSigningAlg: AUTH0_CLIENT_ASSERTION_SIGNING_ALG + clientAssertionSigningAlg: AUTH0_CLIENT_ASSERTION_SIGNING_ALG, + transactionCookie: { + name: AUTH0_TRANSACTION_COOKIE_NAME, + domain: AUTH0_TRANSACTION_COOKIE_DOMAIN, + path: AUTH0_TRANSACTION_COOKIE_PATH || '/', + secure: bool(AUTH0_TRANSACTION_COOKIE_SECURE), + sameSite: AUTH0_TRANSACTION_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none' | undefined, + ...baseParams.transactionCookie + } }); const nextConfig: NextConfig = { diff --git a/tests/auth0-session/handlers/callback.test.ts b/tests/auth0-session/handlers/callback.test.ts index 26dcc302c..332eaeebf 100644 --- a/tests/auth0-session/handlers/callback.test.ts +++ b/tests/auth0-session/handlers/callback.test.ts @@ -636,4 +636,62 @@ describe('callback', () => { 'Discovery requests failing for https://op2.example.com, expected 200 OK, got: 500 Internal Server Error' ); }); + + it('should use custom transaction cookie name', async () => { + const idToken = await makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ' + }); + + const baseURL = await setup({ + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access' + }, + transactionCookie: { name: 'foo_bar' } + }); + + let credentials = ''; + let body = ''; + nock('https://op.example.com') + .post('/oauth/token') + .reply(200, function (_uri, requestBody) { + credentials = this.req.headers.authorization.replace('Basic ', ''); + body = requestBody as string; + return { + access_token: '__test_access_token__', + refresh_token: '__test_refresh_token__', + id_token: idToken, + token_type: 'Bearer', + expires_in: 86400 + }; + }); + + const cookieJar = await toSignedCookieJar( + { + foo_bar: JSON.stringify({ + state: expectedDefaultState, + nonce: '__test_nonce__' + }) + }, + baseURL + ); + + const code = 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y'; + await post(baseURL, '/callback', { + body: { + state: expectedDefaultState, + id_token: idToken, + code + }, + cookieJar + }); + + expect(Buffer.from(credentials, 'base64').toString()).toEqual('__test_client_id__:__test_client_secret__'); + expect(body).toEqual( + `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(baseURL)}%2Fcallback` + ); + }); }); diff --git a/tests/auth0-session/handlers/login.test.ts b/tests/auth0-session/handlers/login.test.ts index 8aeda3c94..6b01a47d8 100644 --- a/tests/auth0-session/handlers/login.test.ts +++ b/tests/auth0-session/handlers/login.test.ts @@ -260,4 +260,29 @@ describe('login', () => { expect(cookie?.sameSite).toEqual('none'); expect(cookie?.secure).toBeTruthy(); }); + + it('transient cookie should honor transaction cookie config in code flow', async () => { + const baseURL = await setup( + { + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code' + }, + transactionCookie: { + name: 'foo_bar', + sameSite: 'none' + } + }, + { https: true } + ); + const cookieJar = new CookieJar(); + + const { res } = await get(baseURL, '/login', { fullResponse: true, cookieJar }); + expect(res.statusCode).toEqual(302); + + const cookie = getCookie('foo_bar', cookieJar, baseURL); + expect(cookie?.sameSite).toEqual('none'); + expect(cookie?.secure).toBeTruthy(); + }); }); diff --git a/tests/config.test.ts b/tests/config.test.ts index 041cdac31..4022bde67 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -80,7 +80,14 @@ describe('config params', () => { 'at_hash', 'c_hash' ], - clientAuthMethod: 'client_secret_basic' + clientAuthMethod: 'client_secret_basic', + transactionCookie: { + name: 'auth_verification', + domain: undefined, + path: '/', + sameSite: 'lax', + secure: true + } }); expect(nextConfig).toStrictEqual({ identityClaimFilter: [