Skip to content

Commit

Permalink
Merge pull request #191 from auth0/express-mw-tests
Browse files Browse the repository at this point in the history
[SDK-2057] Express mw tests
  • Loading branch information
adamjmcgrath authored Nov 13, 2020
2 parents 22872a7 + 87045cc commit b6dd6e9
Show file tree
Hide file tree
Showing 29 changed files with 18,452 additions and 5,143 deletions.
2 changes: 1 addition & 1 deletion examples/typescript-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Typescript example for nextjs-auth0",
"main": "index.js",
"scripts": {
"dev": "NODE_OPTIONS='--inspect' next dev",
"dev": "next",
"build": "next build",
"start": "next start",
"format": "prettier --write \"{components,pages}/**/*.{ts,tsx}\"",
Expand Down
20,997 changes: 15,935 additions & 5,062 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"pretty": "prettier --write \"src/**/*.ts\" \"src/*.ts\"",
"lint": "eslint --fix --ext .ts ./src",
"build": "tsc -p tsconfig.build.json",
"test": "jest tests/hooks/ --coverage --maxWorkers=10",
"test": "jest tests/hooks/ tests/auth0-session --coverage --maxWorkers=10",
"test:watch": "jest --coverage --watch",
"prepublishOnly": "npm test && npm run lint",
"prepublish": "npm run build",
Expand Down Expand Up @@ -56,6 +56,7 @@
"devDependencies": {
"@panva/jose": "^1.9.3",
"@testing-library/react-hooks": "^3.4.2",
"@types/body-parser": "^1.19.0",
"@types/clone": "^2.1.0",
"@types/cookie": "^0.3.3",
"@types/hapi__iron": "^5.1.0",
Expand All @@ -70,6 +71,7 @@
"@types/webpack": "^4.41.24",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"body-parser": "^1.19.0",
"cypress": "^5.5.0",
"eslint": "^7.0.0",
"eslint-config-airbnb": "^18.1.0",
Expand All @@ -81,7 +83,7 @@
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"jest": "^26.6.2",
"jest": "^26.6.3",
"next": "^9.4.0",
"nock": "^12.0.3",
"prettier": "^2.0.5",
Expand All @@ -91,6 +93,7 @@
"request": "^2.88.2",
"start-server-and-test": "^1.11.5",
"timekeeper": "^2.2.0",
"tough-cookie": "^4.0.0",
"ts-jest": "^26.4.3",
"typescript": "^3.8.3"
},
Expand Down
28 changes: 15 additions & 13 deletions src/auth0-session/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,16 @@ export interface ClientFactory {
(): Promise<Client>;
}

// @TODO
const pkg = { name: 'some-name', version: 'some-version' };

const telemetryHeader = {
name: 'nextjs-auth0',
version: pkg.version,
env: {
node: process.version
}
export type Telemetry = {
name: string;
version: string;
};

function sortSpaceDelimitedString(str: string): string {
return str.split(' ').sort().join(' ');
}

export default function get(config: Config): ClientFactory {
export default function get(config: Config, { name, version }: Telemetry): ClientFactory {
let client: Client | null = null;

return async (): Promise<Client> => {
Expand All @@ -37,10 +31,18 @@ export default function get(config: Config): ClientFactory {
...options,
headers: {
...options.headers,
'User-Agent': `${pkg.name}/${pkg.version}`,
'User-Agent': `${name}/${version}`,
...(config.enableTelemetry
? {
'Auth0-Client': Buffer.from(JSON.stringify(telemetryHeader)).toString('base64')
'Auth0-Client': Buffer.from(
JSON.stringify({
name,
version,
env: {
node: process.version
}
})
).toString('base64')
}
: undefined)
},
Expand Down Expand Up @@ -96,7 +98,7 @@ export default function get(config: Config): ClientFactory {
client[custom.clock_tolerance] = config.clockTolerance;

if (config.idpLogout && !issuer.end_session_endpoint) {
if (config.auth0Logout || url.parse(issuer.metadata.issuer).hostname?.match('\\.auth0\\.com$')) {
if (config.auth0Logout || (url.parse(issuer.metadata.issuer).hostname as string).match('\\.auth0\\.com$')) {
Object.defineProperty(client, 'endSessionUrl', {
value(params: EndSessionParameters) {
const parsedUrl = url.parse(urlJoin(issuer.metadata.issuer, '/v2/logout'));
Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export interface LoginOptions {
/**
* Override the default {@link Config.authorizationParams authorizationParams}
*/
authorizationParams?: AuthorizationParameters;
authorizationParams?: Partial<AuthorizationParameters>;

/**
* URL to return to after login, overrides the Default is {@link Config.baseURL}
Expand Down
14 changes: 8 additions & 6 deletions src/auth0-session/cookie-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ export default class CookieStore {
}

private calculateExp(iat: number, uat: number): number {
let { absoluteDuration } = this.config.session;
const { absoluteDuration } = this.config.session;
const { rolling, rollingDuration } = this.config.session;
absoluteDuration = typeof absoluteDuration !== 'number' ? 0 : absoluteDuration;

if (typeof absoluteDuration !== 'number') {
return uat + rollingDuration;
}
if (!rolling) {
return iat + absoluteDuration;
}

return Math.min(...[uat + rollingDuration, iat + absoluteDuration].filter(Boolean));
return Math.min(uat + rollingDuration, iat + absoluteDuration);
}

public read(req: IncomingMessage): [{ [key: string]: any }?, number?] {
Expand Down Expand Up @@ -119,6 +120,7 @@ export default class CookieStore {
return [JSON.parse(cleartext.toString()), iat];
}
} catch (err) {
/* istanbul ignore else */
if (err instanceof AssertionError) {
debug('existing session was rejected because', err.message);
} else if (err instanceof errors.JOSEError) {
Expand Down Expand Up @@ -161,8 +163,8 @@ export default class CookieStore {

const cookieOptions = {
...cookieConfig,
maxAge: transient ? -1 : exp, // @TODO check
secure: 'secure' in cookieConfig ? cookieConfig.secure : req.url?.startsWith('https:') // @TODO check
maxAge: transient ? undefined : exp,
secure: 'secure' in cookieConfig ? cookieConfig.secure : this.config.baseURL.startsWith('https:')
};

debug('found session, creating signed session cookie(s) with name %o(.i)', sessionName);
Expand Down
4 changes: 2 additions & 2 deletions src/auth0-session/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const paramsSchema = Joi.object({
domain: Joi.string().optional(),
transient: Joi.boolean().optional().default(false),
httpOnly: Joi.boolean().optional().default(true),
sameSite: Joi.string().valid('Lax', 'Strict', 'None').optional().default('Lax'),
sameSite: Joi.string().valid('lax', 'strict', 'none').optional().default('lax'),
secure: Joi.boolean().optional(),
path: Joi.string().uri({ relativeOnly: true }).optional()
})
Expand Down Expand Up @@ -115,7 +115,7 @@ const paramsSchema = Joi.object({
.unknown(false)
});

type DeepPartial<T> = {
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<infer I> ? Array<DeepPartial<I>> : DeepPartial<T[P]>;
};

Expand Down
15 changes: 6 additions & 9 deletions src/auth0-session/handlers/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BadRequest } from 'http-errors';
import { TokenSet } from 'openid-client';
import { Config } from '../config';
import { ClientFactory } from '../client';
import TransientCookieHandler from '../transient-handler';
import TransientStore from '../transient-store';
import { decodeState } from '../hooks/get-login-state';
import { SessionCache } from '../session-cache';

Expand All @@ -17,24 +17,21 @@ export default function callbackHandler(
config: Config,
getClient: ClientFactory,
sessionCache: SessionCache,
transientCookieHandler: TransientCookieHandler
transientCookieHandler: TransientStore
) {
return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
const client = await getClient();
if (!client) {
return;
}

const redirectUri = getRedirectUri(config);

let expectedState;
let tokenSet;
try {
const callbackParams = client.callbackParams(req);
expectedState = transientCookieHandler.getOnce('state', req, res);
const max_age = transientCookieHandler.getOnce('max_age', req, res);
const code_verifier = transientCookieHandler.getOnce('code_verifier', req, res);
const nonce = transientCookieHandler.getOnce('nonce', req, res);
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);

tokenSet = await client.callback(redirectUri, callbackParams, {
max_age: max_age !== undefined ? +max_age : undefined,
Expand Down
22 changes: 9 additions & 13 deletions src/auth0-session/handlers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IncomingMessage, ServerResponse } from 'http';
import urlJoin from 'url-join';
import { strict as assert } from 'assert';
import { Config, LoginOptions } from '../config';
import TransientCookieHandler, { StoreOptions } from '../transient-handler';
import TransientStore, { StoreOptions } from '../transient-store';
import { encodeState } from '../hooks/get-login-state';
import { ClientFactory } from '../client';
import createDebug from '../utils/debug';
Expand All @@ -13,11 +13,7 @@ function getRedirectUri(config: Config): string {
return urlJoin(config.baseURL, config.routes.callback);
}

export default function loginHandler(
config: Config,
getClient: ClientFactory,
transientHandler: TransientCookieHandler
) {
export default function loginHandler(config: Config, getClient: ClientFactory, transientHandler: TransientStore) {
return async (req: IncomingMessage, res: ServerResponse, options: LoginOptions = {}): Promise<void> => {
const client = await getClient();

Expand Down Expand Up @@ -45,23 +41,23 @@ export default function loginHandler(
}
stateValue.nonce = transientHandler.generateNonce();

const usePKCE = opts.authorizationParams.response_type?.includes('code');
const usePKCE = (opts.authorizationParams.response_type as string).includes('code');
if (usePKCE) {
debug('response_type includes code, the authorization request will use PKCE');
stateValue.code_verifier = transientHandler.generateCodeVerifier();
}

const authParams = {
...opts.authorizationParams,
nonce: transientHandler.store('nonce', req, res, transientOpts),
state: transientHandler.store('state', req, res, {
nonce: transientHandler.save('nonce', req, res, transientOpts),
state: transientHandler.save('state', req, res, {
...transientOpts,
value: encodeState(stateValue)
}),
...(usePKCE
? {
code_challenge: transientHandler.calculateCodeChallenge(
transientHandler.store('code_verifier', req, res, transientOpts)
transientHandler.save('code_verifier', req, res, transientOpts)
),
code_challenge_method: 'S256'
}
Expand All @@ -70,13 +66,13 @@ export default function loginHandler(

const validResponseTypes = ['id_token', 'code id_token', 'code'];
assert(
validResponseTypes.includes(authParams.response_type),
validResponseTypes.includes(authParams.response_type as string),
`response_type should be one of ${validResponseTypes.join(', ')}`
);
assert(/\bopenid\b/.test(authParams.scope), 'scope should contain "openid"');
assert(/\bopenid\b/.test(authParams.scope as string), 'scope should contain "openid"');

if (authParams.max_age) {
transientHandler.store('max_age', req, res, {
transientHandler.save('max_age', req, res, {
...transientOpts,
value: authParams.max_age.toString()
});
Expand Down
8 changes: 4 additions & 4 deletions src/auth0-session/hooks/get-login-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ const debug = createDebug('get-login-state');
* return URL after the user authenticates. State is not used to carry unique PRNG values here
* because the library utilizes either nonce or PKCE for CSRF protection.
*
* @param {IncomingMessage} req
* @param {IncomingMessage} _req
* @param {LoginOptions} options
*
* @return {object}
*/
export function defaultState(req: IncomingMessage, options: LoginOptions): { [key: string]: any } {
const state = { returnTo: options.returnTo || req.url };
export function defaultState(_req: IncomingMessage, options: LoginOptions): { [key: string]: any } {
const state = { returnTo: options.returnTo };
debug('adding default state %O', state);
return state;
}
Expand All @@ -28,7 +28,7 @@ export function defaultState(req: IncomingMessage, options: LoginOptions): { [ke
*
* @return {string}
*/
export function encodeState(stateObject: any = {}): string {
export function encodeState(stateObject: { [key: string]: any }): string {
// this filters out nonce, code_verifier, and max_age from the state object so that the values are
// only stored in its dedicated transient cookie
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { default as CookieStore } from './cookie-store';
export { default as TransientCookieHandler } from './transient-handler';
export { default as TransientStore } from './transient-store';
export { Config, SessionConfig, CookieConfig, LoginOptions, LogoutOptions } from './config';
export { get as getConfig, ConfigParameters } from './get-config';
export { default as loginHandler } from './handlers/login';
Expand Down
Loading

0 comments on commit b6dd6e9

Please sign in to comment.