Skip to content

Commit

Permalink
Merge branch 'release/v2.0.0-beta.0' of https://github.com/auth0/next…
Browse files Browse the repository at this point in the history
…js-auth0 into release/v2.0.0-beta.0
  • Loading branch information
adamjmcgrath committed Oct 18, 2022
2 parents 6935e1f + 13a4cbf commit 065b933
Show file tree
Hide file tree
Showing 25 changed files with 319 additions and 193 deletions.
14 changes: 2 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,19 +219,9 @@ Check your hosting provider's caching rules, but in general you should **never**

### Error Handling and Security

The default server side error handler for the `/api/auth/*` routes prints the error message to screen, like this:
Errors that come from Auth0 in the `redirect_uri` callback may contain reflected user input via the OpenID Connect `error` and `error_description` query parameter. Because of this, we do some [basic escaping](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-1-html-encode-before-inserting-untrusted-data-into-html-element-content) on the `message`, `error` and `error_description` properties of the `IdentityProviderError`.

```js
try {
await handler(req, res);
} catch (error) {
res.status(error.status || 400).end(error.message);
}
```

Because the error can come from the OpenID Connect `error` query parameter we do some [basic escaping](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-1-html-encode-before-inserting-untrusted-data-into-html-element-content) which makes sure the default error handler is safe from XSS.

If you write your own error handler, you should **not** render the error `message`, or `error` and `error_description` properties without using a templating engine that will properly escape it for other HTML contexts first.
But, if you write your own error handler, you should **not** render the error `message`, or `error` and `error_description` properties without using a templating engine that will properly escape them for other HTML contexts first.

### Base Path and Internationalized Routing

Expand Down
46 changes: 23 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 2 additions & 12 deletions src/auth0-session/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Issuer, custom, HttpOptions, Client, EndSessionParameters } from 'openi
import url, { UrlObject } from 'url';
import urlJoin from 'url-join';
import createDebug from './utils/debug';
import { DiscoveryError } from './utils/errors';
import { Config } from './config';

const debug = createDebug('client');
Expand All @@ -19,17 +20,6 @@ function sortSpaceDelimitedString(str: string): string {
return str.split(' ').sort().join(' ');
}

// Issuer.discover throws an `AggregateError` in some cases, this error includes the stack trace in the
// message which causes the stack to be exposed when reporting the error in production. We're using the non standard
// `_errors` property to identify the polyfilled `AggregateError`.
// See https://github.com/sindresorhus/aggregate-error/issues/4#issuecomment-488356468
function normalizeAggregateError(e: Error | (Error & { _errors: Error[] })): Error {
if ('_errors' in e) {
return e._errors[0];
}
return e;
}

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

Expand Down Expand Up @@ -69,7 +59,7 @@ export default function get(config: Config, { name, version }: Telemetry): Clien
try {
issuer = await Issuer.discover(config.issuerBaseURL);
} catch (e) {
throw normalizeAggregateError(e);
throw new DiscoveryError(e, config.issuerBaseURL);
}
applyHttpOptionsCustom(issuer);

Expand Down
13 changes: 3 additions & 10 deletions src/auth0-session/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,9 @@ const paramsSchema = Joi.object({
sameSite: Joi.string().valid('lax', 'strict', 'none').optional().default('lax'),
secure: Joi.when(Joi.ref('/baseURL'), {
is: Joi.string().pattern(isHttps),
then: Joi.boolean()
.default(true)
.custom((value, { warn }) => {
if (!value) warn('insecure.cookie');
return value;
})
.messages({
'insecure.cookie':
"Setting your cookie to insecure when over https is not recommended, I hope you know what you're doing."
}),
then: Joi.boolean().valid(true).default(true).messages({
'any.only': 'Cookies must be secure when base url is https.'
}),
otherwise: Joi.boolean().valid(false).default(false).messages({
'any.only': 'Cookies set with the `Secure` property wont be attached to http requests'
})
Expand Down
16 changes: 11 additions & 5 deletions src/auth0-session/handlers/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { errors } from 'openid-client';
import { AuthorizationParameters, Config } from '../config';
import { ClientFactory } from '../client';
import TransientStore from '../transient-store';
import { decodeState } from '../hooks/get-login-state';
import { decodeState } from '../utils/encoding';
import { SessionCache } from '../session-cache';
import { htmlSafe } from '../../utils/errors';
import {
ApplicationError,
EscapedError,
htmlSafe,
IdentityProviderError,
MissingStateCookieError,
MissingStateParamError
Expand Down Expand Up @@ -65,6 +66,8 @@ export default function callbackHandlerFactory(
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 {
tokenSet = await client.callback(
Expand All @@ -74,16 +77,19 @@ export default function callbackHandlerFactory(
max_age: max_age !== undefined ? +max_age : undefined,
code_verifier,
nonce,
state: expectedState
state: expectedState,
response_type
},
{ exchangeBody: options?.authorizationParams }
);
} catch (err) {
if (err instanceof errors.OPError) {
err = new IdentityProviderError(err);
}
if (err instanceof errors.RPError) {
} else if (err instanceof errors.RPError) {
err = new ApplicationError(err);
/* c8 ignore next 3 */
} else {
err = new EscapedError(err.message);
}
throw createHttpError(400, err, { openIdState: decodeState(expectedState) });
}
Expand Down
14 changes: 11 additions & 3 deletions src/auth0-session/handlers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import urlJoin from 'url-join';
import { strict as assert } from 'assert';
import { Config, LoginOptions } from '../config';
import TransientStore, { StoreOptions } from '../transient-store';
import { encodeState } from '../hooks/get-login-state';
import { encodeState } from '../utils/encoding';
import { ClientFactory } from '../client';
import createDebug from '../utils/debug';
import { htmlSafe } from '../../utils/errors';
import { htmlSafe } from '../utils/errors';

const debug = createDebug('handlers');

Expand Down Expand Up @@ -50,12 +50,20 @@ export default function loginHandlerFactory(
stateValue.nonce = transientHandler.generateNonce();
stateValue.returnTo = stateValue.returnTo || opts.returnTo;

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

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

const authParams = {
...opts.authorizationParams,
nonce: await transientHandler.save('nonce', req, res, transientOpts),
Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/handlers/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import createDebug from '../utils/debug';
import { Config, LogoutOptions } from '../config';
import { ClientFactory } from '../client';
import { SessionCache } from '../session-cache';
import { htmlSafe } from '../../utils/errors';
import { htmlSafe } from '../utils/errors';

const debug = createDebug('logout');

Expand Down
32 changes: 0 additions & 32 deletions src/auth0-session/hooks/get-login-state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as jose from 'jose';
import createDebug from '../utils/debug';
import { GetLoginState } from '../config';
import { TextDecoder } from 'util';

const debug = createDebug('get-login-state');

Expand All @@ -20,33 +18,3 @@ export const getLoginState: GetLoginState = (_req, options) => {
debug('adding default state %O', state);
return state;
};

/**
* Prepare a state object to send.
*
* @param {object} stateObject
*
* @return {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
const { nonce, code_verifier, max_age, ...filteredState } = stateObject;
return jose.base64url.encode(JSON.stringify(filteredState));
}

/**
* Decode a state value.
*
* @param {string} stateValue
*
* @return {object|undefined}
*/
export function decodeState(stateValue?: string): { [key: string]: any } | undefined {
try {
return JSON.parse(new TextDecoder().decode(jose.base64url.decode(stateValue as string)));
} catch (e) {
return undefined;
}
}
32 changes: 32 additions & 0 deletions src/auth0-session/utils/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as jose from 'jose';
import { TextDecoder } from 'util';

/**
* Prepare a state object to send.
*
* @param {object} stateObject
*
* @return {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
const { nonce, code_verifier, max_age, ...filteredState } = stateObject;
return jose.base64url.encode(JSON.stringify(filteredState));
}

/**
* Decode a state value.
*
* @param {string} stateValue
*
* @return {object|undefined}
*/
export function decodeState(stateValue?: string): { [key: string]: any } | undefined {
try {
return JSON.parse(new TextDecoder().decode(jose.base64url.decode(stateValue as string)));
} catch (e) {
return undefined;
}
}
Loading

0 comments on commit 065b933

Please sign in to comment.