Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to override transaction cookie name and config #1346

Merged
merged 3 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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