Skip to content

Commit

Permalink
Move state cookies to under a single cookie (#1343)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamjmcgrath committed Aug 4, 2023
2 parents cd393ee + 3566e65 commit 156a1a8
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 293 deletions.
28 changes: 20 additions & 8 deletions src/auth0-session/handlers/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { AuthorizationParameters, Config } from '../config';
import TransientStore from '../transient-store';
import { decodeState } from '../utils/encoding';
import { SessionCache } from '../session-cache';
import { MissingStateCookieError, MissingStateParamError } from '../utils/errors';
import { MalformedStateCookieError, MissingStateCookieError, MissingStateParamError } from '../utils/errors';
import { Auth0Request, Auth0Response } from '../http';
import { AbstractClient } from '../client/abstract-client';
import type { AuthVerification } from './login';

function getRedirectUri(config: Config): string {
return urlJoin(config.baseURL, config.routes.callback);
Expand Down Expand Up @@ -34,11 +35,27 @@ export default function callbackHandlerFactory(

let tokenResponse;

const expectedState = await transientCookieHandler.read('state', req, res);
if (!expectedState) {
let authVerification: AuthVerification;
const cookie = await transientCookieHandler.read('auth_verification', req, res);

if (!cookie) {
throw new MissingStateCookieError();
}

try {
authVerification = JSON.parse(cookie);
} catch (_) {
throw new MalformedStateCookieError();
}

const {
max_age,
code_verifier,
nonce,
state: expectedState,
response_type = config.authorizationParams.response_type
} = authVerification;

let callbackParams: URLSearchParams;
try {
callbackParams = await client.callbackParams(req, expectedState);
Expand All @@ -52,11 +69,6 @@ export default function callbackHandlerFactory(
if (!callbackParams.get('state')) {
throw new MissingStateParamError();
}
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);
const response_type =
(await transientCookieHandler.read('response_type', req, res)) || config.authorizationParams.response_type;

try {
tokenResponse = await client.callback(
Expand Down
72 changes: 34 additions & 38 deletions src/auth0-session/handlers/login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import urlJoin from 'url-join';
import { Config, LoginOptions } from '../config';
import TransientStore, { StoreOptions } from '../transient-store';
import TransientStore from '../transient-store';
import { encodeState } from '../utils/encoding';
import createDebug from '../utils/debug';
import { Auth0Request, Auth0Response } from '../http';
Expand All @@ -14,6 +14,14 @@ function getRedirectUri(config: Config): string {

export type HandleLogin = (req: Auth0Request, res: Auth0Response, options?: LoginOptions) => Promise<void>;

export type AuthVerification = {
nonce: string;
state: string;
max_age?: number;
code_verifier?: string;
response_type?: string;
};

export default function loginHandlerFactory(
config: Config,
client: AbstractClient,
Expand All @@ -35,10 +43,6 @@ export default function loginHandlerFactory(
...(opts.authorizationParams || {})
};

const transientOpts: Pick<StoreOptions, 'sameSite'> = {
sameSite: opts.authorizationParams.response_mode === 'form_post' ? 'none' : config.session.cookie.sameSite
};

const stateValue = await opts.getLoginState(opts);
if (typeof stateValue !== 'object') {
throw new Error('Custom state value must be an object.');
Expand All @@ -53,48 +57,40 @@ export default function loginHandlerFactory(
stateValue.code_verifier = client.generateRandomCodeVerifier();
}

if (responseType !== config.authorizationParams.response_type) {
await transientHandler.save('response_type', req, res, {
...transientOpts,
value: responseType
});
const validResponseTypes = ['id_token', 'code id_token', 'code'];
if (!validResponseTypes.includes(responseType)) {
throw new Error(`response_type should be one of ${validResponseTypes.join(', ')}`);
}
if (!/\bopenid\b/.test(opts.authorizationParams.scope as string)) {
throw new Error('scope should contain "openid"');
}

const authParams = {
...opts.authorizationParams,
nonce: await transientHandler.save('nonce', req, res, { ...transientOpts, value: client.generateRandomNonce() }),
state: await transientHandler.save('state', req, res, {
...transientOpts,
value: encodeState(stateValue)
}),
...(usePKCE
? {
code_challenge: await client.calculateCodeChallenge(
await transientHandler.save('code_verifier', req, res, {
...transientOpts,
value: client.generateRandomCodeVerifier()
})
),
code_challenge_method: 'S256'
}
: undefined)
const authVerification: AuthVerification = {
nonce: client.generateRandomNonce(),
state: encodeState(stateValue)
};

const validResponseTypes = ['id_token', 'code id_token', 'code'];
if (!validResponseTypes.includes(authParams.response_type as string)) {
throw new Error(`response_type should be one of ${validResponseTypes.join(', ')}`);
if (opts.authorizationParams.max_age) {
authVerification.max_age = opts.authorizationParams.max_age;
}
if (!/\bopenid\b/.test(authParams.scope as string)) {
throw new Error('scope should contain "openid"');

const authParams = { ...opts.authorizationParams, ...authVerification };

if (usePKCE) {
authVerification.code_verifier = client.generateRandomCodeVerifier();
authParams.code_challenge_method = 'S256';
authParams.code_challenge = await client.calculateCodeChallenge(authVerification.code_verifier);
}

if (authParams.max_age) {
await transientHandler.save('max_age', req, res, {
...transientOpts,
value: authParams.max_age.toString()
});
if (responseType !== config.authorizationParams.response_type) {
authVerification.response_type = responseType;
}

await transientHandler.save('auth_verification', req, res, {
sameSite: authParams.response_mode === 'form_post' ? 'none' : config.session.cookie.sameSite,
value: JSON.stringify(authVerification)
});

const authorizationUrl = await client.authorizationUrl(authParams);
debug('redirecting to %s', authorizationUrl);

Expand Down
1 change: 1 addition & 0 deletions src/auth0-session/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
MissingStateParamError,
MissingStateCookieError,
MalformedStateCookieError,
IdentityProviderError,
ApplicationError
} from './utils/errors';
Expand Down
12 changes: 12 additions & 0 deletions src/auth0-session/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ export class MissingStateParamError extends Error {
}
}

export class MalformedStateCookieError extends Error {
static message = 'Your state cookie is not valid JSON.';
status = 400;
statusCode = 400;

constructor() {
/* c8 ignore next */
super(MalformedStateCookieError.message);
Object.setPrototypeOf(this, MalformedStateCookieError.prototype);
}
}

export class MissingStateCookieError extends Error {
static message = 'Missing state cookie from login request (check login URL, callback URL and cookie config).';
status = 400;
Expand Down
1 change: 1 addition & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export {

export {
MissingStateCookieError,
MalformedStateCookieError,
MissingStateParamError,
IdentityProviderError,
ApplicationError
Expand Down
Loading

0 comments on commit 156a1a8

Please sign in to comment.