Skip to content

Commit

Permalink
Add option to override transaction cookie name and config (#1346)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamjmcgrath committed Aug 7, 2023
2 parents 75c41b6 + 8e049ec commit dd4b586
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 8 deletions.
8 changes: 8 additions & 0 deletions src/auth0-session/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CookieConfig, 'transient' | 'httpOnly'> & { name: string };
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/auth0-session/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = {
Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/handlers/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/auth0-session/handlers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});

Expand Down
4 changes: 2 additions & 2 deletions src/auth0-session/transient-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class TransientStore {
{ sameSite = 'none', value }: StoreOptions
): Promise<string> {
const isSameSiteNone = sameSite === 'none';
const { domain, path, secure } = this.config.session.cookie;
const { domain, path, secure } = this.config.transactionCookie;
const basicAttr = {
httpOnly: true,
secure,
Expand Down Expand Up @@ -81,7 +81,7 @@ export default class TransientStore {
async read(key: string, req: Auth0Request, res: Auth0Response): Promise<string | undefined> {
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);
Expand Down
35 changes: 34 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CookieConfig, 'transient' | 'httpOnly'> & { name: string };
}

/**
Expand Down Expand Up @@ -417,6 +432,11 @@ export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> {
* - `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}
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down
58 changes: 58 additions & 0 deletions tests/auth0-session/handlers/callback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
);
});
});
25 changes: 25 additions & 0 deletions tests/auth0-session/handlers/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
9 changes: 8 additions & 1 deletion tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down

0 comments on commit dd4b586

Please sign in to comment.