express-openid-connect
Advanced tools
Comparing version 1.0.2 to 2.0.0-beta.0
# CHANGELOG | ||
## [2.0.0-beta.0](https://github.com/auth0/express-openid-connect/tree/v2.0.0-beta.0) (2020-08-31) | ||
[Full Changelog](https://github.com/auth0/express-openid-connect/compare/v1.0.2...v2.0.0-beta.0) | ||
For a full list of breaking changes and migration guide, checkout https://github.com/auth0/express-openid-connect/blob/master/V2_MIGRATION_GUIDE.md | ||
**Breaking Changes** | ||
- postLogoutRedirect and response_type check [#123](https://github.com/auth0/express-openid-connect/pull/123) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Logout returnTo param [#115](https://github.com/auth0/express-openid-connect/pull/115) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Session duration behaviour [#114](https://github.com/auth0/express-openid-connect/pull/114) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Update Session cookie [#111](https://github.com/auth0/express-openid-connect/pull/111) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Configuration and API updates [#109](https://github.com/auth0/express-openid-connect/pull/109) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Update token set [#108](https://github.com/auth0/express-openid-connect/pull/108) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
**Added** | ||
- attemptSilentLogin feature [#121](https://github.com/auth0/express-openid-connect/pull/121) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Add refresh method to access token [#124](https://github.com/auth0/express-openid-connect/pull/124) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
- Architecture [#128](https://github.com/auth0/express-openid-connect/pull/128) ([adamjmcgrath](https://github.com/adamjmcgrath)) | ||
## [v1.0.2](https://github.com/auth0/express-openid-connect/tree/v1.0.2) (2020-05-12) | ||
@@ -4,0 +22,0 @@ [Full Changelog](https://github.com/auth0/express-openid-connect/compare/v1.0.1...v1.0.2) |
718
index.d.ts
// Type definitions for express-openid-connect | ||
import { AuthorizationParameters, TokenSet, UserinfoResponse } from 'openid-client'; | ||
import { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'; | ||
import { | ||
AuthorizationParameters, | ||
IdTokenClaims, | ||
UserinfoResponse, | ||
} from 'openid-client'; | ||
import { Request, Response, RequestHandler } from 'express'; | ||
/** | ||
* The Express.js Request with `oidc` context added by the `auth` middleware. | ||
* | ||
* ```js | ||
* app.use(auth()); | ||
* | ||
* app.get('/profile', (req, res) => { | ||
* const user = req.oidc.user; | ||
* ... | ||
* }) | ||
* ``` | ||
*/ | ||
interface OpenidRequest extends Request { | ||
/** | ||
* Library namespace for methods and data. | ||
* See RequestContext and ResponseContext for how this is used. | ||
*/ | ||
openid: object; | ||
/** | ||
* Library namespace for authentication methods and data. | ||
*/ | ||
oidc: RequestContext; | ||
} | ||
/** | ||
* Decoded state for use in config.handleCallback(). | ||
*/ | ||
openidState: object; | ||
/** | ||
* The Express.js Response with `oidc` context added by the `auth` middleware. | ||
* | ||
* ```js | ||
* app.use(auth()); | ||
* | ||
* app.get('/login', (req, res) => { | ||
* res.oidc.login(); | ||
* }) | ||
* ``` | ||
*/ | ||
interface OpenidResponse extends Response { | ||
/** | ||
* Library namespace for authentication methods and data. | ||
*/ | ||
oidc: ResponseContext; | ||
} | ||
/** | ||
* Tokens for use in config.handleCallback(). | ||
*/ | ||
openidTokens: TokenSet; | ||
/** | ||
* The request authentication context found on the Express request when | ||
* OpenID Connect auth middleware is added to your application. | ||
* | ||
* ```js | ||
* app.use(auth()); | ||
* | ||
* app.get('/profile', (req, res) => { | ||
* const user = req.oidc.user; | ||
* ... | ||
* }) | ||
* ``` | ||
*/ | ||
interface RequestContext { | ||
/** | ||
* Method to check the user's authenticated state, returns `true` if logged in. | ||
*/ | ||
isAuthenticated: () => boolean; | ||
/** | ||
* The OpenID Connect ID Token. | ||
* | ||
* See: https://auth0.com/docs/protocols/oidc#id-tokens | ||
*/ | ||
idToken?: string; | ||
/** | ||
* Credentials that can be used by an application to access an API. | ||
* | ||
* See: https://auth0.com/docs/protocols/oidc#access-tokens | ||
*/ | ||
accessToken?: AccessToken; | ||
/** | ||
* Credentials that can be used to refresh an access token. | ||
* | ||
* See: https://auth0.com/docs/tokens/concepts/refresh-tokens | ||
*/ | ||
refreshToken?: string; | ||
/** | ||
* An object containing all the claims of the ID Token. | ||
*/ | ||
idTokenClaims?: IdTokenClaims; | ||
/** | ||
* An object containing all the claims of the ID Token with the claims | ||
* specified in {@link ConfigParams.identityClaimFilter identityClaimFilter} removed. | ||
*/ | ||
user?: object; | ||
/** | ||
* Fetches the OIDC userinfo response. | ||
* | ||
* ```js | ||
* app.use(auth()); | ||
* | ||
* app.get('/user-info', async (req, res) => { | ||
* const userInfo = await req.oidc.fetchUserInfo(); | ||
* res.json(userInfo); | ||
* }) | ||
* ``` | ||
* | ||
*/ | ||
fetchUserInfo(): Promise<UserinfoResponse>; | ||
} | ||
/** | ||
* Configuration parameters passed to the auth() middleware. | ||
* The response authentication context found on the Express response when | ||
* OpenID Connect auth middleware is added to your application. | ||
* | ||
* ```js | ||
* app.use(auth()); | ||
* | ||
* app.get('/admin-login', (req, res) => { | ||
* res.openid.login({ returnTo: '/admin' }) | ||
* }) | ||
* ``` | ||
*/ | ||
interface ResponseContext { | ||
/** | ||
* Provided by default via the `/login` route. Call this to override or have other | ||
* login routes with custom {@link ConfigParams.authorizationParams authorizationParams} or returnTo | ||
* | ||
* ```js | ||
* app.get('/admin-login', (req, res) => { | ||
* res.oidc.login({ | ||
* returnTo: '/admin', | ||
* authorizationParams: { | ||
* scope: 'openid profile email admin:user', | ||
* } | ||
* }); | ||
* }); | ||
* ``` | ||
*/ | ||
login: (opts?: LoginOptions) => Promise<void>; | ||
/** | ||
* Provided by default via the `/logout` route. Call this to override or have other | ||
* logout routes with custom returnTo | ||
* | ||
* ```js | ||
* app.get('/admin-logout', (req, res) => { | ||
* res.oidc.logout({ returnTo: '/admin-welcome' }) | ||
* }); | ||
* ``` | ||
*/ | ||
logout: (opts?: LogoutOptions) => Promise<void>; | ||
} | ||
/** | ||
* Custom options to pass to login. | ||
*/ | ||
interface LoginOptions { | ||
/** | ||
* Override the default {@link ConfigParams.authorizationParams authorizationParams} | ||
*/ | ||
authorizationParams?: AuthorizationParameters; | ||
/** | ||
* URL to return to after login, overrides the Default is {@link Request.originalUrl} | ||
*/ | ||
returnTo?: string; | ||
} | ||
/** | ||
* Custom options to pass to logout. | ||
*/ | ||
interface LogoutOptions { | ||
/** | ||
* URL to returnTo after logout, overrides the Default in {@link ConfigParams.routes.postLogoutRedirect routes.postLogoutRedirect} | ||
*/ | ||
returnTo?: string; | ||
} | ||
/** | ||
* Configuration parameters passed to the `auth()` middleware. | ||
* | ||
* {@link ConfigParams.issuerBaseURL issuerBaseURL}, {@link ConfigParams.baseURL baseURL}, {@link ConfigParams.clientID clientID} | ||
* and {@link ConfigParams.secret secret} are required but can be configured with environmental variables: | ||
* | ||
* ```js | ||
* ISSUER_BASE_URL=https://YOUR_DOMAIN | ||
* CLIENT_ID=YOUR_CLIENT_ID | ||
* BASE_URL=https://YOUR_APPLICATION_ROOT_URL | ||
* SECRET=LONG_RANDOM_VALUE | ||
* ``` | ||
*/ | ||
interface ConfigParams { | ||
/** | ||
* Object defining application session cookie attributes. | ||
*/ | ||
appSession: boolean | AppSessionConfigParams; | ||
/** | ||
* REQUIRED. The secret(s) used to derive an encryption key for the user identity in a session cookie and | ||
* to sign the transient cookies used by the login callback. | ||
* Use a single string key or array of keys for an encrypted session cookie. | ||
* Can use env key SECRET instead. | ||
*/ | ||
secret?: string | Array<string>; | ||
/** | ||
* Boolean value to enable Auth0's logout feature. | ||
*/ | ||
auth0Logout?: boolean; | ||
/** | ||
* Object defining application session cookie attributes. | ||
*/ | ||
session?: boolean | SessionConfigParams; | ||
/** | ||
* URL parameters used when redirecting users to the authorization server to log in. | ||
*/ | ||
authorizationParams?: AuthorizationParameters | ||
/** | ||
* Boolean value to enable Auth0's logout feature. | ||
*/ | ||
auth0Logout?: boolean; | ||
/** | ||
* REQUIRED. The root URL for the application router. | ||
* Can use env key BASE_URL instead. | ||
*/ | ||
baseURL?: string; | ||
/** | ||
* URL parameters used when redirecting users to the authorization server to log in. | ||
* | ||
* If this property is not provided by your application, its default values will be: | ||
* | ||
* ```js | ||
* { | ||
* response_type: 'id_token', | ||
* response_mode: 'form_post, | ||
* scope: openid profile email' | ||
* } | ||
* ``` | ||
* | ||
* New values can be passed in to change what is returned from the authorization server depending on your specific scenario. | ||
* | ||
* For example, to receive an access token for an API, you could initialize like the sample below. Note that `response_mode` can be omitted because the OAuth2 default mode of `query` is fine: | ||
* | ||
* ```js | ||
* app.use( | ||
* auth({ | ||
* authorizationParams: { | ||
* response_type: 'code', | ||
* scope: 'openid profile email read:reports', | ||
* audience: 'https://your-api-identifier', | ||
* }, | ||
* }) | ||
* ); | ||
* ``` | ||
* | ||
* Additional custom parameters can be added as well: | ||
* | ||
* ```js | ||
* app.use(auth({ | ||
* authorizationParams: { | ||
* // Note: you need to provide required parameters if this object is set. | ||
* response_type: "id_token", | ||
* response_mode: "form_post", | ||
* scope: "openid profile email" | ||
* // Additional parameters | ||
* acr_value: "tenant:test-tenant", | ||
* custom_param: "custom-value" | ||
* } | ||
* })); | ||
* ``` | ||
*/ | ||
authorizationParams?: AuthorizationParameters; | ||
/** | ||
* REQUIRED. The Client ID for your application. | ||
* Can use env key CLIENT_ID instead. | ||
*/ | ||
clientID?: string; | ||
/** | ||
* REQUIRED. The root URL for the application router, eg https://localhost | ||
* Can use env key BASE_URL instead. | ||
*/ | ||
baseURL?: string; | ||
/** | ||
* The Client Secret for your application. | ||
* Required when requesting access tokens. | ||
* Can use env key CLIENT_SECRET instead. | ||
*/ | ||
clientSecret?: string; | ||
/** | ||
* REQUIRED. The Client ID for your application. | ||
* Can use env key CLIENT_ID instead. | ||
*/ | ||
clientID?: string; | ||
/** | ||
* Integer value for the system clock's tolerance (leeway) in seconds for ID token verification. | ||
*/ | ||
clockTolerance?: number; | ||
/** | ||
* The Client Secret for your application. | ||
* Required when requesting access tokens. | ||
* Can use env key CLIENT_SECRET instead. | ||
*/ | ||
clientSecret?: string; | ||
/** | ||
* Opt-in to sending the library and node version to your authorization server | ||
* via the `Auth0-Client` header. | ||
*/ | ||
enableTelemetry?: boolean; | ||
/** | ||
* Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.` | ||
* Default is 60 | ||
*/ | ||
clockTolerance?: number; | ||
/** | ||
* Throw a 401 error instead of triggering the login process for routes that require authentication. | ||
*/ | ||
errorOnRequiredAuth?: boolean; | ||
/** | ||
* To opt-out of sending the library and node version to your authorization server | ||
* via the `Auth0-Client` header. Default is `true | ||
*/ | ||
enableTelemetry?: boolean; | ||
/** | ||
* Function that returns a URL-safe state value for `res.openid.login()`. | ||
*/ | ||
getLoginState?: (req: OpenidRequest, config: object) => object; | ||
/** | ||
* Throw a 401 error instead of triggering the login process for routes that require authentication. | ||
* Default is `false` | ||
*/ | ||
errorOnRequiredAuth?: boolean; | ||
/** | ||
* Function that returns the profile for `req.openid.user`. | ||
*/ | ||
getUser?: (req: OpenidRequest, config: ConfigParams) => undefined | UserinfoResponse; | ||
/** | ||
* Attempt silent login (`prompt: 'none'`) on the first unauthenticated route the user visits. | ||
* For protected routes this can be useful if your Identity Provider does not default to | ||
* `prompt: 'none'` and you'd like to attempt this before requiring the user to interact with a login prompt. | ||
* For unprotected routes this can be useful if you want to check the user's logged in state on their IDP, to | ||
* show them a login/logout button for example. | ||
* Default is `false` | ||
*/ | ||
attemptSilentLogin?: boolean; | ||
/** | ||
* Function that runs on the callback route, after callback processing but before redirection. | ||
*/ | ||
handleCallback?: (req: OpenidRequest, res: Response, next: NextFunction) => void; | ||
/** | ||
* Function that returns an object with URL-safe state values for `res.oidc.login()`. | ||
* Used for passing custom state parameters to your authorization server. | ||
* | ||
* ```js | ||
* app.use(auth({ | ||
* ... | ||
* getLoginState(req, options) { | ||
* return { | ||
* returnTo: options.returnTo || req.originalUrl, | ||
* customState: 'foo' | ||
* }; | ||
* } | ||
* })) | ||
* `` | ||
*/ | ||
getLoginState?: (req: OpenidRequest, options: LoginOptions) => object; | ||
/** | ||
* Default options object used for all HTTP calls made by the library. | ||
*/ | ||
httpOptions?: object; | ||
/** | ||
* Array value of claims to remove from the ID token before storing the cookie session. | ||
* Default is `['aud', 'iss', 'iat', 'exp', 'nbf', 'nonce', 'azp', 'auth_time', 's_hash', 'at_hash', 'c_hash' ]` | ||
*/ | ||
identityClaimFilter?: string[]; | ||
/** | ||
* Array value of claims to remove from the ID token before storing the cookie session. | ||
*/ | ||
identityClaimFilter?: string[]; | ||
/** | ||
* Boolean value to log the user out from the identity provider on application logout. Default is `false` | ||
*/ | ||
idpLogout?: boolean; | ||
/** | ||
* Boolean value to log the user out from the identity provider on application logout. | ||
*/ | ||
idpLogout?: boolean; | ||
/** | ||
* String value for the expected ID token algorithm. Default is 'RS256' | ||
*/ | ||
idTokenSigningAlg?: string; | ||
/** | ||
* String value for the expected ID token algorithm. | ||
*/ | ||
idTokenAlg?: string; | ||
/** | ||
* REQUIRED. The root URL for the token issuer with no trailing slash. | ||
* Can use env key ISSUER_BASE_URL instead. | ||
*/ | ||
issuerBaseURL?: string; | ||
/** | ||
* REQUIRED. The root URL for the token issuer with no trailing slash. | ||
* Can use env key ISSUER_BASE_URL instead. | ||
*/ | ||
issuerBaseURL?: string; | ||
/** | ||
* Set a fallback cookie with no SameSite attribute when response_mode is form_post. | ||
* Default is true | ||
*/ | ||
legacySameSiteCookie?: boolean; | ||
/** | ||
* Set a fallback cookie with no SameSite attribute when response_mode is form_post. | ||
*/ | ||
legacySameSiteCookie?: boolean; | ||
/** | ||
* Require authentication for all routes. | ||
*/ | ||
authRequired?: boolean; | ||
/** | ||
* Boolean value to automatically install the login and logout routes. | ||
*/ | ||
routes?: { | ||
/** | ||
* Relative path to application login. | ||
*/ | ||
loginPath?: string; | ||
login?: string | false; | ||
@@ -132,3 +371,3 @@ /** | ||
*/ | ||
logoutPath?: string; | ||
logoutPath?: string | false; | ||
@@ -140,3 +379,3 @@ /** | ||
*/ | ||
postLogoutRedirectUri?: string; | ||
postLogoutRedirect?: string; | ||
@@ -146,13 +385,4 @@ /** | ||
*/ | ||
redirectUriPath?: string; | ||
/** | ||
* Require authentication for all routes. | ||
*/ | ||
required?: boolean | ((request: Request) => boolean); | ||
/** | ||
* Boolean value to automatically install the login and logout routes. | ||
*/ | ||
routes?: boolean; | ||
callback?: string; | ||
}; | ||
} | ||
@@ -163,59 +393,237 @@ | ||
*/ | ||
interface AppSessionConfigParams { | ||
/** | ||
* REQUIRED. The secret(s) used to derive an encryption key for the user identity in a session cookie. | ||
* Use a single string key or array of keys for an encrypted session cookie. | ||
* Can use env key APP_SESSION_SECRET instead. | ||
*/ | ||
secret?: string | Array<string>; | ||
interface SessionConfigParams { | ||
/** | ||
* String value for the cookie name used for the internal session. | ||
* This value must only include letters, numbers, and underscores. | ||
* Default is `appSession`. | ||
*/ | ||
name?: string; | ||
/** | ||
* String value for the cookie name used for the internal session. | ||
* This value must only include letters, numbers, and underscores. | ||
* Default is `appSession`. | ||
*/ | ||
name?: string; | ||
/** | ||
* If you want your session duration to be rolling, eg reset everytime the | ||
* user is active on your site, set this to a `true`. If you want the session | ||
* duration to be absolute, where the user is logged out a fixed time after login, | ||
* regardless of activity, set this to `false` | ||
* Default is `true`. | ||
*/ | ||
rolling?: boolean; | ||
/** | ||
* Integer value, in seconds, for application session duration. | ||
* Default is 86400 seconds (1 day). | ||
*/ | ||
duration?: number | ||
/** | ||
* Integer value, in seconds, for application session rolling duration. | ||
* The amount of time for which the user must be idle for then to be logged out. | ||
* Default is 86400 seconds (1 day). | ||
*/ | ||
rollingDuration?: number; | ||
/** | ||
* Domain name for the cookie. | ||
*/ | ||
cookieDomain?: string; | ||
/** | ||
* Integer value, in seconds, for application absolute rolling duration. | ||
* The amount of time after the user has logged in that they will be logged out. | ||
* Set this to `false` if you don't want an absolute duration on your session. | ||
* Default is 604800 seconds (7 days). | ||
*/ | ||
absoluteDuration?: boolean | number; | ||
/** | ||
* Set to true to use a transient cookie (cookie without an explicit expiration). | ||
* Defaults to `false` which will use appSession.duration as the cookie expiration. | ||
*/ | ||
cookieTransient?: boolean; | ||
/** | ||
* Domain name for the cookie. | ||
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `domain` | ||
*/ | ||
domain?: string; | ||
/** | ||
* Flags the cookie to be accessible only by the web server. | ||
* Defaults to `true`. | ||
*/ | ||
cookieHttpOnly?: boolean; | ||
/** | ||
* Path for the cookie. | ||
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `path` | ||
*/ | ||
path?: string; | ||
/** | ||
* Path for the cookie. | ||
*/ | ||
cookiePath?: string; | ||
/** | ||
* Set to true to use a transient cookie (cookie without an explicit expiration). | ||
* Default is `false` | ||
*/ | ||
transient?: boolean; | ||
/** | ||
* Marks the cookie to be used over secure channels only. | ||
*/ | ||
cookieSecure?: boolean; | ||
/** | ||
* Flags the cookie to be accessible only by the web server. | ||
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `httponly`. | ||
* Defaults to `true`. | ||
*/ | ||
httpOnly?: boolean; | ||
/** | ||
* Value of the SameSite Set-Cookie attribute. | ||
* Defaults to "Lax" but will be adjusted based on response_type. | ||
*/ | ||
cookieSameSite?: string; | ||
/** | ||
* Marks the cookie to be used over secure channels only. | ||
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `secure`. | ||
* Defaults to {@link Request.secure}. | ||
*/ | ||
secure?: boolean; | ||
/** | ||
* Value of the SameSite Set-Cookie attribute. | ||
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `samesite`. | ||
* Defaults to "Lax" but will be adjusted based on {@link AuthorizationParameters.response_type}. | ||
*/ | ||
sameSite?: string; | ||
} | ||
interface AccessToken { | ||
/** | ||
* The access token itself, can be an opaque string, JWT, or non-JWT token. | ||
*/ | ||
access_token: string; | ||
/** | ||
* The type of access token, Usually "Bearer". | ||
*/ | ||
token_type: string; | ||
/** | ||
* Number of seconds until the access token expires. | ||
*/ | ||
expires_in: number; | ||
/** | ||
* Returns `true` if the access_token has expired. | ||
*/ | ||
isExpired: () => boolean; | ||
/** | ||
* Performs refresh_token grant type exchange and updates the session's access token. | ||
* | ||
* ```js | ||
* let accessToken = req.oidc.accessToken; | ||
* if (accessToken.isExpired()) { | ||
* accessToken = await accessToken.refresh(); | ||
* } | ||
* ``` | ||
*/ | ||
refresh(): Promise<AccessToken>; | ||
} | ||
/** | ||
* Express JS middleware implementing sign on for Express web apps using OpenID Connect. | ||
* | ||
* The `auth()` middleware requires {@link ConfigParams.secret secret}, {@link ConfigParams.baseURL baseURL}, {@link ConfigParams.clientID clientID} | ||
* and {@link ConfigParams.issuerBaseURL issuerBaseURL}. | ||
* | ||
* If you are using a response type that includes `code`, you will also need: {@link ConfigParams.clientSecret clientSecret} | ||
* ``` | ||
* const express = require('express'); | ||
* const { auth } = require('express-openid-connect'); | ||
* | ||
* const app = express(); | ||
* | ||
* app.use( | ||
* auth({ | ||
* issuerBaseURL: 'https://YOUR_DOMAIN', | ||
* baseURL: 'https://YOUR_APPLICATION_ROOT_URL', | ||
* clientID: 'YOUR_CLIENT_ID', | ||
* secret: 'LONG_RANDOM_STRING', | ||
* }) | ||
* ); | ||
* | ||
* app.get('/', (req, res) => { | ||
* res.send(`hello ${req.oidc.user.name}`); | ||
* }); | ||
* | ||
* app.listen(3000, () => console.log('listening at http://localhost:3000')) | ||
* ``` | ||
*/ | ||
export function auth(params?: ConfigParams): RequestHandler; | ||
export function requiresAuth(): RequestHandler; | ||
export function unauthorizedHandler(): ErrorRequestHandler; | ||
/** | ||
* Set {@link ConfigParams.authRequired authRequired} to `false` then require authentication | ||
* on specific routes. | ||
* | ||
* ```js | ||
* const { auth, requiresAuth } = require('express-openid-connect'); | ||
* | ||
* app.use( | ||
* auth({ | ||
* ... | ||
* authRequired: false | ||
* }) | ||
* ); | ||
* | ||
* app.get('/profile', requiresAuth(), (req, res) => { | ||
* res.send(`hello ${req.oidc.user.name}`); | ||
* }); | ||
* | ||
* ``` | ||
*/ | ||
export function requiresAuth( | ||
requiresLoginCheck?: (req: OpenidRequest) => boolean | ||
): RequestHandler; | ||
/** | ||
* Use this MW to protect a route based on the value of a specific claim. | ||
* | ||
* ```js | ||
* const { claimEquals } = require('express-openid-connect'); | ||
* | ||
* app.get('/admin', claimEquals('isAdmin', true), (req, res) => { | ||
* res.send(...); | ||
* }); | ||
* | ||
* ``` | ||
* | ||
* @param claim The name of the claim | ||
* @param value The value of the claim, should be a primitive | ||
*/ | ||
export function claimEquals( | ||
claim: string, | ||
value: boolean | number | string | null | ||
): RequestHandler; | ||
/** | ||
* Use this MW to protect a route, checking that _all_ values are in a claim. | ||
* | ||
* ```js | ||
* const { claimIncludes } = require('express-openid-connect'); | ||
* | ||
* app.get('/admin/delete', claimIncludes('roles', 'admin', 'superadmin'), (req, res) => { | ||
* res.send(...); | ||
* }); | ||
* | ||
* ``` | ||
* | ||
* @param claim The name of the claim | ||
* @param args Claim values that must all be included | ||
*/ | ||
export function claimIncludes( | ||
claim: string, | ||
...args: (boolean | number | string | null)[] | ||
): RequestHandler; | ||
/** | ||
* Use this MW to protect a route, providing a custom function to check. | ||
* | ||
* ```js | ||
* const { claimCheck } = require('express-openid-connect'); | ||
* | ||
* app.get('/admin/community', claimCheck((req, claims) => { | ||
* return claims.isAdmin && claims.roles.includes('community'); | ||
* }), (req, res) => { | ||
* res.send(...); | ||
* }); | ||
* | ||
* ``` | ||
*/ | ||
export function claimCheck( | ||
checkFn: (req: OpenidRequest, claims: IdTokenClaims) => boolean | ||
): RequestHandler; | ||
/** | ||
* Use this MW to attempt silent login (`prompt=none`) but not require authentication. | ||
* | ||
* See {@link ConfigParams.attemptSilentLogin attemptSilentLogin} | ||
* | ||
* ```js | ||
* const { attemptSilentLogin } = require('express-openid-connect'); | ||
* | ||
* app.get('/', attemptSilentLogin(), (req, res) => { | ||
* res.render('homepage', { | ||
* isAuthenticated: req.isAuthenticated() // show a login or logout button | ||
* }); | ||
* }); | ||
* | ||
* ``` | ||
*/ | ||
export function attemptSilentLogin(): RequestHandler; |
@@ -1,11 +0,9 @@ | ||
const ResponseMode = require('./lib/ResponseMode'); | ||
const auth = require('./middleware/auth'); | ||
const requiresAuth = require('./middleware/requiresAuth'); | ||
const unauthorizedHandler = require('./middleware/unauthorizedHandler'); | ||
const attemptSilentLogin = require('./middleware/attemptSilentLogin'); | ||
module.exports = { | ||
auth, | ||
requiresAuth, | ||
unauthorizedHandler, | ||
ResponseMode, | ||
...requiresAuth, | ||
attemptSilentLogin, | ||
}; |
@@ -1,25 +0,53 @@ | ||
const { strict: assert } = require('assert'); | ||
const { JWK, JWKS, JWE } = require('jose'); | ||
const { strict: assert, AssertionError } = require('assert'); | ||
const { | ||
JWK, | ||
JWKS, | ||
JWE, | ||
errors: { JOSEError }, | ||
} = require('jose'); | ||
const onHeaders = require('on-headers'); | ||
const cookie = require('cookie'); | ||
const hkdf = require('futoin-hkdf'); | ||
const COOKIES = require('./cookies'); | ||
const { encryption: deriveKey } = require('./hkdf'); | ||
const debug = require('./debug')('appSession'); | ||
const { sessionNameDefault, sessionDurationDefault } = require('./config'); | ||
const epoch = () => (Date.now() / 1000) | 0; | ||
const CHUNK_BYTE_SIZE = 4000; | ||
const deriveKey = (secret) => hkdf(secret, 32, { info: 'JWE CEK', hash: 'SHA-256' }); | ||
const epoch = () => Date.now() / 1000 | 0; | ||
function attachSessionObject(req, sessionName, value) { | ||
Object.defineProperty(req, sessionName, { | ||
enumerable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(arg) { | ||
if (arg === null || arg === undefined) { | ||
value = arg; | ||
} else { | ||
throw new TypeError('session object cannot be reassigned'); | ||
} | ||
return undefined; | ||
}, | ||
}); | ||
} | ||
module.exports = (sessionConfig) => { | ||
module.exports = (config) => { | ||
let current; | ||
const COOKIES = Symbol('cookies'); | ||
const alg = 'dir'; | ||
const enc = 'A256GCM'; | ||
const sessionSecrets = Array.isArray(sessionConfig.secret) ? sessionConfig.secret : [sessionConfig.secret]; | ||
const sessionName = sessionConfig.name || sessionNameDefault; | ||
const sessionDuration = sessionConfig.duration || sessionDurationDefault; | ||
const secrets = Array.isArray(config.secret) | ||
? config.secret | ||
: [config.secret]; | ||
const sessionName = config.session.name; | ||
const cookieConfig = config.session.cookie; | ||
const { | ||
absoluteDuration, | ||
rolling: rollingEnabled, | ||
rollingDuration, | ||
} = config.session; | ||
let keystore = new JWKS.KeyStore(); | ||
sessionSecrets.forEach((secretString, i) => { | ||
secrets.forEach((secretString, i) => { | ||
const key = JWK.asKey(deriveKey(secretString)); | ||
@@ -36,28 +64,74 @@ if (i === 0) { | ||
function encrypt (payload, headers) { | ||
return JWE.encrypt(payload, current, { alg, enc, zip: 'DEF', ...headers }); | ||
function encrypt(payload, headers) { | ||
return JWE.encrypt(payload, current, { alg, enc, ...headers }); | ||
} | ||
function decrypt (jwe) { | ||
function decrypt(jwe) { | ||
return JWE.decrypt(jwe, keystore, { complete: true, algorithms: [enc] }); | ||
} | ||
function setCookie (req, res, { uat = epoch(), iat = uat, exp = uat + sessionDuration }) { | ||
const cookieOptions = {}; | ||
Object.keys(sessionConfig).filter(key => /^cookie/.test(key)).forEach((key) => { | ||
const cookieOptionKey = key.replace(/^cookie([A-Z])/, (match, p1) => p1.toLowerCase()); | ||
cookieOptions[cookieOptionKey] = sessionConfig[key]; | ||
}); | ||
const expires = cookieOptions.transient ? 0 : new Date(exp * 1000); | ||
function calculateExp(iat, uat) { | ||
if (!rollingEnabled) { | ||
return iat + absoluteDuration; | ||
} | ||
return Math.min( | ||
...[uat + rollingDuration, iat + absoluteDuration].filter(Boolean) | ||
); | ||
} | ||
function setCookie( | ||
req, | ||
res, | ||
{ uat = epoch(), iat = uat, exp = calculateExp(iat, uat) } | ||
) { | ||
const cookieOptions = { | ||
...cookieConfig, | ||
expires: cookieConfig.transient ? 0 : new Date(exp * 1000), | ||
secure: | ||
typeof cookieConfig.secure === 'boolean' | ||
? cookieConfig.secure | ||
: req.secure, | ||
}; | ||
delete cookieOptions.transient; | ||
if ((!req[sessionName] || !Object.keys(req[sessionName]).length) && sessionName in req[COOKIES]) { | ||
res.clearCookie(sessionName, cookieOptions); | ||
return; | ||
} | ||
// session was deleted or is empty, this matches all session cookies (chunked or unchunked) | ||
// and clears them, essentially cleaning up what we've set in the past that is now trash | ||
if (!req[sessionName] || !Object.keys(req[sessionName]).length) { | ||
debug( | ||
'session was deleted or is empty, clearing all matching session cookies' | ||
); | ||
for (const cookieName of Object.keys(req[COOKIES])) { | ||
if (cookieName.match(`^${sessionName}(?:\\.\\d)?$`)) { | ||
res.clearCookie(cookieName, { | ||
domain: cookieOptions.domain, | ||
path: cookieOptions.path, | ||
}); | ||
} | ||
} | ||
} else { | ||
debug( | ||
'found session, creating signed session cookie(s) with name %o(.i)', | ||
sessionName | ||
); | ||
const value = encrypt(JSON.stringify(req[sessionName]), { | ||
iat, | ||
uat, | ||
exp, | ||
}); | ||
if (req[sessionName] && Object.keys(req[sessionName]).length > 0) { | ||
const value = encrypt(JSON.stringify(req[sessionName]), { iat, uat, exp }); | ||
res.cookie(sessionName, value, { expires, ...cookieOptions }); | ||
const chunkCount = Math.ceil(value.length / CHUNK_BYTE_SIZE); | ||
if (chunkCount > 1) { | ||
debug('cookie size greater than %d, chunking', CHUNK_BYTE_SIZE); | ||
for (let i = 0; i < chunkCount; i++) { | ||
const chunkValue = value.slice( | ||
i * CHUNK_BYTE_SIZE, | ||
(i + 1) * CHUNK_BYTE_SIZE | ||
); | ||
const chunkCookieName = `${sessionName}.${i}`; | ||
res.cookie(chunkCookieName, chunkValue, cookieOptions); | ||
} | ||
} else { | ||
res.cookie(sessionName, value, cookieOptions); | ||
} | ||
} | ||
@@ -67,27 +141,94 @@ } | ||
return (req, res, next) => { | ||
if (!req.hasOwnProperty(COOKIES)) { | ||
req[COOKIES] = cookie.parse(req.get('cookie') || ''); | ||
} | ||
if (req.hasOwnProperty(sessionName)) { | ||
return next(); | ||
debug( | ||
'request object (req) already has %o property, this is indicative of a middleware setup problem', | ||
sessionName | ||
); | ||
return next( | ||
new Error( | ||
`req[${sessionName}] is already set, did you run this middleware twice?` | ||
) | ||
); | ||
} | ||
req[COOKIES] = cookie.parse(req.get('cookie') || ''); | ||
let iat; | ||
let uat; | ||
let exp; | ||
let existingSessionValue; | ||
try { | ||
if (req[COOKIES].hasOwnProperty(sessionName)) { | ||
const { protected: header, cleartext } = decrypt(req[COOKIES][sessionName]); | ||
({ iat, exp } = header); | ||
assert(exp > epoch()); | ||
req[sessionName] = JSON.parse(cleartext); | ||
// get JWE from unchunked session cookie | ||
debug('reading session from %s cookie', sessionName); | ||
existingSessionValue = req[COOKIES][sessionName]; | ||
} else if (req[COOKIES].hasOwnProperty(`${sessionName}.0`)) { | ||
// get JWE from chunked session cookie | ||
// iterate all cookie names | ||
// match and filter for the ones that match sessionName.<number> | ||
// sort by chunk index | ||
// concat | ||
existingSessionValue = Object.entries(req[COOKIES]) | ||
.map(([cookie, value]) => { | ||
const match = cookie.match(`^${sessionName}\\.(\\d+)$`); | ||
if (match) { | ||
return [match[1], value]; | ||
} | ||
}) | ||
.filter(Boolean) | ||
.sort(([a], [b]) => { | ||
return parseInt(a, 10) - parseInt(b, 10); | ||
}) | ||
.map(([i, chunk]) => { | ||
debug('reading session chunk from %s.%d cookie', sessionName, i); | ||
return chunk; | ||
}) | ||
.join(''); | ||
} | ||
} finally { | ||
if (!req.hasOwnProperty(sessionName) || !req[sessionName]) { | ||
req[sessionName] = {}; | ||
if (existingSessionValue) { | ||
const { protected: header, cleartext } = decrypt(existingSessionValue); | ||
({ iat, uat, exp } = header); | ||
// check that the existing session isn't expired based on options when it was established | ||
assert( | ||
exp > epoch(), | ||
'it is expired based on options when it was established' | ||
); | ||
// check that the existing session isn't expired based on current rollingDuration rules | ||
if (rollingDuration) { | ||
assert( | ||
uat + rollingDuration > epoch(), | ||
'it is expired based on current rollingDuration rules' | ||
); | ||
} | ||
// check that the existing session isn't expired based on current absoluteDuration rules | ||
if (absoluteDuration) { | ||
assert( | ||
iat + absoluteDuration > epoch(), | ||
'it is expired based on current absoluteDuration rules' | ||
); | ||
} | ||
attachSessionObject(req, sessionName, JSON.parse(cleartext)); | ||
} | ||
} catch (err) { | ||
if (err instanceof AssertionError) { | ||
debug('existing session was rejected because', err.message); | ||
} else if (err instanceof JOSEError) { | ||
debug( | ||
'existing session was rejected because it could not be decrypted', | ||
err | ||
); | ||
} else { | ||
debug('unexpected error handling session', err); | ||
} | ||
} | ||
if (!req.hasOwnProperty(sessionName) || !req[sessionName]) { | ||
attachSessionObject(req, sessionName, {}); | ||
} | ||
onHeaders(res, setCookie.bind(undefined, req, res, { iat })); | ||
@@ -94,0 +235,0 @@ |
@@ -6,2 +6,3 @@ const { Issuer, custom } = require('openid-client'); | ||
const pkg = require('../package.json'); | ||
const debug = require('./debug')('client'); | ||
@@ -12,30 +13,61 @@ const telemetryHeader = { | ||
env: { | ||
node: process.version | ||
} | ||
node: process.version, | ||
}, | ||
}; | ||
function spacedStringsToAlphabetical(string) { | ||
function sortSpaceDelimitedString(string) { | ||
return string.split(' ').sort().join(' '); | ||
} | ||
const getIssuer = memoize((issuer) => Issuer.discover(issuer)); | ||
async function get(config) { | ||
const defaultHttpOptions = (options) => { | ||
options.headers = { | ||
...options.headers, | ||
'User-Agent': `${pkg.name}/${pkg.version}`, | ||
...(config.enableTelemetry | ||
? { | ||
'Auth0-Client': Buffer.from( | ||
JSON.stringify(telemetryHeader) | ||
).toString('base64'), | ||
} | ||
: undefined), | ||
}; | ||
options.timeout = 5000; | ||
return options; | ||
}; | ||
const applyHttpOptionsCustom = (entity) => | ||
(entity[custom.http_options] = defaultHttpOptions); | ||
const issuer = await Issuer.discover(config.issuerBaseURL); | ||
applyHttpOptionsCustom(Issuer); | ||
const issuer = await getIssuer(config.issuerBaseURL); | ||
applyHttpOptionsCustom(issuer); | ||
const issuerTokenAlgs = Array.isArray(issuer.id_token_signing_alg_values_supported) ? | ||
issuer.id_token_signing_alg_values_supported : []; | ||
if (!issuerTokenAlgs.includes(config.idTokenAlg)) { | ||
throw new Error( | ||
`ID token algorithm "${config.idTokenAlg}" is not supported by the issuer. ` + | ||
`Supported ID token algorithms are: "${issuerTokenAlgs.join('", "')}". ` | ||
const issuerTokenAlgs = Array.isArray( | ||
issuer.id_token_signing_alg_values_supported | ||
) | ||
? issuer.id_token_signing_alg_values_supported | ||
: []; | ||
if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) { | ||
debug( | ||
'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.', | ||
config.idTokenSigningAlg, | ||
issuerTokenAlgs | ||
); | ||
} | ||
const configRespType = spacedStringsToAlphabetical(config.authorizationParams.response_type); | ||
const issuerRespTypes = Array.isArray(issuer.response_types_supported) ? issuer.response_types_supported : []; | ||
issuerRespTypes.map(spacedStringsToAlphabetical); | ||
const configRespType = sortSpaceDelimitedString( | ||
config.authorizationParams.response_type | ||
); | ||
const issuerRespTypes = Array.isArray(issuer.response_types_supported) | ||
? issuer.response_types_supported | ||
: []; | ||
issuerRespTypes.map(sortSpaceDelimitedString); | ||
if (!issuerRespTypes.includes(configRespType)) { | ||
throw new Error( | ||
`Response type "${configRespType}" is not supported by the issuer. ` + | ||
`Supported response types are: "${issuerRespTypes.join('", "')}". ` | ||
debug( | ||
'Response type %o is not supported by the issuer. ' + | ||
'Supported response types are: %o.', | ||
configRespType, | ||
issuerRespTypes | ||
); | ||
@@ -45,7 +77,11 @@ } | ||
const configRespMode = config.authorizationParams.response_mode; | ||
const issuerRespModes = Array.isArray(issuer.response_modes_supported) ? issuer.response_modes_supported : []; | ||
if (configRespMode && ! issuerRespModes.includes(configRespMode)) { | ||
throw new Error( | ||
`Response mode "${configRespMode}" is not supported by the issuer. ` + | ||
`Supported response modes are "${issuerRespModes.join('", "')}". ` | ||
const issuerRespModes = Array.isArray(issuer.response_modes_supported) | ||
? issuer.response_modes_supported | ||
: []; | ||
if (configRespMode && !issuerRespModes.includes(configRespMode)) { | ||
debug( | ||
'Response mode %o is not supported by the issuer. ' + | ||
'Supported response modes are %o.', | ||
configRespMode, | ||
issuerRespModes | ||
); | ||
@@ -57,33 +93,27 @@ } | ||
client_secret: config.clientSecret, | ||
id_token_signed_response_alg: config.idTokenAlg, | ||
id_token_signed_response_alg: config.idTokenSigningAlg, | ||
}); | ||
applyHttpOptionsCustom(client); | ||
client[custom.clock_tolerance] = config.clockTolerance; | ||
if (config.idpLogout && !issuer.end_session_endpoint) { | ||
if (config.auth0Logout || url.parse(issuer.issuer).hostname.match('auth0.com$')) { | ||
client.endSessionUrl = function(params) { | ||
const parsedUrl = url.parse(urlJoin(issuer.issuer, '/v2/logout')); | ||
parsedUrl.query = { | ||
returnTo: params.post_logout_redirect_uri, | ||
client_id: client.client_id | ||
}; | ||
return url.format(parsedUrl); | ||
}; | ||
if ( | ||
config.auth0Logout || | ||
url.parse(issuer.issuer).hostname.match('\\.auth0\\.com$') | ||
) { | ||
Object.defineProperty(client, 'endSessionUrl', { | ||
value(params) { | ||
const parsedUrl = url.parse(urlJoin(issuer.issuer, '/v2/logout')); | ||
parsedUrl.query = { | ||
returnTo: params.post_logout_redirect_uri, | ||
client_id: client.client_id, | ||
}; | ||
return url.format(parsedUrl); | ||
}, | ||
}); | ||
} else { | ||
throw new Error("The issuer doesn't support session management."); | ||
debug('the issuer does not support RP-Initiated Logout'); | ||
} | ||
} | ||
let httpOptions = config.httpOptions || {}; | ||
httpOptions.headers = Object.assign( | ||
// Allow configuration to override user agent header. | ||
{'User-Agent': `${pkg.name}/${pkg.version}`}, | ||
httpOptions.headers || {}, | ||
// Do not allow overriding telemetry, but allow it to be omitted. | ||
config.enableTelemetry && {'Auth0-Client': Buffer.from(JSON.stringify(telemetryHeader)).toString('base64')} | ||
); | ||
custom.setHttpOptionsDefaults(httpOptions); | ||
client[custom.clock_tolerance] = config.clockTolerance; | ||
return client; | ||
@@ -90,0 +120,0 @@ } |
const Joi = require('@hapi/joi'); | ||
const clone = require('clone'); | ||
const { defaultState: getLoginState } = require('./hooks/getLoginState'); | ||
const getUser = require('./hooks/getUser'); | ||
const handleCallback = require('./hooks/handleCallback'); | ||
const sessionDurationDefault = (24 * 60 * 60); // 1 day | ||
const sessionNameDefault = 'appSession'; | ||
const paramsSchema = Joi.object({ | ||
appSession: Joi.alternatives([ | ||
Joi.boolean().valid(false), | ||
Joi.object({ | ||
secret: Joi.alternatives([ | ||
Joi.string().min(8), | ||
Joi.array().items(Joi.string().min(8)) | ||
]).required(), | ||
duration: Joi.number().integer().optional().default(sessionDurationDefault), | ||
name: Joi.string().token().optional().default(sessionNameDefault), | ||
cookieDomain: Joi.string().optional(), | ||
cookieTransient: Joi.boolean().optional().default(false), | ||
cookieHttpOnly: Joi.boolean().optional().default(true), | ||
cookiePath: Joi.string().optional(), | ||
cookieSameSite: Joi.string().valid('Lax', 'Strict', 'None').optional().default('Lax'), | ||
cookieSecure: Joi.boolean().optional() | ||
}).unknown(false) | ||
secret: Joi.alternatives([ | ||
Joi.string().min(8), | ||
Joi.binary().min(8), | ||
Joi.array().items(Joi.string().min(8), Joi.binary().min(8)), | ||
]).required(), | ||
session: Joi.object({ | ||
rolling: Joi.boolean().optional().default(true), | ||
rollingDuration: Joi.when(Joi.ref('rolling'), { | ||
is: true, | ||
then: Joi.number().integer().messages({ | ||
'number.base': | ||
'"session.rollingDuration" must be provided an integer value when "session.rolling" is true', | ||
}), | ||
otherwise: Joi.boolean().valid(false).messages({ | ||
'any.only': | ||
'"session.rollingDuration" must be false when "session.rolling" is disabled', | ||
}), | ||
}) | ||
.optional() | ||
.default((parent) => (parent.rolling ? 24 * 60 * 60 : false)), // 1 day when rolling is enabled, else false | ||
absoluteDuration: Joi.when(Joi.ref('rolling'), { | ||
is: false, | ||
then: Joi.number().integer().messages({ | ||
'number.base': | ||
'"session.absoluteDuration" must be provided an integer value when "session.rolling" is false', | ||
}), | ||
otherwise: Joi.alternatives([ | ||
Joi.number().integer(), | ||
Joi.boolean().valid(false), | ||
]), | ||
}) | ||
.optional() | ||
.default(7 * 24 * 60 * 60), // 7 days, | ||
name: Joi.string().token().optional().default('appSession'), | ||
cookie: 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'), | ||
secure: Joi.boolean().optional(), | ||
path: Joi.string().uri({ relativeOnly: true }).optional(), | ||
}) | ||
.default() | ||
.unknown(false), | ||
}) | ||
.default() | ||
.unknown(false), | ||
auth0Logout: Joi.boolean().optional().default(false), | ||
authorizationParams: Joi.object({ | ||
response_type: Joi.string().optional().default('id_token'), | ||
scope: Joi.string().optional().default('openid profile email'), | ||
response_mode: Joi.alternatives([ | ||
Joi.string().optional(), | ||
Joi.allow(null).optional() | ||
]).default(function(parent) { | ||
const responseType = parent.response_type.split(' '); | ||
const responseIncludesTokens = responseType.includes('id_token') || responseType.includes('token'); | ||
return responseIncludesTokens ? 'form_post' : undefined; | ||
}), | ||
}).optional().unknown(true).default(), | ||
response_type: Joi.string() | ||
.optional() | ||
.valid('id_token', 'code id_token', 'code') | ||
.default('id_token'), | ||
scope: Joi.string() | ||
.optional() | ||
.pattern(/\bopenid\b/, 'contains openid') | ||
.default('openid profile email'), | ||
response_mode: Joi.string() | ||
.optional() | ||
.when('response_type', { | ||
is: 'code', | ||
then: Joi.valid('query', 'form_post'), | ||
otherwise: Joi.valid('form_post').default('form_post'), | ||
}), | ||
}) | ||
.optional() | ||
.unknown(true) | ||
.default(), | ||
baseURL: Joi.string().uri().required(), | ||
clientID: Joi.string().required(), | ||
clientSecret: Joi.string().when( | ||
Joi.ref('authorizationParams.response_type', {adjust: (value) => value && value.split(' ').includes('code')}), | ||
{ | ||
is: true, | ||
then: Joi.string().required().messages({ | ||
'any.required': '"clientSecret" is required for response_type code' | ||
clientSecret: Joi.string() | ||
.when( | ||
Joi.ref('authorizationParams.response_type', { | ||
adjust: (value) => value && value.includes('code'), | ||
}), | ||
otherwise: Joi.when( | ||
Joi.ref('idTokenAlg', {adjust: (value) => value && 'HS' === value.substring(0,2)}), | ||
{ | ||
is: true, | ||
then: Joi.string().required().messages({ | ||
'any.required': '"clientSecret" is required for ID tokens with HS algorithms' | ||
}) | ||
} | ||
) | ||
} | ||
), | ||
{ | ||
is: true, | ||
then: Joi.string().required().messages({ | ||
'any.required': | ||
'"clientSecret" is required for a response_type that includes code', | ||
}), | ||
} | ||
) | ||
.when( | ||
Joi.ref('idTokenSigningAlg', { | ||
adjust: (value) => value && value.startsWith('HS'), | ||
}), | ||
{ | ||
is: true, | ||
then: Joi.string().required().messages({ | ||
'any.required': | ||
'"clientSecret" is required for ID tokens with HMAC based algorithms', | ||
}), | ||
} | ||
), | ||
clockTolerance: Joi.number().optional().default(60), | ||
enableTelemetry: Joi.boolean().optional().default(true), | ||
errorOnRequiredAuth: Joi.boolean().optional().default(false), | ||
getLoginState: Joi.function().optional().default(() => getLoginState), | ||
getUser: Joi.function().optional().default(() => getUser), | ||
handleCallback: Joi.function().optional().default(() => handleCallback), | ||
httpOptions: Joi.object().optional(), | ||
identityClaimFilter: Joi.array().optional().default(['aud', 'iss', 'iat', 'exp', 'nonce', 'azp', 'auth_time']), | ||
idpLogout: Joi.boolean().optional().default((parent) => parent.auth0Logout || false), | ||
idTokenAlg: Joi.string().not('none').optional().default('RS256'), | ||
issuerBaseURL: Joi.alternatives([ | ||
Joi.string().uri(), | ||
Joi.string().hostname() | ||
]).required(), | ||
attemptSilentLogin: Joi.boolean().optional().default(false), | ||
getLoginState: Joi.function() | ||
.optional() | ||
.default(() => getLoginState), | ||
identityClaimFilter: Joi.array() | ||
.optional() | ||
.default([ | ||
'aud', | ||
'iss', | ||
'iat', | ||
'exp', | ||
'nbf', | ||
'nonce', | ||
'azp', | ||
'auth_time', | ||
's_hash', | ||
'at_hash', | ||
'c_hash', | ||
]), | ||
idpLogout: Joi.boolean() | ||
.optional() | ||
.default((parent) => parent.auth0Logout || false), | ||
idTokenSigningAlg: Joi.string() | ||
.insensitive() | ||
.not('none') | ||
.optional() | ||
.default('RS256'), | ||
issuerBaseURL: Joi.string().uri().required(), | ||
legacySameSiteCookie: Joi.boolean().optional().default(true), | ||
loginPath: Joi.string().uri({relativeOnly: true}).optional().default('/login'), | ||
logoutPath: Joi.string().uri({relativeOnly: true}).optional().default('/logout'), | ||
postLogoutRedirectUri: Joi.string().uri({allowRelative: true}).optional().default(''), | ||
redirectUriPath: Joi.string().uri({relativeOnly: true}).optional().default('/callback'), | ||
required: Joi.alternatives([ Joi.function(), Joi.boolean()]).optional().default(true), | ||
routes: Joi.boolean().optional().default(true), | ||
authRequired: Joi.boolean().optional().default(true), | ||
routes: Joi.object({ | ||
login: Joi.alternatives([ | ||
Joi.string().uri({ relativeOnly: true }), | ||
Joi.boolean().valid(false), | ||
]).default('/login'), | ||
logout: Joi.alternatives([ | ||
Joi.string().uri({ relativeOnly: true }), | ||
Joi.boolean().valid(false), | ||
]).default('/logout'), | ||
callback: Joi.string().uri({ relativeOnly: true }).default('/callback'), | ||
postLogoutRedirect: Joi.string().uri({ allowRelative: true }).default(''), | ||
}) | ||
.default() | ||
.unknown(false), | ||
}); | ||
module.exports.get = function(params) { | ||
let config = (typeof params == 'object' ? clone(params) : {}); | ||
config = Object.assign({ | ||
module.exports.get = function (params) { | ||
let config = typeof params === 'object' ? clone(params) : {}; | ||
config = { | ||
secret: process.env.SECRET, | ||
issuerBaseURL: process.env.ISSUER_BASE_URL, | ||
@@ -91,12 +161,8 @@ baseURL: process.env.BASE_URL, | ||
clientSecret: process.env.CLIENT_SECRET, | ||
appSession: {}, | ||
}, config); | ||
...config, | ||
}; | ||
if (process.env.APP_SESSION_SECRET && typeof config.appSession === 'object') { | ||
config.appSession.secret = config.appSession.secret || process.env.APP_SESSION_SECRET; | ||
} | ||
const paramsValidation = paramsSchema.validate(config); | ||
if (paramsValidation.error) { | ||
throw new Error(paramsValidation.error.details[0].message); | ||
throw new TypeError(paramsValidation.error.details[0].message); | ||
} | ||
@@ -106,4 +172,1 @@ | ||
}; | ||
module.exports.sessionDurationDefault = sessionDurationDefault; | ||
module.exports.sessionNameDefault = sessionNameDefault; |
@@ -5,61 +5,180 @@ const cb = require('cb'); | ||
const { TokenSet } = require('openid-client'); | ||
const clone = require('clone'); | ||
const { strict: assert } = require('assert'); | ||
const transient = require('./transientHandler'); | ||
const { get: getClient } = require('./client'); | ||
const debug = require('./debug')('context'); | ||
const { get: getClient } = require('./client'); | ||
const { encodeState } = require('../lib/hooks/getLoginState'); | ||
const { cancelSilentLogin } = require('../middleware/attemptSilentLogin'); | ||
const weakRef = require('./weakCache'); | ||
function isExpired() { | ||
return tokenSet.call(this).expired(); | ||
} | ||
async function refresh() { | ||
let { config, req } = weakRef(this); | ||
const client = await getClient(config); | ||
const oldTokenSet = tokenSet.call(this); | ||
const newTokenSet = await client.refresh(oldTokenSet); | ||
// Update the session | ||
const session = req[config.session.name]; | ||
Object.assign(session, { | ||
id_token: newTokenSet.id_token, | ||
access_token: newTokenSet.access_token, | ||
// If no new refresh token assume the current refresh token is valid. | ||
refresh_token: newTokenSet.refresh_token || oldTokenSet.refresh_token, | ||
token_type: newTokenSet.token_type, | ||
expires_at: newTokenSet.expires_at, | ||
}); | ||
// Delete the old token set | ||
const cachedTokenSet = weakRef(session); | ||
delete cachedTokenSet.value; | ||
return this.accessToken; | ||
} | ||
function tokenSet() { | ||
const contextCache = weakRef(this); | ||
const session = contextCache.req[contextCache.config.session.name]; | ||
if (!session || !('id_token' in session)) { | ||
return undefined; | ||
} | ||
const cachedTokenSet = weakRef(session); | ||
if (!('value' in cachedTokenSet)) { | ||
const { | ||
id_token, | ||
access_token, | ||
refresh_token, | ||
token_type, | ||
expires_at, | ||
} = session; | ||
cachedTokenSet.value = new TokenSet({ | ||
id_token, | ||
access_token, | ||
refresh_token, | ||
token_type, | ||
expires_at, | ||
}); | ||
} | ||
return cachedTokenSet.value; | ||
} | ||
class RequestContext { | ||
constructor(config, req, res, next) { | ||
this._config = config; | ||
this._req = req; | ||
this._res = res; | ||
this._next = next; | ||
Object.assign(weakRef(this), { config, req, res, next }); | ||
} | ||
get isAuthenticated() { | ||
return !!this.user; | ||
isAuthenticated() { | ||
return !!this.idTokenClaims; | ||
} | ||
makeTokenSet(tokenSet) { | ||
return new TokenSet(tokenSet); | ||
get idToken() { | ||
try { | ||
return tokenSet.call(this).id_token; | ||
} catch (err) { | ||
return undefined; | ||
} | ||
} | ||
async load() { | ||
if (!this.client) { | ||
this.client = await getClient(this._config); | ||
get refreshToken() { | ||
try { | ||
return tokenSet.call(this).refresh_token; | ||
} catch (err) { | ||
return undefined; | ||
} | ||
} | ||
this.user = await this._config.getUser(this._req, this._config); | ||
get accessToken() { | ||
try { | ||
const { access_token, token_type, expires_in } = tokenSet.call(this); | ||
if (!access_token || !token_type || typeof expires_in !== 'number') { | ||
return undefined; | ||
} | ||
return { | ||
access_token, | ||
token_type, | ||
expires_in, | ||
isExpired: isExpired.bind(this), | ||
refresh: refresh.bind(this), | ||
}; | ||
} catch (err) { | ||
return undefined; | ||
} | ||
} | ||
get idTokenClaims() { | ||
try { | ||
return clone(tokenSet.call(this).claims()); | ||
} catch (err) { | ||
return undefined; | ||
} | ||
} | ||
get user() { | ||
try { | ||
const { | ||
config: { identityClaimFilter }, | ||
} = weakRef(this); | ||
const { idTokenClaims } = this; | ||
const user = clone(idTokenClaims); | ||
identityClaimFilter.forEach((claim) => { | ||
delete user[claim]; | ||
}); | ||
return user; | ||
} catch (err) { | ||
return undefined; | ||
} | ||
} | ||
async fetchUserInfo() { | ||
const { config } = weakRef(this); | ||
const client = await getClient(config); | ||
return client.userinfo(tokenSet.call(this)); | ||
} | ||
} | ||
class ResponseContext { | ||
constructor(config, req, res, next) { | ||
this._config = config; | ||
this._req = req; | ||
this._res = res; | ||
this._next = next; | ||
constructor(config, req, res, next, transient) { | ||
Object.assign(weakRef(this), { config, req, res, next, transient }); | ||
} | ||
get errorOnRequiredAuth() { | ||
return this._config.errorOnRequiredAuth; | ||
return weakRef(this).config.errorOnRequiredAuth; | ||
} | ||
getRedirectUri() { | ||
return urlJoin(this._config.baseURL, this._config.redirectUriPath); | ||
const { config } = weakRef(this); | ||
return urlJoin(config.baseURL, config.routes.callback); | ||
} | ||
silentLogin(options) { | ||
return this.login({ | ||
...options, | ||
prompt: 'none', | ||
}); | ||
} | ||
async login(options = {}) { | ||
const next = cb(this._next).once(); | ||
const req = this._req; | ||
const res = this._res; | ||
const config = this._config; | ||
const client = req.openid.client; | ||
let { config, req, res, next, transient } = weakRef(this); | ||
next = cb(next).once(); | ||
const client = await getClient(config); | ||
// Set default returnTo value, allow passed-in options to override or use originalUrl on GET | ||
let returnTo = this._config.baseURL; | ||
let returnTo = config.baseURL; | ||
if (options.returnTo) { | ||
returnTo = options.returnTo; | ||
debug('req.oidc.login() called with returnTo: %s', returnTo); | ||
} else if (req.method === 'GET' && req.originalUrl) { | ||
returnTo = req.originalUrl; | ||
debug('req.oidc.login() without returnTo, using: %s', returnTo); | ||
} | ||
@@ -70,3 +189,3 @@ | ||
returnTo, | ||
...options | ||
...options, | ||
}; | ||
@@ -78,20 +197,25 @@ | ||
...config.authorizationParams, | ||
...options.authorizationParams | ||
...options.authorizationParams, | ||
}; | ||
const transientOpts = { | ||
legacySameSiteCookie: config.legacySameSiteCookie, | ||
sameSite: options.authorizationParams.response_mode === 'form_post' ? 'None' : 'Lax' | ||
sameSite: | ||
options.authorizationParams.response_mode === 'form_post' | ||
? 'None' | ||
: 'Lax', | ||
}; | ||
let stateValue = await config.getLoginState(req, options); | ||
if ( typeof stateValue !== 'object' ) { | ||
next(new Error( 'Custom state value must be an object.' )); | ||
const stateValue = await config.getLoginState(req, options); | ||
if (typeof stateValue !== 'object') { | ||
next(new Error('Custom state value must be an object.')); | ||
} | ||
stateValue.nonce = transient.createNonce(); | ||
stateValue.nonce = transient.generateNonce(); | ||
const stateTransientOpts = { | ||
...transientOpts, | ||
value: encodeState(stateValue) | ||
}; | ||
const usePKCE = options.authorizationParams.response_type.includes('code'); | ||
if (usePKCE) { | ||
debug( | ||
'response_type includes code, the authorization request will use PKCE' | ||
); | ||
stateValue.code_verifier = transient.generateCodeVerifier(); | ||
} | ||
@@ -101,7 +225,38 @@ try { | ||
...options.authorizationParams, | ||
nonce: transient.store('nonce', res, transientOpts), | ||
state: transient.store('state', res, stateTransientOpts) | ||
nonce: transient.store('nonce', req, res, transientOpts), | ||
state: transient.store('state', req, res, { | ||
...transientOpts, | ||
value: encodeState(stateValue), | ||
}), | ||
...(usePKCE | ||
? { | ||
code_challenge: transient.calculateCodeChallenge( | ||
transient.store('code_verifier', req, res, transientOpts) | ||
), | ||
code_challenge_method: 'S256', | ||
} | ||
: undefined), | ||
}; | ||
const validResponseTypes = ['id_token', 'code id_token', 'code']; | ||
assert( | ||
validResponseTypes.includes(authParams.response_type), | ||
`response_type should be one of ${validResponseTypes.join(', ')}` | ||
); | ||
assert( | ||
/\bopenid\b/.test(authParams.scope), | ||
'scope should contain "openid"' | ||
); | ||
// TODO: hook here | ||
if (authParams.max_age) { | ||
transient.store('max_age', req, res, { | ||
...transientOpts, | ||
value: authParams.max_age, | ||
}); | ||
} | ||
const authorizationUrl = client.authorizationUrl(authParams); | ||
debug('redirecting to %s', authorizationUrl); | ||
res.redirect(authorizationUrl); | ||
@@ -114,8 +269,8 @@ } catch (err) { | ||
async logout(params = {}) { | ||
const next = cb(this._next).once(); | ||
const req = this._req; | ||
const res = this._res; | ||
const config = this._config; | ||
let { config, req, res, next } = weakRef(this); | ||
next = cb(next).once(); | ||
const client = await getClient(config); | ||
let returnURL = params.returnTo || config.postLogoutRedirectUri; | ||
let returnURL = params.returnTo || config.routes.postLogoutRedirect; | ||
debug('req.oidc.logout() with return url: %s', returnURL); | ||
@@ -126,29 +281,31 @@ if (url.parse(returnURL).host === null) { | ||
if (!req.isAuthenticated()) { | ||
cancelSilentLogin(req, res); | ||
if (!req.oidc.isAuthenticated()) { | ||
debug('end-user already logged out, redirecting to %s', returnURL); | ||
return res.redirect(returnURL); | ||
} | ||
if (config.appSession) { | ||
req[config.appSession.name] = undefined; | ||
} | ||
const { idToken: id_token_hint } = req.oidc; | ||
req[config.session.name] = undefined; | ||
if (!config.idpLogout) { | ||
debug('performing a local only logout, redirecting to %s', returnURL); | ||
return res.redirect(returnURL); | ||
} | ||
const client = req.openid.client; | ||
try { | ||
returnURL = client.endSessionUrl({ | ||
post_logout_redirect_uri: returnURL, | ||
id_token_hint: req.openid.tokens, | ||
id_token_hint, | ||
}); | ||
} catch(err) { | ||
} catch (err) { | ||
return next(err); | ||
} | ||
debug('logging out of identity provider, redirecting to %s', returnURL); | ||
res.redirect(returnURL); | ||
} | ||
} | ||
module.exports = { RequestContext, ResponseContext }; |
@@ -1,9 +0,8 @@ | ||
const { encode: base64encode, decode: base64decode } = require('base64url'); | ||
const base64url = require('base64url'); | ||
const debug = require('../debug')('getLoginState'); | ||
module.exports.defaultState = defaultState; | ||
module.exports.encodeState = encodeState; | ||
module.exports.decodeState = decodeState; | ||
/** | ||
* Generate a unique state value for use during login transactions. | ||
* Generate the state value for use during login transactions. It is used to store the intended | ||
* 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. | ||
* | ||
@@ -16,5 +15,5 @@ * @param {RequestHandler} req | ||
function defaultState(req, options) { | ||
return { | ||
returnTo: options.returnTo || req.originalUrl | ||
}; | ||
const state = { returnTo: options.returnTo || req.originalUrl }; | ||
debug('adding default state %O', state); | ||
return state; | ||
} | ||
@@ -29,4 +28,7 @@ | ||
*/ | ||
function encodeState(stateObject) { | ||
return base64encode(JSON.stringify(stateObject)); | ||
function encodeState(stateObject = {}) { | ||
// 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 | ||
const { nonce, code_verifier, max_age, ...filteredState } = stateObject; // eslint-disable-line no-unused-vars | ||
return base64url.encode(JSON.stringify(filteredState)); | ||
} | ||
@@ -42,3 +44,7 @@ | ||
function decodeState(stateValue) { | ||
return JSON.parse(base64decode(stateValue)); | ||
return JSON.parse(base64url.decode(stateValue)); | ||
} | ||
module.exports.defaultState = defaultState; | ||
module.exports.encodeState = encodeState; | ||
module.exports.decodeState = decodeState; |
@@ -1,90 +0,201 @@ | ||
const crypto = require('crypto'); | ||
const { generators } = require('openid-client'); | ||
const { JWKS, JWS, JWK } = require('jose'); | ||
const { signing: deriveKey } = require('./hkdf'); | ||
exports.store = store; | ||
exports.getOnce = getOnce; | ||
exports.createNonce = createNonce; | ||
const header = { alg: 'HS256', b64: false, crit: ['b64'] }; | ||
const getPayload = (cookie, value) => Buffer.from(`${cookie}=${value}`); | ||
const flattenedJWSFromCookie = (cookie, value, signature) => ({ | ||
protected: Buffer.from(JSON.stringify(header)) | ||
.toString('base64') | ||
.replace(/=/g, '') | ||
.replace(/\+/g, '-') | ||
.replace(/\//g, '_'), | ||
payload: getPayload(cookie, value), | ||
signature, | ||
}); | ||
const generateSignature = (cookie, value, key) => { | ||
const payload = getPayload(cookie, value); | ||
return JWS.sign.flattened(payload, key, header).signature; | ||
}; | ||
const verifySignature = (cookie, value, signature, keystore) => { | ||
try { | ||
return !!JWS.verify( | ||
flattenedJWSFromCookie(cookie, value, signature), | ||
keystore, | ||
{ algorithm: 'HS256', crit: ['b64'] } | ||
); | ||
} catch (err) { | ||
return false; | ||
} | ||
}; | ||
const getCookieValue = (cookie, value, keystore) => { | ||
if (!value) { | ||
return undefined; | ||
} | ||
let signature; | ||
[value, signature] = value.split('.'); | ||
if (verifySignature(cookie, value, signature, keystore)) { | ||
return value; | ||
} | ||
/** | ||
* Set a cookie with a value or a generated nonce. | ||
* | ||
* @param {String} key Cookie name to use. | ||
* @param {Object} res Express Response object. | ||
* @param {Object} opts Options object. | ||
* @param {String} opts.sameSite SameSite attribute of "None," "Lax," or "Strict". Default is "None." | ||
* @param {String} opts.value Cookie value. Omit this key to store a generated value. | ||
* @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be set? Default is true. | ||
* @param {Boolean} opts.maxAge Cookie MaxAge value, in milliseconds. Default is 600000 (10 minutes). | ||
* | ||
* @return {String} Cookie value that was set. | ||
*/ | ||
function store(key, res, opts = {}) { | ||
const sameSiteAttr = opts.sameSite || 'None'; | ||
const isSameSiteNone = sameSiteAttr === 'None'; | ||
const value = opts.value || createNonce(); | ||
const fallbackCookie = 'legacySameSiteCookie' in opts ? opts.legacySameSiteCookie : true; | ||
return undefined; | ||
}; | ||
const basicAttr = { | ||
httpOnly: true, | ||
maxAge: 'maxAge' in opts ? parseInt(opts.maxAge, 10) : 600 * 1000 // 10 minutes | ||
}; | ||
const generateCookieValue = (cookie, value, key) => { | ||
const signature = generateSignature(cookie, value, key); | ||
return `${value}.${signature}`; | ||
}; | ||
// Set the cookie with the SameSite attribute and, if needed, the Secure flag. | ||
res.cookie(key, value, Object.assign({}, basicAttr, {sameSite: sameSiteAttr, secure: isSameSiteNone})); | ||
const COOKIES = require('./cookies'); | ||
if (isSameSiteNone && fallbackCookie) { | ||
// Set the fallback cookie with no SameSite or Secure attributes. | ||
res.cookie('_' + key, value, basicAttr); | ||
class TransientCookieHandler { | ||
constructor({ secret, session, legacySameSiteCookie }) { | ||
let current; | ||
const secrets = Array.isArray(secret) ? secret : [secret]; | ||
let keystore = new JWKS.KeyStore(); | ||
secrets.forEach((secretString, i) => { | ||
const key = JWK.asKey(deriveKey(secretString)); | ||
if (i === 0) { | ||
current = key; | ||
} | ||
keystore.add(key); | ||
}); | ||
if (keystore.size === 1) { | ||
keystore = current; | ||
} | ||
this.currentKey = current; | ||
this.keyStore = keystore; | ||
this.sessionCookieConfig = (session && session.cookie) || {}; | ||
this.legacySameSiteCookie = legacySameSiteCookie; | ||
} | ||
return value; | ||
} | ||
/** | ||
* Set a cookie with a value or a generated nonce. | ||
* | ||
* @param {String} key Cookie name to use. | ||
* @param {Object} req Express Request object. | ||
* @param {Object} res Express Response object. | ||
* @param {Object} opts Options object. | ||
* @param {String} opts.sameSite SameSite attribute of "None," "Lax," or "Strict". Default is "None." | ||
* @param {String} opts.value Cookie value. Omit this key to store a generated value. | ||
* @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be set? Default is true. | ||
* | ||
* @return {String} Cookie value that was set. | ||
*/ | ||
store( | ||
key, | ||
req, | ||
res, | ||
{ sameSite = 'None', value = this.generateNonce() } = {} | ||
) { | ||
const isSameSiteNone = sameSite === 'None'; | ||
const { domain, path, secure } = this.sessionCookieConfig; | ||
const basicAttr = { | ||
httpOnly: true, | ||
secure: typeof secure === 'boolean' ? secure : req.secure, | ||
domain, | ||
path, | ||
}; | ||
/** | ||
* Get a cookie value then delete it. | ||
* | ||
* @param {String} key Cookie name to use. | ||
* @param {Object} req Express Request object. | ||
* @param {Object} res Express Response object. | ||
* @param {Object} opts Options object. | ||
* @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be checked? Default is true. | ||
* | ||
* @return {String|undefined} Cookie value or undefined if cookie was not found. | ||
*/ | ||
function getOnce(key, req, res, opts = {}) { | ||
{ | ||
const cookieValue = generateCookieValue(key, value, this.currentKey); | ||
// Set the cookie with the SameSite attribute and, if needed, the Secure flag. | ||
res.cookie(key, cookieValue, { | ||
...basicAttr, | ||
sameSite, | ||
secure: isSameSiteNone ? true : basicAttr.secure, | ||
}); | ||
} | ||
if (!req.cookies) { | ||
return undefined; | ||
if (isSameSiteNone && this.legacySameSiteCookie) { | ||
const cookieValue = generateCookieValue( | ||
`_${key}`, | ||
value, | ||
this.currentKey | ||
); | ||
// Set the fallback cookie with no SameSite or Secure attributes. | ||
res.cookie(`_${key}`, cookieValue, basicAttr); | ||
} | ||
return value; | ||
} | ||
let value = req.cookies[key]; | ||
delete req.cookies[key]; | ||
deleteCookie(key, res); | ||
/** | ||
* Get a cookie value then delete it. | ||
* | ||
* @param {String} key Cookie name to use. | ||
* @param {Object} req Express Request object. | ||
* @param {Object} res Express Response object. | ||
* | ||
* @return {String|undefined} Cookie value or undefined if cookie was not found. | ||
*/ | ||
getOnce(key, req, res) { | ||
if (!req[COOKIES]) { | ||
return undefined; | ||
} | ||
if ('legacySameSiteCookie' in opts ? opts.legacySameSiteCookie : true) { | ||
const fallbackKey = '_' + key; | ||
value = value || req.cookies[fallbackKey]; | ||
delete req.cookies[fallbackKey]; | ||
deleteCookie(fallbackKey, res); | ||
let value = getCookieValue(key, req[COOKIES][key], this.keyStore); | ||
this.deleteCookie(key, res); | ||
if (this.legacySameSiteCookie) { | ||
const fallbackKey = `_${key}`; | ||
if (!value) { | ||
value = getCookieValue( | ||
fallbackKey, | ||
req[COOKIES][fallbackKey], | ||
this.keyStore | ||
); | ||
} | ||
this.deleteCookie(fallbackKey, res); | ||
} | ||
return value; | ||
} | ||
return value; | ||
} | ||
/** | ||
* Generates a nonce value. | ||
* | ||
* @return {String} | ||
*/ | ||
generateNonce() { | ||
return generators.nonce(); | ||
} | ||
/** | ||
* Generates a nonce value. | ||
* | ||
* @return {String} | ||
*/ | ||
function createNonce() { | ||
return crypto.randomBytes(16).toString('hex'); | ||
/** | ||
* Generates a code_verifier value. | ||
* | ||
* @return {String} | ||
*/ | ||
generateCodeVerifier() { | ||
return generators.codeVerifier(); | ||
} | ||
/** | ||
* Calculates a code_challenge value for a given codeVerifier | ||
* | ||
* @param {String} codeVerifier Code Verifier to calculate the code_challenge value from. | ||
* | ||
* @return {String} | ||
*/ | ||
calculateCodeChallenge(codeVerifier) { | ||
return generators.codeChallenge(codeVerifier); | ||
} | ||
/** | ||
* Clears the cookie from the browser by setting an empty value and an expiration date in the past | ||
* | ||
* @param {String} name Cookie name | ||
* @param {Object} res Express Response object | ||
*/ | ||
deleteCookie(name, res) { | ||
const { domain, path } = this.sessionCookieConfig; | ||
res.clearCookie(name, { | ||
domain, | ||
path, | ||
}); | ||
} | ||
} | ||
/** | ||
* Sets a blank value and zero max age cookie. | ||
* | ||
* @param {String} name Cookie name | ||
* @param {Object} res Express Response object | ||
*/ | ||
function deleteCookie(name, res) { | ||
res.cookie(name, '', {maxAge: 0}); | ||
} | ||
module.exports = TransientCookieHandler; |
const express = require('express'); | ||
const cb = require('cb'); | ||
const createError = require('http-errors'); | ||
const cookieParser = require('cookie-parser'); | ||
const debug = require('../lib/debug')('auth'); | ||
const { get: getConfig } = require('../lib/config'); | ||
const { get: getClient } = require('../lib/client'); | ||
const requiresAuth = require('./requiresAuth'); | ||
const transient = require('../lib/transientHandler'); | ||
const { requiresAuth } = require('./requiresAuth'); | ||
const attemptSilentLogin = require('./attemptSilentLogin'); | ||
const TransientCookieHandler = require('../lib/transientHandler'); | ||
const { RequestContext, ResponseContext } = require('../lib/context'); | ||
@@ -15,124 +16,143 @@ const appSession = require('../lib/appSession'); | ||
const enforceLeadingSlash = (path) => { | ||
return '/' === path.split('')[0] ? path : '/' + path; | ||
return path.split('')[0] === '/' ? path : '/' + path; | ||
}; | ||
/** | ||
* Returns a router with two routes /login and /callback | ||
* | ||
* @param {Object} [params] The parameters object; see index.d.ts for types and descriptions. | ||
* | ||
* @returns {express.Router} the router | ||
*/ | ||
* Returns a router with two routes /login and /callback | ||
* | ||
* @param {Object} [params] The parameters object; see index.d.ts for types and descriptions. | ||
* | ||
* @returns {express.Router} the router | ||
*/ | ||
module.exports = function (params) { | ||
const config = getConfig(params); | ||
const authorizeParams = config.authorizationParams; | ||
const router = express.Router(); | ||
const useAppSession = config.appSession && config.appSession.secret; | ||
debug('configuration object processed, resulting configuration: %O', config); | ||
const router = new express.Router(); | ||
const transient = new TransientCookieHandler(config); | ||
// Only use the internal cookie-based session if appSession secret is provided. | ||
if (useAppSession) { | ||
router.use(appSession(config.appSession)); | ||
} | ||
router.use(appSession(config)); | ||
// Express context and OpenID Issuer discovery. | ||
router.use(async (req, res, next) => { | ||
req.openid = new RequestContext(config, req, res, next); | ||
try { | ||
await req.openid.load(); | ||
} catch(err) { | ||
next(err); | ||
} | ||
res.openid = new ResponseContext(config, req, res, next); | ||
req.isAuthenticated = () => req.openid.isAuthenticated; | ||
req.oidc = new RequestContext(config, req, res, next); | ||
res.oidc = new ResponseContext(config, req, res, next, transient); | ||
next(); | ||
}); | ||
if (config.routes) { | ||
// Login route, configurable with loginPath. | ||
router.get( | ||
enforceLeadingSlash(config.loginPath), | ||
express.urlencoded({ extended: false }), | ||
(req, res) => { | ||
res.openid.login({ returnTo: config.baseURL }); | ||
} | ||
// Login route, configurable with routes.login | ||
if (config.routes.login) { | ||
const path = enforceLeadingSlash(config.routes.login); | ||
debug('adding GET %s route', path); | ||
router.get(path, express.urlencoded({ extended: false }), (req, res) => | ||
res.oidc.login({ returnTo: config.baseURL }) | ||
); | ||
} else { | ||
debug('login handling route not applied'); | ||
} | ||
// Logout route, configured with logoutPath. | ||
router.get( | ||
enforceLeadingSlash(config.logoutPath), | ||
(req, res) => res.openid.logout() | ||
); | ||
// Logout route, configurable with routes.logout | ||
if (config.routes.logout) { | ||
const path = enforceLeadingSlash(config.routes.logout); | ||
debug('adding GET %s route', path); | ||
router.get(path, (req, res) => res.oidc.logout()); | ||
} else { | ||
debug('logout handling route not applied'); | ||
} | ||
const callbackMethod = ('form_post' === authorizeParams.response_mode ? 'post' : 'get'); | ||
const transientOpts = { legacySameSiteCookie: config.legacySameSiteCookie }; | ||
// Callback route, configured with routes.callback. | ||
{ | ||
let client; | ||
const path = enforceLeadingSlash(config.routes.callback); | ||
const callbackStack = [ | ||
(req, res, next) => { | ||
debug('%s %s called', req.method, path); | ||
next(); | ||
}, | ||
async (req, res, next) => { | ||
next = cb(next).once(); | ||
// Callback route, configured with redirectUriPath. | ||
router[callbackMethod]( | ||
enforceLeadingSlash(config.redirectUriPath), | ||
express.urlencoded({ extended: false }), | ||
cookieParser(), | ||
async (req, res, next) => { | ||
next = cb(next).once(); | ||
try { | ||
const redirectUri = res.openid.getRedirectUri(); | ||
const client = req.openid.client; | ||
client = | ||
client || | ||
(await getClient(config).catch((err) => { | ||
next(err); | ||
})); | ||
const returnedState = transient.getOnce('state', req, res, transientOpts); | ||
if (!client) { | ||
return; | ||
} | ||
let tokenSet; | ||
try { | ||
const callbackParams = client.callbackParams(req); | ||
tokenSet = await client.callback(redirectUri, callbackParams, { | ||
nonce: transient.getOnce('nonce', req, res, transientOpts), | ||
state: returnedState, | ||
response_type: authorizeParams.response_type, | ||
}); | ||
} catch (err) { | ||
throw createError.BadRequest(err.message); | ||
} | ||
const redirectUri = res.oidc.getRedirectUri(); | ||
req.openidState = decodeState(returnedState); | ||
req.openidTokens = tokenSet; | ||
let expectedState; | ||
let tokenSet; | ||
try { | ||
const callbackParams = client.callbackParams(req); | ||
expectedState = transient.getOnce('state', req, res); | ||
const max_age = parseInt( | ||
transient.getOnce('max_age', req, res), | ||
10 | ||
); | ||
const code_verifier = transient.getOnce('code_verifier', req, res); | ||
const nonce = transient.getOnce('nonce', req, res); | ||
if (useAppSession) { | ||
let identityClaims = tokenSet.claims(); | ||
tokenSet = await client.callback(redirectUri, callbackParams, { | ||
max_age, | ||
code_verifier, | ||
nonce, | ||
state: expectedState, | ||
}); | ||
} catch (err) { | ||
throw createError.BadRequest(err.message); | ||
} | ||
config.identityClaimFilter.forEach(claim => { | ||
delete identityClaims[claim]; | ||
// TODO:? | ||
req.openidState = decodeState(expectedState); | ||
// intentional clone of the properties on tokenSet | ||
Object.assign(req[config.session.name], { | ||
id_token: tokenSet.id_token, | ||
access_token: tokenSet.access_token, | ||
refresh_token: tokenSet.refresh_token, | ||
token_type: tokenSet.token_type, | ||
expires_at: tokenSet.expires_at, | ||
}); | ||
req[config.appSession.name].claims = identityClaims; | ||
attemptSilentLogin.resumeSilentLogin(req, res); | ||
next(); | ||
} catch (err) { | ||
next(err); | ||
} | ||
}, | ||
(req, res) => res.redirect(req.openidState.returnTo || config.baseURL), | ||
]; | ||
next(); | ||
} catch (err) { | ||
next(err); | ||
} | ||
}, | ||
config.handleCallback, | ||
function (req, res) { | ||
res.redirect(req.openidState.returnTo || config.baseURL); | ||
} | ||
); | ||
debug('adding GET %s route', path); | ||
router.get(path, ...callbackStack); | ||
debug('adding POST %s route', path); | ||
router.post( | ||
path, | ||
express.urlencoded({ extended: false }), | ||
...callbackStack | ||
); | ||
} | ||
if (config.required) { | ||
const requiresAuthMiddleware = requiresAuth(); | ||
if (typeof config.required === 'function') { | ||
router.use((req, res, next) => { | ||
if (!config.required(req)) { return next(); } | ||
requiresAuthMiddleware(req, res, next); | ||
}); | ||
} else { | ||
router.use(requiresAuthMiddleware); | ||
} | ||
if (config.authRequired) { | ||
debug( | ||
'authentication is required for all routes this middleware is applied to' | ||
); | ||
router.use(requiresAuth()); | ||
} else { | ||
debug( | ||
'authentication is not required for any of the routes this middleware is applied to ' + | ||
'see and apply `requiresAuth` middlewares to your protected resources' | ||
); | ||
} | ||
if (config.attemptSilentLogin) { | ||
debug("silent login will be attempted on end-user's initial HTML request"); | ||
router.use(attemptSilentLogin()); | ||
} | ||
// Fail on initialization if config is invalid. | ||
getClient(config); | ||
return router; | ||
}; |
const createError = require('http-errors'); | ||
const debug = require('../lib/debug')('requiresAuth'); | ||
const defaultRequiresLogin = (req) => !req.oidc.isAuthenticated(); | ||
/** | ||
* Returns a middleware that verifies the existence of req.openid.user. | ||
* If "user" is not in the session it will redirect to /login, | ||
* otherwise continue to the next middleware in the stack. | ||
*/ | ||
module.exports = function() { | ||
return async function(req, res, next) { | ||
const requiresLogin = !req.openid || | ||
!req.openid.user; | ||
* Returns a middleware that checks whether an end-user is authenticated. | ||
* If end-user is not authenticated `res.oidc.login()` is triggered for an HTTP | ||
* request that can perform a redirect. | ||
*/ | ||
async function requiresLoginMiddleware(requiresLoginCheck, req, res, next) { | ||
if (!req.oidc) { | ||
next( | ||
new Error('req.oidc is not found, did you include the auth middleware?') | ||
); | ||
return; | ||
} | ||
if (requiresLogin) { | ||
if (res.openid && res.openid.login && !res.openid.errorOnRequiredAuth) { | ||
return res.openid.login(); | ||
} | ||
return next(createError.Unauthorized('Authentication is required for this route.')); | ||
if (requiresLoginCheck(req)) { | ||
if (!res.oidc.errorOnRequiredAuth && req.accepts('html')) { | ||
debug( | ||
'authentication requirements not met with errorOnRequiredAuth() returning false, calling res.oidc.login()' | ||
); | ||
return res.oidc.login(); | ||
} | ||
debug( | ||
'authentication requirements not met with errorOnRequiredAuth() returning true, calling next() with an Unauthorized error' | ||
); | ||
next( | ||
createError.Unauthorized('Authentication is required for this route.') | ||
); | ||
return; | ||
} | ||
next(); | ||
debug('authentication requirements met, calling next()'); | ||
next(); | ||
} | ||
module.exports.requiresAuth = function requiresAuth( | ||
requiresLoginCheck = defaultRequiresLogin | ||
) { | ||
return requiresLoginMiddleware.bind(undefined, requiresLoginCheck); | ||
}; | ||
function checkJSONprimitive(value) { | ||
if ( | ||
typeof value !== 'string' && | ||
typeof value !== 'number' && | ||
typeof value !== 'boolean' && | ||
value !== null | ||
) { | ||
throw new TypeError('"expected" must be a string, number, boolean or null'); | ||
} | ||
} | ||
module.exports.claimEquals = function claimEquals(claim, expected) { | ||
// check that claim is a string value | ||
if (typeof claim !== 'string') { | ||
throw new TypeError('"claim" must be a string'); | ||
} | ||
// check that expected is a JSON supported primitive | ||
checkJSONprimitive(expected); | ||
const authenticationCheck = (req) => { | ||
if (defaultRequiresLogin(req)) { | ||
return true; | ||
} | ||
const { idTokenClaims } = req.oidc; | ||
if (!(claim in idTokenClaims)) { | ||
return true; | ||
} | ||
const actual = idTokenClaims[claim]; | ||
if (actual !== expected) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
return requiresLoginMiddleware.bind(undefined, authenticationCheck); | ||
}; | ||
module.exports.claimIncludes = function claimIncludes(claim, ...expected) { | ||
// check that claim is a string value | ||
if (typeof claim !== 'string') { | ||
throw new TypeError('"claim" must be a string'); | ||
} | ||
// check that all expected are JSON supported primitives | ||
expected.forEach(checkJSONprimitive); | ||
const authenticationCheck = (req) => { | ||
if (defaultRequiresLogin(req)) { | ||
return true; | ||
} | ||
const { idTokenClaims } = req.oidc; | ||
if (!(claim in idTokenClaims)) { | ||
return true; | ||
} | ||
let actual = idTokenClaims[claim]; | ||
if (typeof actual === 'string') { | ||
actual = actual.split(' '); | ||
} else if (!Array.isArray(actual)) { | ||
debug( | ||
'unexpected claim type. expected array or string, got %o', | ||
typeof actual | ||
); | ||
return true; | ||
} | ||
actual = new Set(actual); | ||
return !expected.every(Set.prototype.has.bind(actual)); | ||
}; | ||
return requiresLoginMiddleware.bind(undefined, authenticationCheck); | ||
}; | ||
module.exports.claimCheck = function claimCheck(func) { | ||
// check that func is a function | ||
if (typeof func !== 'function' || func.constructor.name !== 'Function') { | ||
throw new TypeError('"claimCheck" expects a function'); | ||
} | ||
const authenticationCheck = (req) => { | ||
if (defaultRequiresLogin(req)) { | ||
return true; | ||
} | ||
const { idTokenClaims } = req.oidc; | ||
return !func(req, idTokenClaims); | ||
}; | ||
return requiresLoginMiddleware.bind(undefined, authenticationCheck); | ||
}; |
@@ -7,7 +7,7 @@ /** | ||
* routes. | ||
*/ | ||
module.exports = function() { | ||
*/ | ||
module.exports = function () { | ||
return (err, req, res, next) => { | ||
if (err.statusCode === 401) { | ||
return res.openid.login(); | ||
return res.oidc.login(); | ||
} | ||
@@ -14,0 +14,0 @@ next(err); |
{ | ||
"name": "express-openid-connect", | ||
"version": "1.0.2", | ||
"version": "2.0.0-beta.0", | ||
"description": "Express middleware to protect web applications using OpenID Connect.", | ||
@@ -15,43 +15,62 @@ "homepage": "https://github.com/auth0/express-openid-connect", | ||
"scripts": { | ||
"lint": "eslint . --ignore-path .gitignore", | ||
"lint": "eslint .", | ||
"start:example": "node ./examples/run_example.js", | ||
"test": "mocha", | ||
"test:ci": "nyc --reporter=lcov npm test" | ||
"test:ci": "nyc --reporter=lcov npm test", | ||
"docs": "typedoc --options typedoc.js index.d.ts", | ||
"test:end-to-end": "mocha end-to-end" | ||
}, | ||
"peerDependencies": { | ||
"express": ">= 4.17.0" | ||
"mocha": { | ||
"exit": true, | ||
"file": "./test/setup.js", | ||
"timeout": 10000 | ||
}, | ||
"dependencies": { | ||
"@hapi/joi": "^16.1.8", | ||
"@hapi/joi": "^17.1.1", | ||
"cb": "^0.1.0", | ||
"clone": "^2.1.2", | ||
"cookie": "^0.4.0", | ||
"cookie-parser": "^1.4.4", | ||
"futoin-hkdf": "^1.3.1", | ||
"cookie": "^0.4.1", | ||
"debug": "^4.1.1", | ||
"futoin-hkdf": "^1.3.2", | ||
"http-errors": "^1.7.3", | ||
"jose": "^1.24.0", | ||
"jose": "^1.27.1", | ||
"on-headers": "^1.0.2", | ||
"openid-client": "^3.14.1", | ||
"p-memoize": "^3.1.0", | ||
"openid-client": "^3.15.3", | ||
"p-memoize": "^4.0.0", | ||
"url-join": "^4.0.1" | ||
}, | ||
"devDependencies": { | ||
"@types/express": "^4.17.2", | ||
"@types/express": "^4.17.6", | ||
"chai": "^4.2.0", | ||
"chai-as-promised": "^7.1.1", | ||
"dotenv": "^8.2.0", | ||
"eslint": "^5.16.0", | ||
"express": "^4.17.1", | ||
"jsdom": "^13.2.0", | ||
"jsonwebtoken": "^8.5.1", | ||
"mocha": "^6.2.2", | ||
"express-oauth2-bearer": "^0.4.0", | ||
"husky": "^4.2.5", | ||
"lodash": "^4.17.15", | ||
"mocha": "^7.2.0", | ||
"nock": "^11.9.1", | ||
"nyc": "^15.0.0", | ||
"pem-jwk": "^2.0.0", | ||
"proxyquire": "^2.1.3", | ||
"nyc": "^15.1.0", | ||
"oidc-provider": "^6.27.0", | ||
"prettier": "^2.0.5", | ||
"pretty-quick": "^2.0.1", | ||
"puppeteer": "^5.2.0", | ||
"request": "^2.88.2", | ||
"request-promise-native": "^1.0.8", | ||
"selfsigned": "^1.10.7", | ||
"sinon": "^7.5.0" | ||
"sinon": "^7.5.0", | ||
"typedoc": "^0.17.8", | ||
"typescript": "^3.9.6" | ||
}, | ||
"peerDependencies": { | ||
"express": ">= 4.17.0" | ||
}, | ||
"engines": { | ||
"node": "^10.13.0 || >=12.0.0" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "pretty-quick --staged" | ||
} | ||
} | ||
} |
@@ -5,7 +5,2 @@ # Express OpenID Connect | ||
This library requires: | ||
- Node v10.13 or higher | ||
- Express v4.17 or higher | ||
[![CircleCI](https://img.shields.io/circleci/build/github/auth0/express-openid-connect/master?style=flat-square)](https://circleci.com/gh/auth0/express-openid-connect/tree/master) | ||
@@ -18,5 +13,7 @@ [![codecov](https://img.shields.io/codecov/c/github/auth0/express-openid-connect?style=flat-square)](https://codecov.io/gh/auth0/express-openid-connect) | ||
- [Documentation](#documentation) | ||
- [Installation](#installation) | ||
- [Install](#install) | ||
- [Getting Started](#getting-started) | ||
- [Architecture](./ARCHITECTURE.md) | ||
- [Contributing](#contributing) | ||
- [Troubleshooting](./TROUBLESHOOTING.md) | ||
- [Support + Feedback](#support--feedback) | ||
@@ -31,12 +28,12 @@ - [Vulnerability Reporting](#vulnerability-reporting) | ||
- Use the [Examples for common configurations](https://github.com/auth0/express-openid-connect/blob/master/EXAMPLES.md) for use cases beyond the basics. | ||
- The [API documentation](https://github.com/auth0/express-openid-connect/blob/master/API.md) details all configuration options, methods, and data that this library provides. | ||
- The [API documentation](https://auth0.github.io/express-openid-connect) details all configuration options, methods, and data that this library provides. | ||
- You can [run the sample application](https://github.com/auth0-samples/auth0-express-webapp-sample/tree/master) to see how this SDK functions without writing your own integration. | ||
## Installation | ||
## Install | ||
This library is installed with [npm](https://npmjs.org/package/express-openid-connect): | ||
Node.js version **>=12.0.0** is recommended, but **^10.13.0** lts/dubnium is also supported. | ||
```bash | ||
npm install express-openid-connect | ||
``` | ||
npm i express-openid-connect --save | ||
``` | ||
@@ -47,3 +44,3 @@ ## Getting Started | ||
The library needs [the following required configuration keys](https://github.com/auth0/express-openid-connect/blob/master/API.md#required-keys) to request and accept authentication. These can be configured with environmental variables: | ||
The library needs [issuerBaseURL](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#issuerbaseurl), [baseURL](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#baseurl), [clientID](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#clientid) and [secret](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#secret) to request and accept authentication. These can be configured with environmental variables: | ||
@@ -54,3 +51,3 @@ ```text | ||
BASE_URL=https://YOUR_APPLICATION_ROOT_URL | ||
APP_SESSION_SECRET=LONG_RANDOM_VALUE | ||
SECRET=LONG_RANDOM_VALUE | ||
``` | ||
@@ -63,11 +60,9 @@ | ||
const { auth } = require("express-openid-connect"); | ||
const { auth } = require('express-openid-connect'); | ||
app.use( | ||
auth({ | ||
issuerBaseURL: "https://YOUR_DOMAIN", | ||
baseURL: "https://YOUR_APPLICATION_ROOT_URL", | ||
clientID: "YOUR_CLIENT_ID", | ||
appSession: { | ||
secret: "LONG_RANDOM_STRING" | ||
} | ||
issuerBaseURL: 'https://YOUR_DOMAIN', | ||
baseURL: 'https://YOUR_APPLICATION_ROOT_URL', | ||
clientID: 'YOUR_CLIENT_ID', | ||
secret: 'LONG_RANDOM_STRING', | ||
}) | ||
@@ -81,3 +76,3 @@ ); | ||
See the [API documentation](API.md) for additional configuration possibilities and provided methods. | ||
See the [API documentation](https://auth0.github.io/express-openid-connect) for additional configuration possibilities and provided methods. | ||
@@ -84,0 +79,0 @@ ## A note on error handling |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
78294
20
1889
6
21
1
124
3
+ Addeddebug@^4.1.1
+ Added@hapi/address@4.1.0(transitive)
+ Added@hapi/formula@2.0.0(transitive)
+ Added@hapi/hoek@9.3.0(transitive)
+ Added@hapi/joi@17.1.1(transitive)
+ Added@hapi/pinpoint@2.0.1(transitive)
+ Added@hapi/topo@5.1.0(transitive)
+ Addeddebug@4.4.0(transitive)
+ Addedmimic-fn@3.1.0(transitive)
+ Addedp-limit@2.3.0(transitive)
+ Addedp-memoize@4.0.4(transitive)
+ Addedp-reflect@2.1.0(transitive)
+ Addedp-settle@4.1.1(transitive)
+ Addedp-try@2.2.0(transitive)
- Removedcookie-parser@^1.4.4
- Removed@hapi/address@2.1.4(transitive)
- Removed@hapi/formula@1.2.0(transitive)
- Removed@hapi/hoek@8.5.1(transitive)
- Removed@hapi/joi@16.1.8(transitive)
- Removed@hapi/pinpoint@1.0.2(transitive)
- Removed@hapi/topo@3.1.6(transitive)
- Removedcookie@0.7.2(transitive)
- Removedcookie-parser@1.4.7(transitive)
- Removedcookie-signature@1.0.6(transitive)
- Removedmem@4.3.0(transitive)
- Removedmimic-fn@2.1.0(transitive)
- Removedp-is-promise@2.1.0(transitive)
- Removedp-memoize@3.1.0(transitive)
Updated@hapi/joi@^17.1.1
Updatedcookie@^0.4.1
Updatedfutoin-hkdf@^1.3.2
Updatedjose@^1.27.1
Updatedopenid-client@^3.15.3
Updatedp-memoize@^4.0.0