diff --git a/.circleci/config.yml b/.circleci/config.yml index f1b81381..33790632 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: circleci/node:10 + - image: circleci/node:12-browsers environment: LANG: en_US.UTF-8 steps: @@ -26,6 +26,9 @@ jobs: - run: name: Run Tests command: npm run test:ci + - run: + name: Run End to End Tests + command: npm run test:end-to-end - run: name: Run Lint command: npm run lint diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..d8f8d469 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +docs diff --git a/.eslintrc.json b/.eslintrc.json index a9602b2c..3a9fe607 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,35 +1,16 @@ { - "env": { - "es6": true, - "node": true, - "mocha": true - }, - "extends": "eslint:recommended", - "rules": { - "no-useless-escape": 1, - "no-console": 0, - "indent": [ - "error", - 2, - { "SwitchCase": 1 } - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single", - { - "avoidEscape": true - } - ], - "semi": [ - "error", - "always" - ] - }, - "parserOptions": { - "ecmaVersion": 2018 - } + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "no-useless-escape": 1, + "no-console": 0, + "linebreak-style": ["error", "unix"] + }, + "parserOptions": { + "ecmaVersion": 2018 + } } diff --git a/.github/stale.yml b/.github/stale.yml index b2e13fc7..3cc35f17 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -17,4 +17,4 @@ staleLabel: closed:stale # Comment to post when marking as stale. Set to `false` to disable markComment: > - This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ \ No newline at end of file + This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..0422b546 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +CHANGELOG.md +coverage +.nyc_output +docs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..2ddc9656 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "printWidth": 80 +} diff --git a/API.md b/API.md deleted file mode 100644 index 7bb0fe9c..00000000 --- a/API.md +++ /dev/null @@ -1,134 +0,0 @@ -# Public API - -## Configuration Keys - -Please see the [Getting Started section of the README](https://github.com/auth0/express-openid-connect#getting-started) for examples of how to apply the configuration options to the `auth()` middleware. - -### Required Keys - -The `auth()` middleware has a few configuration keys that are required for initialization. - -- **`appSession.secret`** - The secret used to derive an encryption key for the user identity in a session cookie. It must be a string or an array of strings to use the built-in encrypted cookie session. When an array is provided, the first member is used for signing and other members can be used for decrypting old cookies (to enable secret rotation). This can be set automatically with an `APP_SESSION_SECRET` variable in your environment. -- **`baseURL`** - The root URL for the application router. This can be set automatically with a `BASE_URL` variable in your environment. -- **`clientID`** - The Client ID for your application. This can be set automatically with a `CLIENT_ID` variable in your environment. -- **`issuerBaseURL`** - The root URL for the token issuer with no trailing slash. In Auth0, this is your Application's **Domain** prepended with `https://`. This can be set automatically with an `ISSUER_BASE_URL` variable in your environment. - -If you are using a response type that includes `code` (typically combined with an `audience` parameter), you will need an additional key: - -- **`clientSecret`** - The Client Secret for your application. This can be set automatically with a `CLIENT_SECRET` variable in your environment. - -### Optional Keys - -Additional configuration keys that can be passed to `auth()` on initialization: - -- **`appSession`** - Object defining application session configuration. If this is set to `false`, the internal storage will not be used (see [this example](https://github.com/auth0/express-openid-connect/blob/master/EXAMPLES.md#4-custom-user-session-handling) for how to provide your own session mechanism). Otherwise, the `secret` key is required (see above). - - **`appSession.secret`** - See the **Required Keys** section above. - - **`appSession.duration`** - Integer value, in seconds, for application session duration. Default is 1 day. - - **`appSession.name`** - String value for the cookie name used for the internal session. This value must only include letters, numbers, and underscores. Default is `appSession`. - - **`appSession.cookieTransient`** - Sets the application session cookie expiration to `0` to create a transient cookie. Set to `false` by default. - - **`appSession.cookieDomain`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `domain`. - - **`appSession.cookieHttpOnly`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `httponly`. Set to `true` by default. - - **`appSession.cookiePath`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `path`. - - **`appSession.cookieSecure`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `secure`. - - **`appSession.cookieSameSite`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `samesite`. Set to `"Lax"` by default. -- **`auth0Logout`** - Boolean value to enable Auth0's logout feature. Default is `false`. -- **`authorizationParams`** - Object that describes the authorization server request. [See below](#authorization-params-key) for defaults and more details. -- **`clockTolerance`** - Integer value for the system clock's tolerance (leeway) in seconds for ID token verification. Default is `60`. -- **`enableTelemetry`** - Opt-in to sending the library and node version to your authorization server via the `Auth0-Client` header. Default is `true`. -- **`errorOnRequiredAuth`** - Boolean value to throw a `Unauthorized 401` error instead of triggering the login process for routes that require authentication. Default is `false`. -- **`getUser`** - Function that returns the profile for `req.openid.user`. This runs on each application page load for authenticated users. Default is [here](lib/hooks/getUser.js). -- **`handleCallback`** - Function that runs on the callback route, after callback processing but before redirection. Default is [here](lib/hooks/handleCallback.js). -- **`httpOptions`** - Default options object used for all HTTP calls made by the library ([possible options](https://github.com/sindresorhus/got/tree/v9.6.0#options)). Default is empty. -- **`identityClaimFilter`** - Array value of claims to remove from the ID token before storing the cookie session. Default is `['aud', 'iss', 'iat', 'exp', 'nonce', 'azp', 'auth_time']`. -- **`idpLogout`** - Boolean value to log the user out from the identity provider on application logout. Requires the issuer to provide a `end_session_endpoint` value. Default is `false`. -- **`idTokenAlg`** - String value for the expected ID token algorithm. Default is `RS256`. -- **`legacySameSiteCookie`** - Set a fallback cookie with no SameSite attribute when `authorizationParams.response_mode` is `form_post`. Default is `true`. -- **`loginPath`** - Relative path to application login. Default is `/login`. -- **`logoutPath`** - Relative path to application logout. Default is `/logout`. -- **`postLogoutRedirectUri`** - Either a relative path to the application or a valid URI to an external domain. The user will be redirected to this after a logout has been performed. Adding a `returnTo` parameter on the logout route will override this value. The value used must be registered at the authorization server. Default is `baseUrl`. -- **`redirectUriPath`** - Relative path to the application callback to process the response from the authorization server. This value is combined with the `baseUrl` and sent to the authorize endpoint as the `redirectUri` parameter. Default is `/callback`. -- **`required`** - Use a boolean value to require authentication for all routes. Pass a function instead to base this value on the request. Default is `true`. -- **`routes`** - Boolean value to automatically install the login and logout routes. See [the examples](EXAMPLES.md) for more information on how this key is used. Default is `true`. - -### Authorization Params Key - -The `authorizationParams` key defines the URL parameters used when redirecting users to the authorization server to log in. If this key 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" - } -})); -``` - -## `requiresAuth()` - -The `requiresAuth()` function is an optional middleware that protects specific application routes when the `required` configuration key is set to `false`: - -```javascript -const { auth, requiresAuth } = require('express-openid-connect'); -app.use( auth( { required: false } ) ); -app.use( '/admin', requiresAuth(), (req, res) => res.render('admin') ); -``` - -Using `requiresAuth()` on its own without initializing `auth()` will throw a `401 Unauthorized` error instead of triggering the login process: - -```js -// app.use(auth({required: true})); -app.get('/', requiresAuth(), (req, res) => res.render('home')); -``` - -## Session and Context - -This library adds properties and methods to the request and response objects used within route handling. - -### Request - -Every request object (typically named `req` in your route handler) is augmented with the following when the request is authenticated. If the request is not authenticated, `req.openid` is `undefined` and `req.isAuthenticated()` returns `false`. - -- **`req.openid.user`** - Contains the user information returned from the authorization server. You can change what is provided here by passing a function to the `getUser` configuration key. -- **`req.openid.client`** - Is the [OpenID Client](https://github.com/panva/node-openid-client/blob/master/docs/README.md#client) instance that can be used for additional OAuth2 and OpenID calls. See [the examples](EXAMPLES.md) for more information on how this is used. -- **`req.isAuthenticated()`** - Returns true if the request is authenticated. -- **`req.makeTokenSet()`** - Make a TokenSet object from a JSON representation of one. - -### Response - -Every response object (typically named `res` in your route handler) is augmented with the following: - -- **`res.openid.login({})`** - trigger an authentication request from any route. It receives an object with the following keys: - - `returnTo`: The URL to return to after authentication. Defaults to the current URL for `GET` routes and `baseURL` for other methods. - - `authorizationParams`: Additional parameters for the authorization call. -- **`res.openid.logout({})`** - trigger the openid connect logout if supported by the issuer. It receives an object with the following key: - - `returnTo`: The URL to return to after signing out at the authorization server. Defaults to the `baseURL`. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..216c26a8 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,13 @@ +# Architecture + +## Protect a route + +With default config - Implicit Flow with Form Post https://auth0.com/docs/flows/concepts/implicit + +![login](./login.png) + +## logout flow + +With `idpLogout: true` + +![login](./logout.png) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcdac2c4..5cc1e08a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # 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) [Full Changelog](https://github.com/auth0/express-openid-connect/compare/v1.0.1...v1.0.2) diff --git a/EXAMPLES.md b/EXAMPLES.md index ec06dd9d..269851f3 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,37 +1,39 @@ - # Examples ## 1. Basic setup -The simplest use case for this middleware: +The simplest use case for this middleware. By default all routes are protected. The middleware uses the [Implicit Flow with Form Post](https://auth0.com/docs/flows/concepts/implicit) to acquire an ID Token from the authorization server and an encrypted cookie session to persist it. ```text # .env ISSUER_BASE_URL=https://YOUR_DOMAIN CLIENT_ID=YOUR_CLIENT_ID BASE_URL=https://YOUR_APPLICATION_ROOT_URL -APP_SESSION_SECRET=LONG_RANDOM_STRING +SECRET=LONG_RANDOM_STRING ``` -```javascript -// app.js +```js +// basic.js +const express = require('express'); const { auth } = require('express-openid-connect'); -app.use(auth({ - required: true -})) +const app = express(); -app.use('/', (req, res) => { - res.send(`hello ${req.openid.user.name}`); +app.use(auth()); + +app.get('/', (req, res) => { + res.send(`hello ${req.oidc.user.sub}`); }); ``` -What you get: +**What you get:** - Every route after the `auth()` middleware requires authentication. - If a user tries to access a resource without being authenticated, the application will redirect the user to log in. After completion the user is redirected back to the resource. - The application creates `/login` and `/logout` `GET` routes. +Full example at [basic.js](./examples/basic.js), to run it: `npm run start:example -- basic` + ## 2. Require authentication for specific routes If your application has routes accessible to anonymous users, you can enable authorization per route: @@ -39,230 +41,123 @@ If your application has routes accessible to anonymous users, you can enable aut ```js const { auth, requiresAuth } = require('express-openid-connect'); -app.use(auth({ - required: false -})); +app.use( + auth({ + authRequired: false, + }) +); // Anyone can access the homepage -app.get('/', (req, res) => res.render('home')); +app.get('/', (req, res) => { + res.send('Admin Section'); +}); -// Require routes under the /admin/ prefix to check authentication. -app.get('/admin/users', requiresAuth(), (req, res) => res.render('admin-users')); -app.get('/admin/posts', requiresAuth(), (req, res) => res.render('admin-posts')); +// requiresAuth checks authentication. +app.get('/admin', requiresAuth(), (req, res) => + res.send(`Hello ${req.oidc.user.sub}, this is the admin section.`) +); ``` -Another way to configure this scenario: - -```js -const { auth } = require('express-openid-connect'); - -app.use(auth({ - required: req => req.originalUrl.startsWith('/admin/') -})); - -app.use('/', (req, res) => res.render('home')); -app.use('/admin/users', (req, res) => res.render('admin-users')); -app.use('/admin/posts', (req, res) => res.render('admin-posts')); -``` +Full example at [routes.js](./examples/routes.js), to run it: `npm run start:example -- routes` ## 3. Route customization -If you need to customize the provided login and logout routes, you can disable the default routes and write your own route handler: +If you need to customize the provided login and logout routes, you can disable the default routes and write your own route handler and pass custom paths to mount the handler at that path: ```js -app.use(auth({ routes: false })); - -app.get('/account/login', (req, res) => res.openid.login({ returnTo: '/' })); -app.get('/account/logout', (req, res) => res.openid.logout()); -``` - -... or you can define specific routes in configuration keys where the default handler will run: - -```js -app.use(auth({ - redirectUriPath: '/custom-callback-path', - loginPath: '/custom-login-path', - logoutPath: '/custom-logout-path', -})); +app.use( + auth({ + routes: { + // Override the default login route + login: false, + // Pass a custom path to redirect users to a different + // path after login. + postLogoutRedirect: '/custom-logout', + }, + }) +); + +app.get('/login', (req, res) => res.oidc.login({ returnTo: '/profile' })); + +app.get('/custom-logout', (req, res) => res.send('Bye!')); + +module.exports = app; ``` Please note that the login and logout routes are not required. Trying to access any protected resource triggers a redirect directly to Auth0 to login. These are helpful if you need to provide user-facing links to login or logout. -## 4. Custom user session handling +Full example at [custom-routes.js](./examples/custom-routes.js), to run it: `npm run start:example -- custom-routes` -By default, this library uses an encrypted and signed cookie to store the user identity claims as an application session. If the size of the user identity is too large or you're concerned about sensitive data being stored, you can provide your own session handling as part of the `getUser` function. +## 4. Obtaining access tokens to call external APIs -If, for example, you want the user session to be stored on the server, you can use a session middleware like `express-session`. We recommend persisting the data in a session store other than in-memory (which is the default), otherwise all sessions will be lost when the server restarts. The basics of handling the user identity server-side is below: +If your application needs an [access token](https://auth0.com/docs/tokens/access-tokens) for external APIs you can request one by adding `code` to your `response_type`. The Access Token will be available on the request context: ```js -const session = require('express-session'); -app.use(session({ - secret: 'replace this with a long, random, static string', - cookie: { - // Sets the session cookie to expire after 7 days. - maxAge: 7 * 24 * 60 * 60 * 1000 - } -})); - -app.use(auth({ - // Setting this configuration key to false will turn off internal session handling. - appSession: false, - handleCallback: async function (req, res, next) { - // This will store the user identity claims in the session. - req.session.userIdentity = req.openidTokens.claims(); - next(); - }, - getUser: async function (req) { - return req.session.userIdentity; - } -})); +app.use( + auth({ + authorizationParams: { + response_type: 'code', + audience: 'https://api.example.com/products', + scope: 'openid profile email read:products', + }, + }) +); + +app.get('/', async (req, res) => { + let { token_type, access_token } = req.oidc.accessToken; + const products = await request.get('https://api.example.com/products', { + headers: { + Authorization: `${token_type} ${access_token}`, + }, + }); + res.send(`Products: ${products}`); +}); ``` -## 5. Obtaining and storing access tokens to call external APIs +Full example at [access-an-api.js](./examples/access-an-api.js), to run it: `npm run start:example -- access-an-api` -If your application needs to request and store [access tokens](https://auth0.com/docs/tokens/access-tokens) for external APIs, you must provide a method to store the incoming tokens during callback. We recommend to use a persistant store, like a database or Redis, to store these tokens directly associated with the user for which they were requested. +## 5. Obtaining and using refresh tokens -If the tokens only need to be used during the user's session, they can be stored using a session middleware like `express-session`. We recommend persisting the data in a session store other than in-memory (which is the default), otherwise all tokens will be lost when the server restarts. The basics of handling the tokens is below: +[Refresh tokens](https://auth0.com/docs/tokens/concepts/refresh-tokens) can be requested along with access tokens using the `offline_access` scope during login. On a route that calls an API, check for an expired token and attempt a refresh: ```js -const session = require('express-session'); -app.use(session({ - secret: 'replace this with a long, random, static string', - cookie: { - // Sets the session cookie to expire after 7 days. - maxAge: 7 * 24 * 60 * 60 * 1000 +app.use( + auth({ + authorizationParams: { + response_type: 'code', + audience: 'https://api.example.com/products', + scope: 'openid profile email offline_access read:products', + }, + }) +); + +app.get('/', async (req, res) => { + let { token_type, access_token, isExpired, refresh } = req.oidc.accessToken; + if (isExpired()) { + ({ access_token } = await refresh()); } -})); - -app.use(auth({ - authorizationParams: { - response_type: 'code', - audience: process.env.API_AUDIENCE, - scope: 'openid profile email read:reports' - }, - handleCallback: async function (req, res, next) { - // Store recevied tokens (access and ID in this case) in server-side storage. - req.session.openidTokens = req.openidTokens; - next(); - } -})); -``` - -On a route that needs to use the access token, pull the token data from the storage and initialize a new `TokenSet` using `makeTokenSet()` method exposed by this library: - -```js -app.get('/route-that-calls-an-api', async (req, res, next) => { - - const tokenSet = req.openid.makeTokenSet(req.session.openidTokens); - let apiData = {}; - - // Check for and use tokenSet.access_token for the API call ... + const products = await request.get('https://api.example.com/products', { + headers: { + Authorization: `${token_type} ${access_token}`, + }, + }); + res.send(`Products: ${products}`); }); ``` -## 6. Obtaining and using refresh tokens - -[Refresh tokens](https://auth0.com/docs/tokens/concepts/refresh-tokens) can be requested along with access tokens using the `offline_access` scope during login. Please see the section on access tokens above for information on token storage. +Full example at [access-an-api.js](./examples/access-an-api.js), to run it: `npm run start:example -- access-an-api` -```js -app.use(auth({ - authorizationParams: { - response_type: 'code id_token', - response_mode: 'form_post', - // API identifier to indicate which API this application will be calling. - audience: process.env.API_AUDIENCE, - // Include the required scopes as well as offline_access to generate a refresh token. - scope: 'openid profile email read:reports offline_access' - }, - handleCallback: async function (req, res, next) { - // See the "Using access tokens" section above for token handling. - next(); - } -})); -``` +## 6. Calling userinfo -On a route that calls an API, check for an expired token and attempt a refresh: +If your application needs to call the `/userinfo` endpoint you can use the `fetchUserInfo` method on the request context: ```js -app.get('/route-that-calls-an-api', async (req, res, next) => { - - let apiData = {}; - - // How the tokenSet is created will depend on how the tokens are stored. - let tokenSet = req.openid.makeTokenSet(req.session.openidTokens); - let refreshToken = tokenSet.refresh_token; - - if (tokenSet && tokenSet.expired() && refreshToken) { - try { - tokenSet = await req.openid.client.refresh(tokenSet); - } catch(err) { - next(err); - } - - // New tokenSet may not include a new refresh token. - tokenSet.refresh_token = tokenSet.refresh_token || refreshToken; - - // Where you store the refreshed tokenSet will depend on how the tokens are stored. - req.session.openidTokens = tokenSet; - - // You can also refresh the session with a returned ID token. - // The req property below is the same as appSession.name, which defaults to "appSession". - // If you're using custom session handling, the claims might be stored elsewhere. - req.appSession.claims = tokenSet.claims(); - } +app.use(auth()); - // Check for and use tokenSet.access_token for the API call ... +app.get('/', async (req, res) => { + const userInfo = await req.oidc.fetchUserInfo(); + // ... }); ``` -## 7. Calling userinfo - -If your application needs to call the userinfo endpoint for the user's identity instead of the ID token used by default, add a `handleCallback` function during initialization that will make this call. Save the claims retrieved from the userinfo endpoint to the `appSession.name` on the request object (default is `appSession`): - -```js -app.use(auth({ - handleCallback: async function (req, res, next) { - const client = req.openid.client; - req.appSession = req.appSession || {}; - try { - req.appSession.claims = await client.userinfo(req.openidTokens); - next(); - } catch(e) { - next(e); - } - }, - authorizationParams: { - response_type: 'code', - scope: 'openid profile email' - } -})); -``` - -## 8. Custom state handling - -If your application needs to keep track of the request state before redirecting to log in, you can use the built-in state handling. By default, this library stores the post-callback redirect URL in a state object (along with a generated nonce) that is converted to a string, base64 encoded, and verified during callback (see [our documentation](https://auth0.com/docs/protocols/oauth2/oauth-state) for general information about this parameter). This state object can be added to and used during callback. - -You can define a `getLoginState` configuration key set to a function that takes an Express `RequestHandler` and an options object and returns a plain object: - -```js -app.use(auth({ - getLoginState: function (req, options) { - // This object will be stringified and base64 URL-safe encoded. - return { - // Property used by the library for redirecting after logging in. - returnTo: '/custom-return-path', - // Additional properties as needed. - customProperty: req.someProperty, - }; - }, - handleCallback: function (req, res, next) { - // The req.openidState.customProperty is now available to use. - if ( req.openidState.customProperty ) { - // Do something ... - } - - // Call next() to redirect to req.openidState.returnTo. - next(); - } -})); -``` +Full example at [userinfo.js](./examples/userinfo.js), to run it: `npm run start:example -- userinfo` diff --git a/README.md b/README.md index fcc71265..9b776a36 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,6 @@ Express JS middleware implementing sign on for Express web apps using 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) [![codecov](https://img.shields.io/codecov/c/github/auth0/express-openid-connect?style=flat-square)](https://codecov.io/gh/auth0/express-openid-connect) [![NPM version](https://img.shields.io/npm/v/express-openid-connect.svg?style=flat-square)](https://npmjs.org/package/express-openid-connect) @@ -15,9 +10,11 @@ This library requires: ## Table of Contents - [Documentation](#documentation) -- [Installation](#installation) +- [Install](#install) - [Getting Started](#getting-started) +- [Architecture](./ARCHITECTURE.md) - [Contributing](#contributing) +- [Troubleshooting](./TROUBLESHOOTING.md) - [Support + Feedback](#support--feedback) - [Vulnerability Reporting](#vulnerability-reporting) - [What is Auth0](#what-is-auth0) @@ -27,28 +24,28 @@ This library requires: - Our [Express Quickstart](https://auth0.com/docs/quickstart/webapp/express) is the quickest way to get up and running from scratch. - 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.19.0** lts/dubnium is also supported. -``` -npm i express-openid-connect --save +```bash +npm install express-openid-connect ``` ## Getting Started Follow our [Secure Local Development guide](https://auth0.com/docs/libraries/secure-local-development) to ensure that applications using this library are running over secure channels (HTTPS URLs). Applications using this library without HTTPS may experience "invalid state" errors. -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: ```text ISSUER_BASE_URL=https://YOUR_DOMAIN CLIENT_ID=YOUR_CLIENT_ID BASE_URL=https://YOUR_APPLICATION_ROOT_URL -APP_SESSION_SECRET=LONG_RANDOM_VALUE +SECRET=LONG_RANDOM_VALUE ``` ... or in the library initialization: @@ -56,15 +53,13 @@ APP_SESSION_SECRET=LONG_RANDOM_VALUE ```js // index.js -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', }) ); ``` @@ -73,7 +68,7 @@ With this basic configuration, your application will require authentication for See the [examples](EXAMPLES.md) for route-specific authentication, custom application session handling, requesting and using access tokens for external APIs, and more. -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. ## A note on error handling @@ -128,5 +123,4 @@ Auth0 helps you to easily: This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fauth0%2Fexpress-openid-connect.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fauth0%2Fexpress-openid-connect?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fauth0%2Fexpress-openid-connect.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fauth0%2Fexpress-openid-connect?ref=badge_large) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 00000000..0649ffd5 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,25 @@ +# Troubleshooting + +If you're running into problems there are 2 sources of logs you can check to get more information about what's going on: + +## 1. Debug logs + +Add the `DEBUG` environment variable and filter the messages by `express-openid-connect`. + +```shell script +$ DEBUG=express-openid-connect:* node index.js +``` + +On Windows, use the corresponding command. + +```shell script +> set DEBUG=express-openid-connect:* & node index.js +``` + +For more information about `debug`, see [debug](https://github.com/visionmedia/debug) + +## 2. Auth0 tenant logs + +If you're an Auth0 customer, you can check out what's happening on the Authorization Server using your [Auth0 tenant logs](https://manage.auth0.com/#/logs). + +Visit [View Log Data in the Dashboard](https://auth0.com/docs/logs/guides/view-log-data-dashboard) for more information. diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md new file mode 100644 index 00000000..499fa049 --- /dev/null +++ b/V2_MIGRATION_GUIDE.md @@ -0,0 +1,184 @@ +# V2 Migration Guide + +`v2.x` brings a number of breaking changes in the library behaviour, configuration options as well as its cookie format. As a result, `v1.x` session will not be accepted by the library after upgrading to `v2.x`, they will in fact be silently ignored and cleaned up. + +## Configuration + +### Required Configuration Properties + +- `appSession.secret` is now just `secret` (because it is now used to sign the transient cookies as well as the `appSession` cookie). The environment variable has changed from `APP_SESSION_SECRET` to `SECRET`. + +```dotenv +# Before +SESSION_SECRET=LONG_RANDOM_STRING + +# After +SECRET=LONG_RANDOM_STRING +``` + +```js +const { auth } = require('express-openid-connect'); + +// Before +app.use( + auth({ + session: { + secret: 'LONG_RANDOM_STRING', + }, + }) +); + +// After +app.use( + auth({ + secret: 'LONG_RANDOM_STRING', + }) +); +``` + +### Route customization + +You can now enable individual routes rather than all or nothing and the path to routes can be customized using the `routes` config. + +```js +// Before +app.use( + auth({ + routes: true, + loginPath: '/custom/login', + logoutPath: '/custom/logout', + redirectUriPath: '/custom/callback', + postLogoutRedirectUri: '/custom/post-logout', + }) +); + +// After +app.use( + auth({ + routes: { + login: '/custom/login', + logout: '/custom/logout', + callback: '/custom/callback', + postLogoutRedirect: '/custom/post-logout', + }, + }) +); +``` + +### Session Lifecycle configuration + +Session duration was being refreshed (e.g. it was "rolling") for another 24 hours (default value unchanged) with every page visit and configured using `appSession.duration`. In addition to that being optional behaviour now (`appSession.rolling`) every session may also have a an absolute duration it will be "rolled" for, when that duration is passed the session is not accepted as valid anymore. The default for this "absolute" duration is 7 days. + +```js +// Before +app.use( + auth({ + appSession: { + duration: 86400, // default 1 day in secs + }, + }) +); + +// After +app.use( + auth({ + appSession: { + rolling: true, + rollingDuration: 86400, // default 1 day rolling duration in secs + absoluteDuration: 86400 * 7, // default 7 days absolute duration in secs + }, + }) +); +``` + +### `required` is now `authRequired` + +To enable or disable all routes to require authentication, use the `authRequired` configuration (default `true`) + +```js +// Before +app.use( + auth({ + required: true, + }) +); + +// After +app.use( + auth({ + authRequired: true, + }) +); +``` + +### Configuration items renamed/removed + +- **`idTokenAlg` is now `idTokenSigningAlg`** - to specify an id token signing algorithm, use `idTokenSigningAlg` +- **`httpOptions`** was removed - it is no longer possible to pass custom http request options to the underlying library. It will be again in the future in a more curated and comprehensive feature we have in mind for this library. +- **`handleCallback`** and **`getUser`** were removed - These "hooks" will be made available in the future in a more curated and comprehensive feature we have in mind for this library. + +```js +// Before +app.use( + auth({ + required: true, + idTokenAlg: 'RS256', + httpOptions: {}, + handleCallback: () => {}, + getUser: () => {}, + }) +); + +// After +app.use( + auth({ + authRequired: true, + idTokenSigningAlg: 'RS256', + }) +); +``` + +## Session and Context + +This library adds an auth context to the request and response objects used within route handling called `oidc` (was `openid`). + +```js +// Before +app.get('/', (req, res) => { + const user = req.openid.user; + const client = req.openid.client; // It is no longer possible to access the underlying client + const isAuthenticated = req.isAuthenticated(); + const tokenSet = req.makeTokenSet({ tokens }); + + // Login + res.openid.login({}); + // Logout + res.openid.logout({}); +}); + +// After +app.get('/', async (req, res) => { + const user = req.oidc.user; + const claims = req.oidc.idTokenClaims; + const isAuthenticated = req.oidc.isAuthenticated(); + const idToken = req.oidc.idToken; + const { + access_token, + token_type, + expires_in, + isExpired, + refresh, + } = req.oidc.accessToken; // If `code` in response_type + const refreshToken = req.oidc.refreshToken; // if `offline_access` in scope + const userInfo = await req.oidc.fetchUserInfo(); + + // Login + res.oidc.login({}); + // Logout + res.oidc.logout({}); +}); +``` + +## Custom Session Handling + +The ability to add custom session stores to the SDK using `appSession: false` has been removed but could be added back in a later release. diff --git a/codecov.yml b/codecov.yml index 710b8156..0272cedc 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ coverage: precision: 2 round: down - range: "80...100" + range: '80...100' status: project: default: @@ -19,4 +19,4 @@ coverage: default: enabled: true if_no_uploads: error -comment: false \ No newline at end of file +comment: false diff --git a/docs/assets/css/main.css b/docs/assets/css/main.css new file mode 100644 index 00000000..959edd73 --- /dev/null +++ b/docs/assets/css/main.css @@ -0,0 +1,2679 @@ +/*! normalize.css v1.1.3 | MIT License | git.io/normalize */ +/* ========================================================================== + * * HTML5 display definitions + * * ========================================================================== */ +/** + * * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { + display: block; +} + +/** + * * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. */ +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/** + * * Prevent modern browsers from displaying `audio` without controls. + * * Remove excess height in iOS 5 devices. */ +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. + * * Known issue: no IE 6 support. */ +[hidden] { + display: none; +} + +/* ========================================================================== + * * Base + * * ========================================================================== */ +/** + * * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using + * * `em` units. + * * 2. Prevent iOS text size adjust after orientation change, without disabling + * * user zoom. */ +html { + font-size: 100%; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + font-family: sans-serif; +} + +/** + * * Address `font-family` inconsistency between `textarea` and other form + * * elements. */ +button, input, select, textarea { + font-family: sans-serif; +} + +/** + * * Address margins handled incorrectly in IE 6/7. */ +body { + margin: 0; +} + +/* ========================================================================== + * * Links + * * ========================================================================== */ +/** + * * Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { + outline: thin dotted; +} +a:active, a:hover { + outline: 0; +} + +/** + * * Improve readability when focused and also mouse hovered in all browsers. */ +/* ========================================================================== + * * Typography + * * ========================================================================== */ +/** + * * Address font sizes and margins set differently in IE 6/7. + * * Address font sizes within `section` and `article` in Firefox 4+, Safari 5, + * * and Chrome. */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4, .tsd-index-panel h3 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; +} + +/** + * * Address styling not present in IE 7/8/9, Safari 5, and Chrome. */ +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. */ +b, strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/** + * * Address styling not present in Safari 5 and Chrome. */ +dfn { + font-style: italic; +} + +/** + * * Address differences between Firefox and other browsers. + * * Known issue: no IE 6/7 normalization. */ +hr { + box-sizing: content-box; + height: 0; +} + +/** + * * Address styling not present in IE 6/7/8/9. */ +mark { + background: #ff0; + color: #000; +} + +/** + * * Address margins set differently in IE 6/7. */ +p, pre { + margin: 1em 0; +} + +/** + * * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. */ +code, kbd, pre, samp { + font-family: monospace, serif; + _font-family: "courier new", monospace; + font-size: 1em; +} + +/** + * * Improve readability of pre-formatted text in all browsers. */ +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/** + * * Address CSS quotes not supported in IE 6/7. */ +q { + quotes: none; +} +q:before, q:after { + content: ""; + content: none; +} + +/** + * * Address `quotes` property not supported in Safari 4. */ +/** + * * Address inconsistent and variable font size in all browsers. */ +small { + font-size: 80%; +} + +/** + * * Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + * * Lists + * * ========================================================================== */ +/** + * * Address margins set differently in IE 6/7. */ +dl, menu, ol, ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/** + * * Address paddings set differently in IE 6/7. */ +menu, ol, ul { + padding: 0 0 0 40px; +} + +/** + * * Correct list images handled incorrectly in IE 7. */ +nav ul, nav ol { + list-style: none; + list-style-image: none; +} + +/* ========================================================================== + * * Embedded content + * * ========================================================================== */ +/** + * * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. + * * 2. Improve image quality when scaled in IE 7. */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; +} + +/* 2 */ +/** + * * Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + * * Figures + * * ========================================================================== */ +/** + * * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. */ +figure, form { + margin: 0; +} + +/* ========================================================================== + * * Forms + * * ========================================================================== */ +/** + * * Correct margin displayed oddly in IE 6/7. */ +/** + * * Define consistent border, margin, and padding. */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * * 1. Correct color not being inherited in IE 6/7/8/9. + * * 2. Correct text not wrapping in Firefox 3. + * * 3. Correct alignment displayed oddly in IE 6/7. */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; +} + +/* 3 */ +/** + * * 1. Correct font size not being inherited in all browsers. + * * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, + * * and Chrome. + * * 3. Improve appearance and consistency in all browsers. */ +button, input, select, textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; +} + +/* 3 */ +/** + * * Address Firefox 3+ setting `line-height` on `input` using `!important` in + * * the UA stylesheet. */ +button, input { + line-height: normal; +} + +/** + * * Address inconsistent `text-transform` inheritance for `button` and `select`. + * * All other form control elements do not inherit `text-transform` values. + * * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. + * * Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { + text-transform: none; +} + +/** + * * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * * and `video` controls. + * * 2. Correct inability to style clickable `input` types in iOS. + * * 3. Improve usability and consistency of cursor style between image-type + * * `input` and others. + * * 4. Remove inner spacing in IE 7 without affecting normal text inputs. + * * Known issue: inner spacing remains in IE 6. */ +button, html input[type=button] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ + *overflow: visible; +} + +/* 4 */ +input[type=reset], input[type=submit] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ + *overflow: visible; +} + +/* 4 */ +/** + * * Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { + cursor: default; +} + +/** + * * 1. Address box sizing set to content-box in IE 8/9. + * * 2. Remove excess padding in IE 8/9. + * * 3. Remove excess padding in IE 7. + * * Known issue: excess padding remains in IE 6. */ +input { + /* 3 */ +} +input[type=checkbox], input[type=radio] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; +} +input[type=search] { + -webkit-appearance: textfield; + /* 1 */ + /* 2 */ + box-sizing: content-box; +} +input[type=search]::-webkit-search-cancel-button, input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. + * * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome + * * (include `-moz` to future-proof). */ +/** + * * Remove inner padding and search cancel button in Safari 5 and Chrome + * * on OS X. */ +/** + * * Remove inner padding and border in Firefox 3+. */ +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * * 1. Remove default vertical scrollbar in IE 6/7/8/9. + * * 2. Improve readability and alignment in all browsers. */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; +} + +/* 2 */ +/* ========================================================================== + * * Tables + * * ========================================================================== */ +/** + * * Remove most spacing between table cells. */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* * + * *Visual Studio-like style based on original C# coloring by Jason Diamond */ +.hljs { + display: inline-block; + padding: 0.5em; + background: white; + color: black; +} + +.hljs-comment, .hljs-annotation, .hljs-template_comment, .diff .hljs-header, .hljs-chunk, .apache .hljs-cbracket { + color: #008000; +} + +.hljs-keyword, .hljs-id, .hljs-built_in, .css .smalltalk .hljs-class, .hljs-winutils, .bash .hljs-variable, .tex .hljs-command, .hljs-request, .hljs-status, .nginx .hljs-title { + color: #00f; +} + +.xml .hljs-tag { + color: #00f; +} +.xml .hljs-tag .hljs-value { + color: #00f; +} + +.hljs-string, .hljs-title, .hljs-parent, .hljs-tag .hljs-value, .hljs-rules .hljs-value { + color: #a31515; +} + +.ruby .hljs-symbol { + color: #a31515; +} +.ruby .hljs-symbol .hljs-string { + color: #a31515; +} + +.hljs-template_tag, .django .hljs-variable, .hljs-addition, .hljs-flow, .hljs-stream, .apache .hljs-tag, .hljs-date, .tex .hljs-formula, .coffeescript .hljs-attribute { + color: #a31515; +} + +.ruby .hljs-string, .hljs-decorator, .hljs-filter .hljs-argument, .hljs-localvars, .hljs-array, .hljs-attr_selector, .hljs-pseudo, .hljs-pi, .hljs-doctype, .hljs-deletion, .hljs-envvar, .hljs-shebang, .hljs-preprocessor, .hljs-pragma, .userType, .apache .hljs-sqbracket, .nginx .hljs-built_in, .tex .hljs-special, .hljs-prompt { + color: #2b91af; +} + +.hljs-phpdoc, .hljs-javadoc, .hljs-xmlDocTag { + color: #808080; +} + +.vhdl .hljs-typename { + font-weight: bold; +} +.vhdl .hljs-string { + color: #666666; +} +.vhdl .hljs-literal { + color: #a31515; +} +.vhdl .hljs-attribute { + color: #00b0e8; +} + +.xml .hljs-attribute { + color: #f00; +} + +ul.tsd-descriptions > li > :first-child, .tsd-panel > :first-child, .col > :first-child, .col-11 > :first-child, .col-10 > :first-child, .col-9 > :first-child, .col-8 > :first-child, .col-7 > :first-child, .col-6 > :first-child, .col-5 > :first-child, .col-4 > :first-child, .col-3 > :first-child, .col-2 > :first-child, .col-1 > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child, +.tsd-panel > :first-child > :first-child, +.col > :first-child > :first-child, +.col-11 > :first-child > :first-child, +.col-10 > :first-child > :first-child, +.col-9 > :first-child > :first-child, +.col-8 > :first-child > :first-child, +.col-7 > :first-child > :first-child, +.col-6 > :first-child > :first-child, +.col-5 > :first-child > :first-child, +.col-4 > :first-child > :first-child, +.col-3 > :first-child > :first-child, +.col-2 > :first-child > :first-child, +.col-1 > :first-child > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child > :first-child, +.tsd-panel > :first-child > :first-child > :first-child, +.col > :first-child > :first-child > :first-child, +.col-11 > :first-child > :first-child > :first-child, +.col-10 > :first-child > :first-child > :first-child, +.col-9 > :first-child > :first-child > :first-child, +.col-8 > :first-child > :first-child > :first-child, +.col-7 > :first-child > :first-child > :first-child, +.col-6 > :first-child > :first-child > :first-child, +.col-5 > :first-child > :first-child > :first-child, +.col-4 > :first-child > :first-child > :first-child, +.col-3 > :first-child > :first-child > :first-child, +.col-2 > :first-child > :first-child > :first-child, +.col-1 > :first-child > :first-child > :first-child { + margin-top: 0; +} +ul.tsd-descriptions > li > :last-child, .tsd-panel > :last-child, .col > :last-child, .col-11 > :last-child, .col-10 > :last-child, .col-9 > :last-child, .col-8 > :last-child, .col-7 > :last-child, .col-6 > :last-child, .col-5 > :last-child, .col-4 > :last-child, .col-3 > :last-child, .col-2 > :last-child, .col-1 > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child, +.tsd-panel > :last-child > :last-child, +.col > :last-child > :last-child, +.col-11 > :last-child > :last-child, +.col-10 > :last-child > :last-child, +.col-9 > :last-child > :last-child, +.col-8 > :last-child > :last-child, +.col-7 > :last-child > :last-child, +.col-6 > :last-child > :last-child, +.col-5 > :last-child > :last-child, +.col-4 > :last-child > :last-child, +.col-3 > :last-child > :last-child, +.col-2 > :last-child > :last-child, +.col-1 > :last-child > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child > :last-child, +.tsd-panel > :last-child > :last-child > :last-child, +.col > :last-child > :last-child > :last-child, +.col-11 > :last-child > :last-child > :last-child, +.col-10 > :last-child > :last-child > :last-child, +.col-9 > :last-child > :last-child > :last-child, +.col-8 > :last-child > :last-child > :last-child, +.col-7 > :last-child > :last-child > :last-child, +.col-6 > :last-child > :last-child > :last-child, +.col-5 > :last-child > :last-child > :last-child, +.col-4 > :last-child > :last-child > :last-child, +.col-3 > :last-child > :last-child > :last-child, +.col-2 > :last-child > :last-child > :last-child, +.col-1 > :last-child > :last-child > :last-child { + margin-bottom: 0; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 40px; +} +@media (max-width: 640px) { + .container { + padding: 0 20px; + } +} + +.container-main { + padding-bottom: 200px; +} + +.row { + display: -ms-flexbox; + display: flex; + position: relative; + margin: 0 -10px; +} +.row:after { + visibility: hidden; + display: block; + content: ""; + clear: both; + height: 0; +} + +.col, .col-11, .col-10, .col-9, .col-8, .col-7, .col-6, .col-5, .col-4, .col-3, .col-2, .col-1 { + box-sizing: border-box; + float: left; + padding: 0 10px; +} + +.col-1 { + width: 8.3333333333%; +} + +.offset-1 { + margin-left: 8.3333333333%; +} + +.col-2 { + width: 16.6666666667%; +} + +.offset-2 { + margin-left: 16.6666666667%; +} + +.col-3 { + width: 25%; +} + +.offset-3 { + margin-left: 25%; +} + +.col-4 { + width: 33.3333333333%; +} + +.offset-4 { + margin-left: 33.3333333333%; +} + +.col-5 { + width: 41.6666666667%; +} + +.offset-5 { + margin-left: 41.6666666667%; +} + +.col-6 { + width: 50%; +} + +.offset-6 { + margin-left: 50%; +} + +.col-7 { + width: 58.3333333333%; +} + +.offset-7 { + margin-left: 58.3333333333%; +} + +.col-8 { + width: 66.6666666667%; +} + +.offset-8 { + margin-left: 66.6666666667%; +} + +.col-9 { + width: 75%; +} + +.offset-9 { + margin-left: 75%; +} + +.col-10 { + width: 83.3333333333%; +} + +.offset-10 { + margin-left: 83.3333333333%; +} + +.col-11 { + width: 91.6666666667%; +} + +.offset-11 { + margin-left: 91.6666666667%; +} + +.tsd-kind-icon { + display: block; + position: relative; + padding-left: 20px; + text-indent: -20px; +} +.tsd-kind-icon:before { + content: ""; + display: inline-block; + vertical-align: middle; + width: 17px; + height: 17px; + margin: 0 3px 2px 0; + background-image: url(../images/icons.png); +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-kind-icon:before { + background-image: url(../images/icons@2x.png); + background-size: 238px 204px; + } +} + +.tsd-signature.tsd-kind-icon:before { + background-position: 0 -153px; +} + +.tsd-kind-object-literal > .tsd-kind-icon:before { + background-position: 0px -17px; +} +.tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -17px; +} +.tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -17px; +} + +.tsd-kind-class > .tsd-kind-icon:before { + background-position: 0px -34px; +} +.tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -34px; +} +.tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -34px; +} + +.tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -51px; +} + +.tsd-kind-interface > .tsd-kind-icon:before { + background-position: 0px -68px; +} +.tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -68px; +} +.tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -68px; +} + +.tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -85px; +} + +.tsd-kind-namespace > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-module > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-enum > .tsd-kind-icon:before { + background-position: 0px -119px; +} +.tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -119px; +} +.tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -119px; +} + +.tsd-kind-enum-member > .tsd-kind-icon:before { + background-position: 0px -136px; +} +.tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -136px; +} +.tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -136px; +} + +.tsd-kind-signature > .tsd-kind-icon:before { + background-position: 0px -153px; +} +.tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -153px; +} +.tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -153px; +} + +.tsd-kind-type-alias > .tsd-kind-icon:before { + background-position: 0px -170px; +} +.tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -170px; +} +.tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -170px; +} + +.tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -187px; +} + +.tsd-kind-variable > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-property > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-get-signature > .tsd-kind-icon:before { + background-position: -136px -17px; +} +.tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -17px; +} +.tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -17px; +} + +.tsd-kind-set-signature > .tsd-kind-icon:before { + background-position: -136px -34px; +} +.tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -34px; +} +.tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -34px; +} + +.tsd-kind-accessor > .tsd-kind-icon:before { + background-position: -136px -51px; +} +.tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -51px; +} +.tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -51px; +} + +.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-constructor > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-constructor-signature > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-index-signature > .tsd-kind-icon:before { + background-position: -136px -119px; +} +.tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -119px; +} +.tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -119px; +} + +.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -136px; +} +.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -136px; +} +.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -136px; +} + +.tsd-is-static > .tsd-kind-icon:before { + background-position: -136px -153px; +} +.tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -153px; +} +.tsd-is-static.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -153px; +} +.tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -153px; +} + +.tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -187px; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } +} +@keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } +} +@keyframes shift-to-left { + from { + transform: translate(0, 0); + } + to { + transform: translate(-25%, 0); + } +} +@keyframes unshift-to-left { + from { + transform: translate(-25%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } +} +body { + background: #fdfdfd; + font-family: "Segoe UI", sans-serif; + font-size: 16px; + color: #222; +} + +a { + color: #4da6ff; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +code, pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 14px; + background-color: rgba(0, 0, 0, 0.04); +} + +pre { + padding: 10px; +} +pre code { + padding: 0; + font-size: 100%; + background-color: transparent; +} + +.tsd-typography { + line-height: 1.333em; +} +.tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-typography h4, .tsd-typography .tsd-index-panel h3, .tsd-index-panel .tsd-typography h3, .tsd-typography h5, .tsd-typography h6 { + font-size: 1em; + margin: 0; +} +.tsd-typography h5, .tsd-typography h6 { + font-weight: normal; +} +.tsd-typography p, .tsd-typography ul, .tsd-typography ol { + margin: 1em 0; +} + +@media (min-width: 901px) and (max-width: 1024px) { + html.default .col-content { + width: 72%; + } + html.default .col-menu { + width: 28%; + } + html.default .tsd-navigation { + padding-left: 10px; + } +} +@media (max-width: 900px) { + html.default .col-content { + float: none; + width: 100%; + } + html.default .col-menu { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + width: 100%; + padding: 20px 20px 0 0; + max-width: 450px; + visibility: hidden; + background-color: #fff; + transform: translate(100%, 0); + } + html.default .col-menu > *:last-child { + padding-bottom: 20px; + } + html.default .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + html.default.to-has-menu .overlay { + animation: fade-in 0.4s; + } + html.default.to-has-menu header, +html.default.to-has-menu footer, +html.default.to-has-menu .col-content { + animation: shift-to-left 0.4s; + } + html.default.to-has-menu .col-menu { + animation: pop-in-from-right 0.4s; + } + html.default.from-has-menu .overlay { + animation: fade-out 0.4s; + } + html.default.from-has-menu header, +html.default.from-has-menu footer, +html.default.from-has-menu .col-content { + animation: unshift-to-left 0.4s; + } + html.default.from-has-menu .col-menu { + animation: pop-out-to-right 0.4s; + } + html.default.has-menu body { + overflow: hidden; + } + html.default.has-menu .overlay { + visibility: visible; + } + html.default.has-menu header, +html.default.has-menu footer, +html.default.has-menu .col-content { + transform: translate(-25%, 0); + } + html.default.has-menu .col-menu { + visibility: visible; + transform: translate(0, 0); + } +} + +.tsd-page-title { + padding: 70px 0 20px 0; + margin: 0 0 40px 0; + background: #fff; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.35); +} +.tsd-page-title h1 { + margin: 0; +} + +.tsd-breadcrumb { + margin: 0; + padding: 0; + color: #808080; +} +.tsd-breadcrumb a { + color: #808080; + text-decoration: none; +} +.tsd-breadcrumb a:hover { + text-decoration: underline; +} +.tsd-breadcrumb li { + display: inline; +} +.tsd-breadcrumb li:after { + content: " / "; +} + +html.minimal .container { + margin: 0; +} +html.minimal .container-main { + padding-top: 50px; + padding-bottom: 0; +} +html.minimal .content-wrap { + padding-left: 300px; +} +html.minimal .tsd-navigation { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + z-index: 1; + left: 0; + top: 40px; + bottom: 0; + width: 300px; + padding: 20px; + margin: 0; +} +html.minimal .tsd-member .tsd-member { + margin-left: 0; +} +html.minimal .tsd-page-toolbar { + position: fixed; + z-index: 2; +} +html.minimal #tsd-filter .tsd-filter-group { + right: 0; + transform: none; +} +html.minimal footer { + background-color: transparent; +} +html.minimal footer .container { + padding: 0; +} +html.minimal .tsd-generator { + padding: 0; +} +@media (max-width: 900px) { + html.minimal .tsd-navigation { + display: none; + } + html.minimal .content-wrap { + padding-left: 0; + } +} + +dl.tsd-comment-tags { + overflow: hidden; +} +dl.tsd-comment-tags dt { + float: left; + padding: 1px 5px; + margin: 0 10px 0 0; + border-radius: 4px; + border: 1px solid #808080; + color: #808080; + font-size: 0.8em; + font-weight: normal; +} +dl.tsd-comment-tags dd { + margin: 0 0 10px 0; +} +dl.tsd-comment-tags dd:before, dl.tsd-comment-tags dd:after { + display: table; + content: " "; +} +dl.tsd-comment-tags dd pre, dl.tsd-comment-tags dd:after { + clear: both; +} +dl.tsd-comment-tags p { + margin: 0; +} + +.tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; +} +.tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; +} + +.toggle-protected .tsd-is-private { + display: none; +} + +.toggle-public .tsd-is-private, +.toggle-public .tsd-is-protected, +.toggle-public .tsd-is-private-protected { + display: none; +} + +.toggle-inherited .tsd-is-inherited { + display: none; +} + +.toggle-only-exported .tsd-is-not-exported { + display: none; +} + +.toggle-externals .tsd-is-external { + display: none; +} + +#tsd-filter { + position: relative; + display: inline-block; + height: 40px; + vertical-align: bottom; +} +.no-filter #tsd-filter { + display: none; +} +#tsd-filter .tsd-filter-group { + display: inline-block; + height: 40px; + vertical-align: bottom; + white-space: nowrap; +} +#tsd-filter input { + display: none; +} +@media (max-width: 900px) { + #tsd-filter .tsd-filter-group { + display: block; + position: absolute; + top: 40px; + right: 20px; + height: auto; + background-color: #fff; + visibility: hidden; + transform: translate(50%, 0); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + } + .has-options #tsd-filter .tsd-filter-group { + visibility: visible; + } + .to-has-options #tsd-filter .tsd-filter-group { + animation: fade-in 0.2s; + } + .from-has-options #tsd-filter .tsd-filter-group { + animation: fade-out 0.2s; + } + #tsd-filter label, +#tsd-filter .tsd-select { + display: block; + padding-right: 20px; + } +} + +footer { + border-top: 1px solid #eee; + background-color: #fff; +} +footer.with-border-bottom { + border-bottom: 1px solid #eee; +} +footer .tsd-legend-group { + font-size: 0; +} +footer .tsd-legend { + display: inline-block; + width: 25%; + padding: 0; + font-size: 16px; + list-style: none; + line-height: 1.333em; + vertical-align: top; +} +@media (max-width: 900px) { + footer .tsd-legend { + width: 50%; + } +} + +.tsd-hierarchy { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-hierarchy .target { + font-weight: bold; +} + +.tsd-index-panel .tsd-index-content { + margin-bottom: -30px !important; +} +.tsd-index-panel .tsd-index-section { + margin-bottom: 30px !important; +} +.tsd-index-panel h3 { + margin: 0 -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid #eee; +} +.tsd-index-panel ul.tsd-index-list { + -moz-column-count: 3; + -ms-column-count: 3; + -o-column-count: 3; + column-count: 3; + -moz-column-gap: 20px; + -ms-column-gap: 20px; + -o-column-gap: 20px; + column-gap: 20px; + padding: 0; + list-style: none; + line-height: 1.333em; +} +@media (max-width: 900px) { + .tsd-index-panel ul.tsd-index-list { + -moz-column-count: 1; + -ms-column-count: 1; + -o-column-count: 1; + column-count: 1; + } +} +@media (min-width: 901px) and (max-width: 1024px) { + .tsd-index-panel ul.tsd-index-list { + -moz-column-count: 2; + -ms-column-count: 2; + -o-column-count: 2; + column-count: 2; + } +} +.tsd-index-panel ul.tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; +} +.tsd-index-panel a, +.tsd-index-panel .tsd-parent-kind-module a { + color: #9600ff; +} +.tsd-index-panel .tsd-parent-kind-interface a { + color: #7da01f; +} +.tsd-index-panel .tsd-parent-kind-enum a { + color: #cc9900; +} +.tsd-index-panel .tsd-parent-kind-class a { + color: #4da6ff; +} +.tsd-index-panel .tsd-kind-module a { + color: #9600ff; +} +.tsd-index-panel .tsd-kind-interface a { + color: #7da01f; +} +.tsd-index-panel .tsd-kind-enum a { + color: #cc9900; +} +.tsd-index-panel .tsd-kind-class a { + color: #4da6ff; +} +.tsd-index-panel .tsd-is-private a { + color: #808080; +} + +.tsd-flag { + display: inline-block; + padding: 1px 5px; + border-radius: 4px; + color: #fff; + background-color: #808080; + text-indent: 0; + font-size: 14px; + font-weight: normal; +} + +.tsd-anchor { + position: absolute; + top: -100px; +} + +.tsd-member { + position: relative; +} +.tsd-member .tsd-anchor + h3 { + margin-top: 0; + margin-bottom: 0; + border-bottom: none; +} + +.tsd-navigation { + margin: 0 0 0 40px; +} +.tsd-navigation a { + display: block; + padding-top: 2px; + padding-bottom: 2px; + border-left: 2px solid transparent; + color: #222; + text-decoration: none; + transition: border-left-color 0.1s; +} +.tsd-navigation a:hover { + text-decoration: underline; +} +.tsd-navigation ul { + margin: 0; + padding: 0; + list-style: none; +} +.tsd-navigation li { + padding: 0; +} + +.tsd-navigation.primary { + padding-bottom: 40px; +} +.tsd-navigation.primary a { + display: block; + padding-top: 6px; + padding-bottom: 6px; +} +.tsd-navigation.primary ul li a { + padding-left: 5px; +} +.tsd-navigation.primary ul li li a { + padding-left: 25px; +} +.tsd-navigation.primary ul li li li a { + padding-left: 45px; +} +.tsd-navigation.primary ul li li li li a { + padding-left: 65px; +} +.tsd-navigation.primary ul li li li li li a { + padding-left: 85px; +} +.tsd-navigation.primary ul li li li li li li a { + padding-left: 105px; +} +.tsd-navigation.primary > ul { + border-bottom: 1px solid #eee; +} +.tsd-navigation.primary li { + border-top: 1px solid #eee; +} +.tsd-navigation.primary li.current > a { + font-weight: bold; +} +.tsd-navigation.primary li.label span { + display: block; + padding: 20px 0 6px 5px; + color: #808080; +} +.tsd-navigation.primary li.globals + li > span, .tsd-navigation.primary li.globals + li > a { + padding-top: 20px; +} + +.tsd-navigation.secondary { + max-height: calc(100vh - 1rem - 40px); + overflow: auto; + position: -webkit-sticky; + position: sticky; + top: calc(.5rem + 40px); + transition: 0.3s; +} +.tsd-navigation.secondary.tsd-navigation--toolbar-hide { + max-height: calc(100vh - 1rem); + top: 0.5rem; +} +.tsd-navigation.secondary ul { + transition: opacity 0.2s; +} +.tsd-navigation.secondary ul li a { + padding-left: 25px; +} +.tsd-navigation.secondary ul li li a { + padding-left: 45px; +} +.tsd-navigation.secondary ul li li li a { + padding-left: 65px; +} +.tsd-navigation.secondary ul li li li li a { + padding-left: 85px; +} +.tsd-navigation.secondary ul li li li li li a { + padding-left: 105px; +} +.tsd-navigation.secondary ul li li li li li li a { + padding-left: 125px; +} +.tsd-navigation.secondary ul.current a { + border-left-color: #eee; +} +.tsd-navigation.secondary li.focus > a, +.tsd-navigation.secondary ul.current li.focus > a { + border-left-color: #000; +} +.tsd-navigation.secondary li.current { + margin-top: 20px; + margin-bottom: 20px; + border-left-color: #eee; +} +.tsd-navigation.secondary li.current > a { + font-weight: bold; +} + +@media (min-width: 901px) { + .menu-sticky-wrap { + position: static; + } +} + +.tsd-panel { + margin: 20px 0; + padding: 20px; + background-color: #fff; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +.tsd-panel:empty { + display: none; +} +.tsd-panel > h1, .tsd-panel > h2, .tsd-panel > h3 { + margin: 1.5em -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid #eee; +} +.tsd-panel > h1.tsd-before-signature, .tsd-panel > h2.tsd-before-signature, .tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: 0; +} +.tsd-panel table { + display: block; + width: 100%; + overflow: auto; + margin-top: 10px; + word-break: normal; + word-break: keep-all; +} +.tsd-panel table th { + font-weight: bold; +} +.tsd-panel table th, .tsd-panel table td { + padding: 6px 13px; + border: 1px solid #ddd; +} +.tsd-panel table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} +.tsd-panel table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +.tsd-panel-group { + margin: 60px 0; +} +.tsd-panel-group > h1, .tsd-panel-group > h2, .tsd-panel-group > h3 { + padding-left: 20px; + padding-right: 20px; +} + +#tsd-search { + transition: background-color 0.2s; +} +#tsd-search .title { + position: relative; + z-index: 2; +} +#tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 40px; + height: 40px; +} +#tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: #222; +} +#tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; +} +#tsd-search .field input, +#tsd-search .title { + transition: opacity 0.2s; +} +#tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +#tsd-search .results li { + padding: 0 10px; + background-color: #fdfdfd; +} +#tsd-search .results li:nth-child(even) { + background-color: #fff; +} +#tsd-search .results li.state { + display: none; +} +#tsd-search .results li.current, +#tsd-search .results li:hover { + background-color: #eee; +} +#tsd-search .results a { + display: block; +} +#tsd-search .results a:before { + top: 10px; +} +#tsd-search .results span.parent { + color: #808080; + font-weight: normal; +} +#tsd-search.has-focus { + background-color: #eee; +} +#tsd-search.has-focus .field input { + top: 0; + opacity: 1; +} +#tsd-search.has-focus .title { + z-index: 0; + opacity: 0; +} +#tsd-search.has-focus .results { + visibility: visible; +} +#tsd-search.loading .results li.state.loading { + display: block; +} +#tsd-search.failure .results li.state.failure { + display: block; +} + +.tsd-signature { + margin: 0 0 1em 0; + padding: 10px; + border: 1px solid #eee; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; +} +.tsd-signature.tsd-kind-icon { + padding-left: 30px; +} +.tsd-signature.tsd-kind-icon:before { + top: 10px; + left: 10px; +} +.tsd-panel > .tsd-signature { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signature.tsd-kind-icon:before { + left: 20px; +} + +.tsd-signature-symbol { + color: #808080; + font-weight: normal; +} + +.tsd-signature-type { + font-style: italic; + font-weight: normal; +} + +.tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + border: 1px solid #eee; +} +.tsd-signatures .tsd-signature { + margin: 0; + border-width: 1px 0 0 0; + transition: background-color 0.1s; +} +.tsd-signatures .tsd-signature:first-child { + border-top-width: 0; +} +.tsd-signatures .tsd-signature.current { + background-color: #eee; +} +.tsd-signatures.active > .tsd-signature { + cursor: pointer; +} +.tsd-panel > .tsd-signatures { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon:before { + left: 20px; +} +.tsd-panel > a.anchor + .tsd-signatures { + border-top-width: 0; + margin-top: -20px; +} + +ul.tsd-descriptions { + position: relative; + overflow: hidden; + padding: 0; + list-style: none; +} +ul.tsd-descriptions.active > .tsd-description { + display: none; +} +ul.tsd-descriptions.active > .tsd-description.current { + display: block; +} +ul.tsd-descriptions.active > .tsd-description.fade-in { + animation: fade-in-delayed 0.3s; +} +ul.tsd-descriptions.active > .tsd-description.fade-out { + animation: fade-out-delayed 0.3s; + position: absolute; + display: block; + top: 0; + left: 0; + right: 0; + opacity: 0; + visibility: hidden; +} +ul.tsd-descriptions h4, ul.tsd-descriptions .tsd-index-panel h3, .tsd-index-panel ul.tsd-descriptions h3 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} + +ul.tsd-parameters, +ul.tsd-type-parameters { + list-style: square; + margin: 0; + padding-left: 20px; +} +ul.tsd-parameters > li.tsd-parameter-signature, +ul.tsd-type-parameters > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; +} +ul.tsd-parameters h5, +ul.tsd-type-parameters h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} +ul.tsd-parameters .tsd-comment, +ul.tsd-type-parameters .tsd-comment { + margin-top: -0.5em; +} + +.tsd-sources { + font-size: 14px; + color: #808080; + margin: 0 0 1em 0; +} +.tsd-sources a { + color: #808080; + text-decoration: underline; +} +.tsd-sources ul, .tsd-sources p { + margin: 0 !important; +} +.tsd-sources ul { + list-style: none; + padding: 0; +} + +.tsd-page-toolbar { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 40px; + color: #333; + background: #fff; + border-bottom: 1px solid #eee; + transition: transform 0.3s linear; +} +.tsd-page-toolbar a { + color: #333; + text-decoration: none; +} +.tsd-page-toolbar a.title { + font-weight: bold; +} +.tsd-page-toolbar a.title:hover { + text-decoration: underline; +} +.tsd-page-toolbar .table-wrap { + display: table; + width: 100%; + height: 40px; +} +.tsd-page-toolbar .table-cell { + display: table-cell; + position: relative; + white-space: nowrap; + line-height: 40px; +} +.tsd-page-toolbar .table-cell:first-child { + width: 100%; +} + +.tsd-page-toolbar--hide { + transform: translateY(-100%); +} + +.tsd-select .tsd-select-list li:before, .tsd-select .tsd-select-label:before, .tsd-widget:before { + content: ""; + display: inline-block; + width: 40px; + height: 40px; + margin: 0 -8px 0 0; + background-image: url(../images/widgets.png); + background-repeat: no-repeat; + text-indent: -1024px; + vertical-align: bottom; +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-select .tsd-select-list li:before, .tsd-select .tsd-select-label:before, .tsd-widget:before { + background-image: url(../images/widgets@2x.png); + background-size: 320px 40px; + } +} + +.tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.6; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-widget:hover { + opacity: 0.8; +} +.tsd-widget.active { + opacity: 1; + background-color: #eee; +} +.tsd-widget.no-caption { + width: 40px; +} +.tsd-widget.no-caption:before { + margin: 0; +} +.tsd-widget.search:before { + background-position: 0 0; +} +.tsd-widget.menu:before { + background-position: -40px 0; +} +.tsd-widget.options:before { + background-position: -80px 0; +} +.tsd-widget.options, .tsd-widget.menu { + display: none; +} +@media (max-width: 900px) { + .tsd-widget.options, .tsd-widget.menu { + display: inline-block; + } +} +input[type=checkbox] + .tsd-widget:before { + background-position: -120px 0; +} +input[type=checkbox]:checked + .tsd-widget:before { + background-position: -160px 0; +} + +.tsd-select { + position: relative; + display: inline-block; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-select .tsd-select-label { + opacity: 0.6; + transition: opacity 0.2s; +} +.tsd-select .tsd-select-label:before { + background-position: -240px 0; +} +.tsd-select.active .tsd-select-label { + opacity: 0.8; +} +.tsd-select.active .tsd-select-list { + visibility: visible; + opacity: 1; + transition-delay: 0s; +} +.tsd-select .tsd-select-list { + position: absolute; + visibility: hidden; + top: 40px; + left: 0; + margin: 0; + padding: 0; + opacity: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + transition: visibility 0s 0.2s, opacity 0.2s; +} +.tsd-select .tsd-select-list li { + padding: 0 20px 0 0; + background-color: #fdfdfd; +} +.tsd-select .tsd-select-list li:before { + background-position: 40px 0; +} +.tsd-select .tsd-select-list li:nth-child(even) { + background-color: #fff; +} +.tsd-select .tsd-select-list li:hover { + background-color: #eee; +} +.tsd-select .tsd-select-list li.selected:before { + background-position: -200px 0; +} +@media (max-width: 900px) { + .tsd-select .tsd-select-list { + top: 0; + left: auto; + right: 100%; + margin-right: -5px; + } + .tsd-select .tsd-select-label:before { + background-position: -280px 0; + } +} + +img { + max-width: 100%; +} \ No newline at end of file diff --git a/docs/assets/images/icons.png b/docs/assets/images/icons.png new file mode 100644 index 00000000..3836d5fe Binary files /dev/null and b/docs/assets/images/icons.png differ diff --git a/docs/assets/images/icons@2x.png b/docs/assets/images/icons@2x.png new file mode 100644 index 00000000..5a209e2f Binary files /dev/null and b/docs/assets/images/icons@2x.png differ diff --git a/docs/assets/images/widgets.png b/docs/assets/images/widgets.png new file mode 100644 index 00000000..c7380532 Binary files /dev/null and b/docs/assets/images/widgets.png differ diff --git a/docs/assets/images/widgets@2x.png b/docs/assets/images/widgets@2x.png new file mode 100644 index 00000000..4bbbd572 Binary files /dev/null and b/docs/assets/images/widgets@2x.png differ diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js new file mode 100644 index 00000000..39a80669 --- /dev/null +++ b/docs/assets/js/main.js @@ -0,0 +1 @@ +!function(){var e=function(t){var r=new e.Builder;return r.pipeline.add(e.trimmer,e.stopWordFilter,e.stemmer),r.searchPipeline.add(e.stemmer),t.call(r,r),r.build()};e.version="2.3.7",e.utils={},e.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),e.utils.asString=function(e){return null==e?"":e.toString()},e.utils.clone=function(e){if(null==e)return e;for(var t=Object.create(null),r=Object.keys(e),i=0;i=this.length)return e.QueryLexer.EOS;var t=this.str.charAt(this.pos);return this.pos+=1,t},e.QueryLexer.prototype.width=function(){return this.pos-this.start},e.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},e.QueryLexer.prototype.backup=function(){this.pos-=1},e.QueryLexer.prototype.acceptDigitRun=function(){for(var t,r;47<(r=(t=this.next()).charCodeAt(0))&&r<58;);t!=e.QueryLexer.EOS&&this.backup()},e.QueryLexer.prototype.more=function(){return this.pos=this.scrollTop||0===this.scrollTop,isShown!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),this.secondaryNav.classList.toggle("tsd-navigation--toolbar-hide")),this.lastY=this.scrollTop},Viewport}(typedoc.EventTarget);typedoc.Viewport=Viewport,typedoc.registerService(Viewport,"viewport")}(typedoc||(typedoc={})),function(typedoc){function Component(options){this.el=options.el}typedoc.Component=Component}(typedoc||(typedoc={})),function(typedoc){typedoc.pointerDown="mousedown",typedoc.pointerMove="mousemove",typedoc.pointerUp="mouseup",typedoc.pointerDownPosition={x:0,y:0},typedoc.preventNextClick=!1,typedoc.isPointerDown=!1,typedoc.isPointerTouch=!1,typedoc.hasPointerMoved=!1,typedoc.isMobile=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),document.documentElement.classList.add(typedoc.isMobile?"is-mobile":"not-mobile"),typedoc.isMobile&&"ontouchstart"in document.documentElement&&(typedoc.isPointerTouch=!0,typedoc.pointerDown="touchstart",typedoc.pointerMove="touchmove",typedoc.pointerUp="touchend"),document.addEventListener(typedoc.pointerDown,function(e){typedoc.isPointerDown=!0,typedoc.hasPointerMoved=!1;var t="touchstart"==typedoc.pointerDown?e.targetTouches[0]:e;typedoc.pointerDownPosition.y=t.pageY||0,typedoc.pointerDownPosition.x=t.pageX||0}),document.addEventListener(typedoc.pointerMove,function(e){if(typedoc.isPointerDown&&!typedoc.hasPointerMoved){var t="touchstart"==typedoc.pointerDown?e.targetTouches[0]:e,x=typedoc.pointerDownPosition.x-(t.pageX||0),y=typedoc.pointerDownPosition.y-(t.pageY||0);typedoc.hasPointerMoved=10scrollTop;)index-=1;for(;index"+match+""}),parent=row.parent||"";(parent=parent.replace(new RegExp(this.query,"i"),function(match){return""+match+""}))&&(name=''+parent+"."+name);var item=document.createElement("li");item.classList.value=row.classes,item.innerHTML='\n '+name+"\n ",this.results.appendChild(item)}}},Search.prototype.setLoadingState=function(value){this.loadingState!=value&&(this.el.classList.remove(SearchLoadingState[this.loadingState].toLowerCase()),this.loadingState=value,this.el.classList.add(SearchLoadingState[this.loadingState].toLowerCase()),this.updateResults())},Search.prototype.setHasFocus=function(value){this.hasFocus!=value&&(this.hasFocus=value,this.el.classList.toggle("has-focus"),value?(this.setQuery(""),this.field.value=""):this.field.value=this.query)},Search.prototype.setQuery=function(value){this.query=value.trim(),this.updateResults()},Search.prototype.setCurrentResult=function(dir){var current=this.results.querySelector(".current");if(current){var rel=1==dir?current.nextElementSibling:current.previousElementSibling;rel&&(current.classList.remove("current"),rel.classList.add("current"))}else(current=this.results.querySelector(1==dir?"li:first-child":"li:last-child"))&¤t.classList.add("current")},Search.prototype.gotoCurrentResult=function(){var current=this.results.querySelector(".current");if(current||(current=this.results.querySelector("li:first-child")),current){var link=current.querySelector("a");link&&(window.location.href=link.href),this.field.blur()}},Search.prototype.bindEvents=function(){var _this=this;this.results.addEventListener("mousedown",function(){_this.resultClicked=!0}),this.results.addEventListener("mouseup",function(){_this.resultClicked=!1,_this.setHasFocus(!1)}),this.field.addEventListener("focusin",function(){_this.setHasFocus(!0),_this.loadIndex()}),this.field.addEventListener("focusout",function(){_this.resultClicked?_this.resultClicked=!1:setTimeout(function(){return _this.setHasFocus(!1)},100)}),this.field.addEventListener("input",function(){_this.setQuery(_this.field.value)}),this.field.addEventListener("keydown",function(e){13==e.keyCode||27==e.keyCode||38==e.keyCode||40==e.keyCode?(_this.preventPress=!0,e.preventDefault(),13==e.keyCode?_this.gotoCurrentResult():27==e.keyCode?_this.field.blur():38==e.keyCode?_this.setCurrentResult(-1):40==e.keyCode&&_this.setCurrentResult(1)):_this.preventPress=!1}),this.field.addEventListener("keypress",function(e){_this.preventPress&&e.preventDefault()}),document.body.addEventListener("keydown",function(e){e.altKey||e.ctrlKey||e.metaKey||!_this.hasFocus&&47this.groups.length-1&&(index=this.groups.length-1),this.index!=index){var to=this.groups[index];if(-1 + + + + + express-openid-connect | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+ +

Express OpenID Connect

+
+

Express JS middleware implementing sign on for Express web apps using OpenID Connect.

+

CircleCI + codecov + NPM version

+ +

Table of Contents

+
+ + +

Documentation

+
+ + +

Install

+
+

Node.js version >=12.0.0 is recommended, but ^10.13.0 lts/dubnium is also supported.

+
npm install express-openid-connect
+ +

Getting Started

+
+

Follow our Secure Local Development guide to ensure that applications using this library are running over secure channels (HTTPS URLs). Applications using this library without HTTPS may experience "invalid state" errors.

+

The library needs issuerBaseURL, baseURL, clientID and secret to request and accept authentication. These can be configured with environmental variables:

+
ISSUER_BASE_URL=https://YOUR_DOMAIN
+CLIENT_ID=YOUR_CLIENT_ID
+BASE_URL=https://YOUR_APPLICATION_ROOT_URL
+SECRET=LONG_RANDOM_VALUE
+

... or in the library initialization:

+
// index.js
+
+const { auth } = require('express-openid-connect');
+app.use(
+  auth({
+    issuerBaseURL: 'https://YOUR_DOMAIN',
+    baseURL: 'https://YOUR_APPLICATION_ROOT_URL',
+    clientID: 'YOUR_CLIENT_ID',
+    secret: 'LONG_RANDOM_STRING',
+  })
+);
+

With this basic configuration, your application will require authentication for all routes and store the user identity in an encrypted and signed cookie.

+

See the examples for route-specific authentication, custom application session handling, requesting and using access tokens for external APIs, and more.

+

See the API documentation for additional configuration possibilities and provided methods.

+ +

A note on error handling

+
+

Errors raised by this library are handled by the default Express error handler which, in the interests of security, does not include the stack trace in the production environment.

+

But you may want to go one step further and hide additional error details from client, like the error message. To do this see the Express documentation on writing Custom error handlers

+ +

Contributing

+
+

We appreciate feedback and contribution to this repo! Before you get started, please see the following:

+ +

Contributions can be made to this library through PRs to fix issues, improve documentation or add features. Please fork this repo, create a well-named branch, and submit a PR with a complete template filled out.

+

Code changes in PRs should be accompanied by tests covering the changed or added functionality. Tests can be run for this library with:

+
npm install
+npm test
+

When you're ready to push your changes, please run the lint command first:

+
npm run lint
+ +

Support + Feedback

+
+

Please use the Issues queue in this repo for questions and feedback.

+ +

Vulnerability Reporting

+
+

Please do not report security vulnerabilities on the public GitHub issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.

+ +

What is Auth0?

+
+

Auth0 helps you to easily:

+
    +
  • implement authentication with multiple identity providers, including social (e.g., Google, Facebook, Microsoft, LinkedIn, GitHub, Twitter, etc), or enterprise (e.g., Windows Azure AD, Google Apps, Active Directory, ADFS, SAML, etc.)
  • +
  • log in users with username/password databases, passwordless, or multi-factor authentication
  • +
  • link multiple user accounts together
  • +
  • generate signed JSON Web Tokens to authorize your API calls and flow the user identity securely
  • +
  • access demographics and analytics detailing how, when, and where users are logging in
  • +
  • enrich user profiles from other data sources using customizable JavaScript rules
  • +
+

Why Auth0?

+ +

License

+
+

This project is licensed under the MIT license. See the LICENSE file for more info.

+
+
+

Index

+
+ +
+
+
+

Functions

+
+ +

attemptSilentLogin

+
    +
  • attemptSilentLogin(): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to attempt silent login (prompt=none) but not require authentication.

    +
    +

    See attemptSilentLogin

    +
    const { attemptSilentLogin } = require('express-openid-connect');
    +
    +app.get('/', attemptSilentLogin(), (req, res) => {
    +  res.render('homepage', {
    +    isAuthenticated: req.isAuthenticated() // show a login or logout button
    +  });
    +});
    +
    +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

auth

+ +
    +
  • + +
    +
    +

    Express JS middleware implementing sign on for Express web apps using OpenID Connect.

    +
    +

    The auth() middleware requires secret, baseURL, clientID + and issuerBaseURL.

    +

    If you are using a response type that includes code, you will also need: 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'))
    +
    +

    Parameters

    + +

    Returns RequestHandler

    +
  • +
+
+
+ +

claimCheck

+
    +
  • claimCheck(checkFn: (req: OpenidRequest, claims: IdTokenClaims) => boolean): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to protect a route, providing a custom function to check.

    +
    +
    const { claimCheck } = require('express-openid-connect');
    +
    +app.get('/admin/community', claimCheck((req, claims) => {
    +  return claims.isAdmin && claims.roles.includes('community');
    +}), (req, res) => {
    +  res.send(...);
    +});
    +
    +
    +

    Parameters

    +
      +
    • +
      checkFn: (req: OpenidRequest, claims: IdTokenClaims) => boolean
      +
        +
      • + +
          +
        • +

          Parameters

          + +

          Returns boolean

          +
        • +
        +
      • +
      +
    • +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

claimEquals

+
    +
  • claimEquals(claim: string, value: boolean | number | string | null): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to protect a route based on the value of a specific claim.

    +
    +
    const { claimEquals } = require('express-openid-connect');
    +
    +app.get('/admin', claimEquals('isAdmin', true), (req, res) => {
    +  res.send(...);
    +});
    +
    +
    +

    Parameters

    +
      +
    • +
      claim: string
      +
      +

      The name of the claim

      +
      +
    • +
    • +
      value: boolean | number | string | null
      +
      +

      The value of the claim, should be a primitive

      +
      +
    • +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

claimIncludes

+
    +
  • claimIncludes(claim: string, ...args: (string | number | false | true)[]): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to protect a route, checking that all values are in a claim.

    +
    +
    const { claimIncludes } = require('express-openid-connect');
    +
    +app.get('/admin/delete', claimIncludes('roles', 'admin', 'superadmin'), (req, res) => {
    +  res.send(...);
    +});
    +
    +
    +

    Parameters

    +
      +
    • +
      claim: string
      +
      +

      The name of the claim

      +
      +
    • +
    • +
      Rest ...args: (string | number | false | true)[]
      +
      +

      Claim values that must all be included

      +
      +
    • +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

requiresAuth

+
    +
  • requiresAuth(requiresLoginCheck?: (req: OpenidRequest) => boolean): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Set authRequired to false then require authentication + on specific routes.

    +
    +
    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}`);
    +});
    +
    +
    +

    Parameters

    + +

    Returns RequestHandler

    +
  • +
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..2dcf244f --- /dev/null +++ b/docs/index.html @@ -0,0 +1,3215 @@ + + + + + + express-openid-connect | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+ +

Express OpenID Connect

+
+

Express JS middleware implementing sign on for Express web apps using OpenID Connect.

+

CircleCI + codecov + NPM version

+ +

Table of Contents

+
+ + +

Documentation

+
+ + +

Install

+
+

Node.js version >=12.0.0 is recommended, but ^10.13.0 lts/dubnium is also supported.

+
npm install express-openid-connect
+ +

Getting Started

+
+

Follow our Secure Local Development guide to ensure that applications using this library are running over secure channels (HTTPS URLs). Applications using this library without HTTPS may experience "invalid state" errors.

+

The library needs issuerBaseURL, baseURL, clientID and secret to request and accept authentication. These can be configured with environmental variables:

+
ISSUER_BASE_URL=https://YOUR_DOMAIN
+CLIENT_ID=YOUR_CLIENT_ID
+BASE_URL=https://YOUR_APPLICATION_ROOT_URL
+SECRET=LONG_RANDOM_VALUE
+

... or in the library initialization:

+
// index.js
+
+const { auth } = require('express-openid-connect');
+app.use(
+  auth({
+    issuerBaseURL: 'https://YOUR_DOMAIN',
+    baseURL: 'https://YOUR_APPLICATION_ROOT_URL',
+    clientID: 'YOUR_CLIENT_ID',
+    secret: 'LONG_RANDOM_STRING',
+  })
+);
+

With this basic configuration, your application will require authentication for all routes and store the user identity in an encrypted and signed cookie.

+

See the examples for route-specific authentication, custom application session handling, requesting and using access tokens for external APIs, and more.

+

See the API documentation for additional configuration possibilities and provided methods.

+ +

A note on error handling

+
+

Errors raised by this library are handled by the default Express error handler which, in the interests of security, does not include the stack trace in the production environment.

+

But you may want to go one step further and hide additional error details from client, like the error message. To do this see the Express documentation on writing Custom error handlers

+ +

Contributing

+
+

We appreciate feedback and contribution to this repo! Before you get started, please see the following:

+ +

Contributions can be made to this library through PRs to fix issues, improve documentation or add features. Please fork this repo, create a well-named branch, and submit a PR with a complete template filled out.

+

Code changes in PRs should be accompanied by tests covering the changed or added functionality. Tests can be run for this library with:

+
npm install
+npm test
+

When you're ready to push your changes, please run the lint command first:

+
npm run lint
+ +

Support + Feedback

+
+

Please use the Issues queue in this repo for questions and feedback.

+ +

Vulnerability Reporting

+
+

Please do not report security vulnerabilities on the public GitHub issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.

+ +

What is Auth0?

+
+

Auth0 helps you to easily:

+
    +
  • implement authentication with multiple identity providers, including social (e.g., Google, Facebook, Microsoft, LinkedIn, GitHub, Twitter, etc), or enterprise (e.g., Windows Azure AD, Google Apps, Active Directory, ADFS, SAML, etc.)
  • +
  • log in users with username/password databases, passwordless, or multi-factor authentication
  • +
  • link multiple user accounts together
  • +
  • generate signed JSON Web Tokens to authorize your API calls and flow the user identity securely
  • +
  • access demographics and analytics detailing how, when, and where users are logging in
  • +
  • enrich user profiles from other data sources using customizable JavaScript rules
  • +
+

Why Auth0?

+ +

License

+
+

This project is licensed under the MIT license. See the LICENSE file for more info.

+
+
+
+

Index

+
+ +
+
+
+

Functions

+
+ +

attemptSilentLogin

+
    +
  • attemptSilentLogin(): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to attempt silent login (prompt=none) but not require authentication.

    +
    +

    See attemptSilentLogin

    +
    const { attemptSilentLogin } = require('express-openid-connect');
    +
    +app.get('/', attemptSilentLogin(), (req, res) => {
    +  res.render('homepage', {
    +    isAuthenticated: req.isAuthenticated() // show a login or logout button
    +  });
    +});
    +
    +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

auth

+ +
    +
  • + +
    +
    +

    Express JS middleware implementing sign on for Express web apps using OpenID Connect.

    +
    +

    The auth() middleware requires secret, baseURL, clientID + and issuerBaseURL.

    +

    If you are using a response type that includes code, you will also need: 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'))
    +
    +

    Parameters

    + +

    Returns RequestHandler

    +
  • +
+
+
+ +

claimCheck

+
    +
  • claimCheck(checkFn: (req: OpenidRequest, claims: IdTokenClaims) => boolean): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to protect a route, providing a custom function to check.

    +
    +
    const { claimCheck } = require('express-openid-connect');
    +
    +app.get('/admin/community', claimCheck((req, claims) => {
    +  return claims.isAdmin && claims.roles.includes('community');
    +}), (req, res) => {
    +  res.send(...);
    +});
    +
    +
    +

    Parameters

    +
      +
    • +
      checkFn: (req: OpenidRequest, claims: IdTokenClaims) => boolean
      +
        +
      • + +
          +
        • +

          Parameters

          + +

          Returns boolean

          +
        • +
        +
      • +
      +
    • +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

claimEquals

+
    +
  • claimEquals(claim: string, value: boolean | number | string | null): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to protect a route based on the value of a specific claim.

    +
    +
    const { claimEquals } = require('express-openid-connect');
    +
    +app.get('/admin', claimEquals('isAdmin', true), (req, res) => {
    +  res.send(...);
    +});
    +
    +
    +

    Parameters

    +
      +
    • +
      claim: string
      +
      +

      The name of the claim

      +
      +
    • +
    • +
      value: boolean | number | string | null
      +
      +

      The value of the claim, should be a primitive

      +
      +
    • +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

claimIncludes

+
    +
  • claimIncludes(claim: string, ...args: (string | number | false | true)[]): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Use this MW to protect a route, checking that all values are in a claim.

    +
    +
    const { claimIncludes } = require('express-openid-connect');
    +
    +app.get('/admin/delete', claimIncludes('roles', 'admin', 'superadmin'), (req, res) => {
    +  res.send(...);
    +});
    +
    +
    +

    Parameters

    +
      +
    • +
      claim: string
      +
      +

      The name of the claim

      +
      +
    • +
    • +
      Rest ...args: (string | number | false | true)[]
      +
      +

      Claim values that must all be included

      +
      +
    • +
    +

    Returns RequestHandler

    +
  • +
+
+
+ +

requiresAuth

+
    +
  • requiresAuth(requiresLoginCheck?: (req: OpenidRequest) => boolean): RequestHandler
  • +
+
    +
  • + +
    +
    +

    Set authRequired to false then require authentication + on specific routes.

    +
    +
    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}`);
    +});
    +
    +
    +

    Parameters

    + +

    Returns RequestHandler

    +
  • +
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/accesstoken.html b/docs/interfaces/accesstoken.html new file mode 100644 index 00000000..25d16941 --- /dev/null +++ b/docs/interfaces/accesstoken.html @@ -0,0 +1,2943 @@ + + + + + + AccessToken | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+

Hierarchy

+
    +
  • + AccessToken +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+

Methods

+ +
+
+
+
+
+

Properties

+
+ +

access_token

+
access_token: string
+ +
+
+

The access token itself, can be an opaque string, JWT, or non-JWT token.

+
+
+
+
+ +

expires_in

+
expires_in: number
+ +
+
+

Number of seconds until the access token expires.

+
+
+
+
+ +

isExpired

+
isExpired: () => boolean
+ +
+
+

Returns true if the access_token has expired.

+
+
+
+

Type declaration

+
    +
  • +
      +
    • (): boolean
    • +
    +
      +
    • +

      Returns boolean

      +
    • +
    +
  • +
+
+
+
+ +

token_type

+
token_type: string
+ +
+
+

The type of access token, Usually "Bearer".

+
+
+
+
+
+

Methods

+
+ +

refresh

+ +
    +
  • + +
    +
    +

    Performs refresh_token grant type exchange and updates the session's access token.

    +
    +
    let accessToken = req.oidc.accessToken;
    +if (accessToken.isExpired()) {
    +  accessToken = await accessToken.refresh();
    +}
    +
    +

    Returns Promise<AccessToken>

    +
  • +
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/configparams.html b/docs/interfaces/configparams.html new file mode 100644 index 00000000..1c086e75 --- /dev/null +++ b/docs/interfaces/configparams.html @@ -0,0 +1,3309 @@ + + + + + + ConfigParams | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

Configuration parameters passed to the auth() middleware.

+
+

issuerBaseURL, baseURL, clientID + and secret are required but can be configured with environmental variables:

+
ISSUER_BASE_URL=https://YOUR_DOMAIN
+CLIENT_ID=YOUR_CLIENT_ID
+BASE_URL=https://YOUR_APPLICATION_ROOT_URL
+SECRET=LONG_RANDOM_VALUE
+
+
+
+

Hierarchy

+
    +
  • + ConfigParams +
  • +
+
+
+

Index

+
+ +
+
+
+

Properties

+
+ +

Optional attemptSilentLogin

+
attemptSilentLogin: boolean
+ +
+
+

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

+
+
+
+
+ +

Optional auth0Logout

+
auth0Logout: boolean
+ +
+
+

Boolean value to enable Auth0's logout feature.

+
+
+
+
+ +

Optional authRequired

+
authRequired: boolean
+ +
+
+

Require authentication for all routes.

+
+
+
+
+ +

Optional authorizationParams

+
authorizationParams: AuthorizationParameters
+ +
+
+

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:

+
{
+  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:

+
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:

+
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"
+  }
+}));
+
+
+
+ +

Optional baseURL

+
baseURL: string
+ +
+
+

REQUIRED. The root URL for the application router, eg https://localhost + Can use env key BASE_URL instead.

+
+
+
+
+ +

Optional clientID

+
clientID: string
+ +
+
+

REQUIRED. The Client ID for your application. + Can use env key CLIENT_ID instead.

+
+
+
+
+ +

Optional clientSecret

+
clientSecret: string
+ +
+
+

The Client Secret for your application. + Required when requesting access tokens. + Can use env key CLIENT_SECRET instead.

+
+
+
+
+ +

Optional clockTolerance

+
clockTolerance: number
+ +
+
+

Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.` + Default is 60

+
+
+
+
+ +

Optional enableTelemetry

+
enableTelemetry: boolean
+ +
+
+

To opt-out of sending the library and node version to your authorization server + via the Auth0-Client header. Default is `true

+
+
+
+
+ +

Optional errorOnRequiredAuth

+
errorOnRequiredAuth: boolean
+ +
+
+

Throw a 401 error instead of triggering the login process for routes that require authentication. + Default is false

+
+
+
+
+ +

Optional getLoginState

+
getLoginState: (req: OpenidRequest, options: LoginOptions) => object
+ +
+
+

Function that returns an object with URL-safe state values for res.oidc.login(). + Used for passing custom state parameters to your authorization server.

+
+
app.use(auth({
+  ...
+  getLoginState(req, options) {
+    return {
+      returnTo: options.returnTo || req.originalUrl,
+      customState: 'foo'
+    };
+  }
+}))
+``
+
+
+

Type declaration

+ +
+
+
+ +

Optional idTokenSigningAlg

+
idTokenSigningAlg: string
+ +
+
+

String value for the expected ID token algorithm. Default is 'RS256'

+
+
+
+
+ +

Optional identityClaimFilter

+
identityClaimFilter: string[]
+ +
+
+

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' ]

+
+
+
+
+ +

Optional idpLogout

+
idpLogout: boolean
+ +
+
+

Boolean value to log the user out from the identity provider on application logout. Default is false

+
+
+
+
+ +

Optional issuerBaseURL

+
issuerBaseURL: string
+ +
+
+

REQUIRED. The root URL for the token issuer with no trailing slash. + Can use env key ISSUER_BASE_URL instead.

+
+
+
+
+ +

Optional legacySameSiteCookie

+
legacySameSiteCookie: boolean
+ +
+
+

Set a fallback cookie with no SameSite attribute when response_mode is form_post. + Default is true

+
+
+
+
+ +

Optional routes

+
routes: { callback?: string; login?: string | false; logoutPath?: string | false; postLogoutRedirect?: string }
+ +
+
+

Boolean value to automatically install the login and logout routes.

+
+
+
+

Type declaration

+
    +
  • +
    Optional callback?: string
    +
    +
    +

    Relative path to the application callback to process the response from the authorization server.

    +
    +
    +
  • +
  • +
    Optional login?: string | false
    +
    +
    +

    Relative path to application login.

    +
    +
    +
  • +
  • +
    Optional logoutPath?: string | false
    +
    +
    +

    Relative path to application logout.

    +
    +
    +
  • +
  • +
    Optional postLogoutRedirect?: string
    +
    +
    +

    Either a relative path to the application or a valid URI to an external domain. + This value must be registered on the authorization server. + The user will be redirected to this after a logout has been performed.

    +
    +
    +
  • +
+
+
+
+ +

Optional secret

+
secret: string | Array<string>
+ +
+
+

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.

+
+
+
+
+ +

Optional session

+
session: boolean | SessionConfigParams
+ +
+
+

Object defining application session cookie attributes.

+
+
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/loginoptions.html b/docs/interfaces/loginoptions.html new file mode 100644 index 00000000..a46a6578 --- /dev/null +++ b/docs/interfaces/loginoptions.html @@ -0,0 +1,2859 @@ + + + + + + LoginOptions | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

Custom options to pass to login.

+
+
+
+
+

Hierarchy

+
    +
  • + LoginOptions +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

Optional authorizationParams

+
authorizationParams: AuthorizationParameters
+ +
+
+

Override the default authorizationParams

+
+
+
+
+ +

Optional returnTo

+
returnTo: string
+ +
+
+

URL to return to after login, overrides the Default is {@link Request.originalUrl}

+
+
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/logoutoptions.html b/docs/interfaces/logoutoptions.html new file mode 100644 index 00000000..1b58cd2e --- /dev/null +++ b/docs/interfaces/logoutoptions.html @@ -0,0 +1,2840 @@ + + + + + + LogoutOptions | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

Custom options to pass to logout.

+
+
+
+
+

Hierarchy

+
    +
  • + LogoutOptions +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

Optional returnTo

+
returnTo: string
+ +
+
+

URL to returnTo after logout, overrides the Default in {@link ConfigParams.routes.postLogoutRedirect routes.postLogoutRedirect}

+
+
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/openidrequest.html b/docs/interfaces/openidrequest.html new file mode 100644 index 00000000..7c0b007c --- /dev/null +++ b/docs/interfaces/openidrequest.html @@ -0,0 +1,6493 @@ + + + + + + OpenidRequest | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

The Express.js Request with oidc context added by the auth middleware.

+
+
app.use(auth());
+
+app.get('/profile', (req, res) => {
+  const user = req.oidc.user;
+  ...
+})
+
+
+
+

Type parameters

+
    +
  • +

    P: core.Params

    +
  • +
  • +

    ResBody

    +
  • +
  • +

    ReqBody

    +
  • +
  • +

    ReqQuery

    +
  • +
+
+
+

Hierarchy

+
    +
  • + Request +
      +
    • + OpenidRequest +
    • +
    +
  • +
+
+
+

Implements

+
    +
  • ReadableStream
  • +
+
+
+

Index

+
+ +
+
+
+

Constructors

+
+ +

constructor

+ +
    +
  • + +

    Parameters

    +
      +
    • +
      socket: Socket
      +
    • +
    +

    Returns OpenidRequest

    +
  • +
+
+
+
+

Properties

+
+ +

accepted

+
accepted: MediaType[]
+ +
+
+

Return an array of Accepted media types + ordered from highest quality to lowest.

+
+
+
+
+ +

app

+
app: Application
+ +
+
+ +

baseUrl

+
baseUrl: string
+ +
+
+ +

body

+
body: ReqBody
+ +
+
+ +

complete

+
complete: boolean
+ +
+
+ +

connection

+
connection: Socket
+ +
+
+
deprecate
+

Use socket instead.

+
+
+
+
+
+ +

cookies

+
cookies: any
+ +
+
+ +

destroyed

+
destroyed: boolean
+ +
+
+ +

fresh

+
fresh: boolean
+ +
+
+

Check if the request is fresh, aka + Last-Modified and/or the ETag + still match.

+
+
+
+
+ +

headers

+
headers: IncomingHttpHeaders
+ +
+
+ +

host

+
host: string
+ +
+
+
deprecated
+

Use hostname instead.

+
+
+
+
+
+ +

hostname

+
hostname: string
+ +
+
+

Parse the "Host" header field hostname.

+
+
+
+
+ +

httpVersion

+
httpVersion: string
+ +
+
+ +

httpVersionMajor

+
httpVersionMajor: number
+ +
+
+ +

httpVersionMinor

+
httpVersionMinor: number
+ +
+
+ +

ip

+
ip: string
+ +
+
+

Return the remote address, or when + "trust proxy" is true return + the upstream addr.

+
+
+
+
+ +

ips

+
ips: string[]
+ +
+
+

When "trust proxy" is true, parse + the "X-Forwarded-For" ip address list.

+
+

For example if the value were "client, proxy1, proxy2" + you would receive the array ["client", "proxy1", "proxy2"] + where "proxy2" is the furthest down-stream.

+
+
+
+ +

method

+
method: string
+ +
+
+ +

Optional next

+
next: NextFunction
+ +
+
+ +

oidc

+ + +
+
+

Library namespace for authentication methods and data.

+
+
+
+
+ +

originalUrl

+
originalUrl: string
+ +
+
+ +

params

+
params: P
+ +
+
+ +

path

+
path: string
+ +
+
+

Short-hand for url.parse(req.url).pathname.

+
+
+
+
+ +

protocol

+
protocol: string
+ +
+
+

Return the protocol string "http" or "https" + when requested with TLS. When the "trust proxy" + setting is enabled the "X-Forwarded-Proto" header + field will be trusted. If you're running behind + a reverse proxy that supplies https for you this + may be enabled.

+
+
+
+
+ +

query

+
query: ReqQuery
+ +
+
+ +

rawHeaders

+
rawHeaders: string[]
+ +
+
+ +

rawTrailers

+
rawTrailers: string[]
+ +
+
+ +

readable

+
readable: boolean
+ +
+
+ +

Readonly readableHighWaterMark

+
readableHighWaterMark: number
+ +
+
+ +

Readonly readableLength

+
readableLength: number
+ +
+
+ +

Readonly readableObjectMode

+
readableObjectMode: boolean
+ +
+
+ +

Optional res

+
res: Response<ResBody>
+ +
+
+

After middleware.init executed, Request will contain res and next properties + See: express/lib/middleware/init.js

+
+
+
+
+ +

route

+
route: any
+ +
+
+ +

secure

+
secure: boolean
+ +
+
+

Short-hand for:

+
+

req.protocol == 'https'

+
+
+
+ +

signedCookies

+
signedCookies: any
+ +
+
+ +

socket

+
socket: Socket
+ +
+
+ +

stale

+
stale: boolean
+ +
+
+

Check if the request is stale, aka + "Last-Modified" and / or the "ETag" for the + resource has changed.

+
+
+
+
+ +

Optional statusCode

+
statusCode: number
+ +
+
+

Only valid for response obtained from http.ClientRequest.

+
+
+
+
+ +

Optional statusMessage

+
statusMessage: string
+ +
+
+

Only valid for response obtained from http.ClientRequest.

+
+
+
+
+ +

subdomains

+
subdomains: string[]
+ +
+
+

Return subdomains as an array.

+
+

Subdomains are the dot-separated parts of the host before the main domain of + the app. By default, the domain of the app is assumed to be the last two + parts of the host. This can be changed by setting "subdomain offset".

+

For example, if the domain is "tobi.ferrets.example.com": + If "subdomain offset" is not set, req.subdomains is ["ferrets", "tobi"]. + If "subdomain offset" is 3, req.subdomains is ["tobi"].

+
+
+
+ +

trailers

+
trailers: {}
+ +
+

Type declaration

+
    +
  • +
    [key: string]: string | undefined
    +
  • +
+
+
+
+ +

url

+
url: string
+ +
+
+ +

xhr

+
xhr: boolean
+ +
+
+

Check if the request was an XMLHttpRequest.

+
+
+
+
+ +

Static defaultMaxListeners

+
defaultMaxListeners: number
+ +
+
+
+

Methods

+
+ +

[Symbol.asyncIterator]

+
    +
  • [Symbol.asyncIterator](): AsyncIterableIterator<any>
  • +
+ +
+
+ +

_destroy

+
    +
  • _destroy(error: Error | null, callback: (error?: Error | null) => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      error: Error | null
      +
    • +
    • +
      callback: (error?: Error | null) => void
      +
        +
      • +
          +
        • (error?: Error | null): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Optional error: Error | null
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

_read

+
    +
  • _read(size: number): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      size: number
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

accepts

+
    +
  • accepts(): string[]
  • +
  • accepts(type: string): string | false
  • +
  • accepts(type: string[]): string | false
  • +
  • accepts(...type: string[]): string | false
  • +
+
    +
  • + +
    +
    +

    Check if the given type(s) is acceptable, returning + the best match when true, otherwise undefined, in which + case you should respond with 406 "Not Acceptable".

    +
    +

    The type value may be a single mime type string + such as "application/json", the extension name + such as "json", a comma-delimted list such as "json, html, text/plain", + or an array ["json", "html", "text/plain"]. When a list + or array is given the best match, if any is returned.

    +

    Examples:

    +
    // Accept: text/html
    +req.accepts('html');
    +// => "html"
    +
    +// Accept: text/*, application/json
    +req.accepts('html');
    +// => "html"
    +req.accepts('text/html');
    +// => "text/html"
    +req.accepts('json, text');
    +// => "json"
    +req.accepts('application/json');
    +// => "application/json"
    +
    +// Accept: text/*, application/json
    +req.accepts('image/png');
    +req.accepts('png');
    +// => undefined
    +
    +// Accept: text/*;q=.5, application/json
    +req.accepts(['html', 'json']);
    +req.accepts('html, json');
    +// => "json"
    +
    +

    Returns string[]

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      type: string
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      type: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      Rest ...type: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
+
+
+ +

acceptsCharsets

+
    +
  • acceptsCharsets(): string[]
  • +
  • acceptsCharsets(charset: string): string | false
  • +
  • acceptsCharsets(charset: string[]): string | false
  • +
  • acceptsCharsets(...charset: string[]): string | false
  • +
+
    +
  • + +
    +
    +

    Returns the first accepted charset of the specified character sets, + based on the request's Accept-Charset HTTP header field. + If none of the specified charsets is accepted, returns false.

    +
    +

    For more information, or if you have issues or concerns, see accepts.

    +
    +

    Returns string[]

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      charset: string
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      charset: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      Rest ...charset: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
+
+
+ +

acceptsEncodings

+
    +
  • acceptsEncodings(): string[]
  • +
  • acceptsEncodings(encoding: string): string | false
  • +
  • acceptsEncodings(encoding: string[]): string | false
  • +
  • acceptsEncodings(...encoding: string[]): string | false
  • +
+
    +
  • + +
    +
    +

    Returns the first accepted encoding of the specified encodings, + based on the request's Accept-Encoding HTTP header field. + If none of the specified encodings is accepted, returns false.

    +
    +

    For more information, or if you have issues or concerns, see accepts.

    +
    +

    Returns string[]

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      encoding: string
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      encoding: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      Rest ...encoding: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
+
+
+ +

acceptsLanguages

+
    +
  • acceptsLanguages(): string[]
  • +
  • acceptsLanguages(lang: string): string | false
  • +
  • acceptsLanguages(lang: string[]): string | false
  • +
  • acceptsLanguages(...lang: string[]): string | false
  • +
+
    +
  • + +
    +
    +

    Returns the first accepted language of the specified languages, + based on the request's Accept-Language HTTP header field. + If none of the specified languages is accepted, returns false.

    +
    +

    For more information, or if you have issues or concerns, see accepts.

    +
    +

    Returns string[]

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      lang: string
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      lang: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      Rest ...lang: string[]
      +
    • +
    +

    Returns string | false

    +
  • +
+
+
+ +

addListener

+
    +
  • addListener(event: "close", listener: () => void): this
  • +
  • addListener(event: "data", listener: (chunk: any) => void): this
  • +
  • addListener(event: "end", listener: () => void): this
  • +
  • addListener(event: "readable", listener: () => void): this
  • +
  • addListener(event: "error", listener: (err: Error) => void): this
  • +
  • addListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +
    +
    +

    Event emitter + The defined events on documents including:

    +
      +
    1. close
    2. +
    3. data
    4. +
    5. end
    6. +
    7. readable
    8. +
    9. error
    10. +
    +
    +
    +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      listener: (chunk: any) => void
      +
        +
      • +
          +
        • (chunk: any): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            chunk: any
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

destroy

+
    +
  • destroy(error?: Error): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional error: Error
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

emit

+
    +
  • emit(event: "close"): boolean
  • +
  • emit(event: "data", chunk: any): boolean
  • +
  • emit(event: "end"): boolean
  • +
  • emit(event: "readable"): boolean
  • +
  • emit(event: "error", err: Error): boolean
  • +
  • emit(event: string | symbol, ...args: any[]): boolean
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      chunk: any
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      err: Error
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      Rest ...args: any[]
      +
    • +
    +

    Returns boolean

    +
  • +
+
+
+ +

eventNames

+
    +
  • eventNames(): Array<string | symbol>
  • +
+
    +
  • + +

    Returns Array<string | symbol>

    +
  • +
+
+
+ +

get

+
    +
  • get(name: "set-cookie"): string[] | undefined
  • +
  • get(name: string): string | undefined
  • +
+
    +
  • + +
    +
    +

    Return request header.

    +
    +

    The Referrer header field is special-cased, + both Referrer and Referer are interchangeable.

    +

    Examples:

    +
    req.get('Content-Type');
    +// => "text/plain"
    +
    +req.get('content-type');
    +// => "text/plain"
    +
    +req.get('Something');
    +// => undefined

    Aliased as req.header().

    +
    +

    Parameters

    +
      +
    • +
      name: "set-cookie"
      +
    • +
    +

    Returns string[] | undefined

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    +

    Returns string | undefined

    +
  • +
+
+
+ +

getMaxListeners

+
    +
  • getMaxListeners(): number
  • +
+ +
+
+ +

header

+
    +
  • header(name: "set-cookie"): string[] | undefined
  • +
  • header(name: string): string | undefined
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      name: "set-cookie"
      +
    • +
    +

    Returns string[] | undefined

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    +

    Returns string | undefined

    +
  • +
+
+
+ +

is

+
    +
  • is(type: string | string[]): string | false | null
  • +
+
    +
  • + +
    +
    +

    Check if the incoming request contains the "Content-Type" + header field, and it contains the give mime type.

    +
    +

    Examples:

    +
     // With Content-Type: text/html; charset=utf-8
    + req.is('html');
    + req.is('text/html');
    + req.is('text/*');
    + // => true
    +
    + // When Content-Type is application/json
    + req.is('json');
    + req.is('application/json');
    + req.is('application/*');
    + // => true
    +
    + req.is('html');
    + // => false
    +
    +

    Parameters

    +
      +
    • +
      type: string | string[]
      +
    • +
    +

    Returns string | false | null

    +
  • +
+
+
+ +

isPaused

+
    +
  • isPaused(): boolean
  • +
+
    +
  • + +

    Returns boolean

    +
  • +
+
+
+ +

listenerCount

+
    +
  • listenerCount(type: string | symbol): number
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      type: string | symbol
      +
    • +
    +

    Returns number

    +
  • +
+
+
+ +

listeners

+
    +
  • listeners(event: string | symbol): Function[]
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    +

    Returns Function[]

    +
  • +
+
+
+ +

off

+
    +
  • off(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

on

+
    +
  • on(event: "close", listener: () => void): this
  • +
  • on(event: "data", listener: (chunk: any) => void): this
  • +
  • on(event: "end", listener: () => void): this
  • +
  • on(event: "readable", listener: () => void): this
  • +
  • on(event: "error", listener: (err: Error) => void): this
  • +
  • on(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      listener: (chunk: any) => void
      +
        +
      • +
          +
        • (chunk: any): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            chunk: any
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

once

+
    +
  • once(event: "close", listener: () => void): this
  • +
  • once(event: "data", listener: (chunk: any) => void): this
  • +
  • once(event: "end", listener: () => void): this
  • +
  • once(event: "readable", listener: () => void): this
  • +
  • once(event: "error", listener: (err: Error) => void): this
  • +
  • once(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      listener: (chunk: any) => void
      +
        +
      • +
          +
        • (chunk: any): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            chunk: any
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

param

+
    +
  • param(name: string, defaultValue?: any): string
  • +
+
    +
  • + +
    +
    +
    deprecated
    +

    since 4.11 Use either req.params, req.body or req.query, as applicable.

    +

    Return the value of param name when present or defaultValue.

    +
      +
    • Checks route placeholders, ex: /user/:id
    • +
    • Checks body params, ex: id=12, {"id":12}
    • +
    • Checks query string params, ex: ?id=12
    • +
    +

    To utilize request bodies, req.body + should be an object. This can be done by using + the connect.bodyParser() middleware.

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    • +
      Optional defaultValue: any
      +
    • +
    +

    Returns string

    +
  • +
+
+
+ +

pause

+
    +
  • pause(): this
  • +
+
    +
  • + +

    Returns this

    +
  • +
+
+
+ +

pipe

+
    +
  • pipe<T>(destination: T, options?: { end?: boolean }): T
  • +
+
    +
  • + +

    Type parameters

    +
      +
    • +

      T: WritableStream

      +
    • +
    +

    Parameters

    +
      +
    • +
      destination: T
      +
    • +
    • +
      Optional options: { end?: boolean }
      +
        +
      • +
        Optional end?: boolean
        +
      • +
      +
    • +
    +

    Returns T

    +
  • +
+
+
+ +

prependListener

+
    +
  • prependListener(event: "close", listener: () => void): this
  • +
  • prependListener(event: "data", listener: (chunk: any) => void): this
  • +
  • prependListener(event: "end", listener: () => void): this
  • +
  • prependListener(event: "readable", listener: () => void): this
  • +
  • prependListener(event: "error", listener: (err: Error) => void): this
  • +
  • prependListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      listener: (chunk: any) => void
      +
        +
      • +
          +
        • (chunk: any): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            chunk: any
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

prependOnceListener

+
    +
  • prependOnceListener(event: "close", listener: () => void): this
  • +
  • prependOnceListener(event: "data", listener: (chunk: any) => void): this
  • +
  • prependOnceListener(event: "end", listener: () => void): this
  • +
  • prependOnceListener(event: "readable", listener: () => void): this
  • +
  • prependOnceListener(event: "error", listener: (err: Error) => void): this
  • +
  • prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      listener: (chunk: any) => void
      +
        +
      • +
          +
        • (chunk: any): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            chunk: any
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

push

+
    +
  • push(chunk: any, encoding?: string): boolean
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      Optional encoding: string
      +
    • +
    +

    Returns boolean

    +
  • +
+
+
+ +

range

+
    +
  • range(size: number, options?: RangeParserOptions): RangeParserRanges | RangeParserResult | undefined
  • +
+
    +
  • + +
    +
    +

    Parse Range header field, capping to the given size.

    +
    +

    Unspecified ranges such as "0-" require knowledge of your resource length. In + the case of a byte range this is of course the total number of bytes. + If the Range header field is not given undefined is returned. + If the Range header field is given, return value is a result of range-parser. + See more ./types/range-parser/index.d.ts

    +

    NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" + should respond with 4 users when available, not 3.

    +
    +

    Parameters

    +
      +
    • +
      size: number
      +
    • +
    • +
      Optional options: RangeParserOptions
      +
    • +
    +

    Returns RangeParserRanges | RangeParserResult | undefined

    +
  • +
+
+
+ +

rawListeners

+
    +
  • rawListeners(event: string | symbol): Function[]
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    +

    Returns Function[]

    +
  • +
+
+
+ +

read

+
    +
  • read(size?: number): any
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional size: number
      +
    • +
    +

    Returns any

    +
  • +
+
+
+ +

removeAllListeners

+
    +
  • removeAllListeners(event?: string | symbol): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional event: string | symbol
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

removeListener

+
    +
  • removeListener(event: "close", listener: () => void): this
  • +
  • removeListener(event: "data", listener: (chunk: any) => void): this
  • +
  • removeListener(event: "end", listener: () => void): this
  • +
  • removeListener(event: "readable", listener: () => void): this
  • +
  • removeListener(event: "error", listener: (err: Error) => void): this
  • +
  • removeListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "data"
      +
    • +
    • +
      listener: (chunk: any) => void
      +
        +
      • +
          +
        • (chunk: any): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            chunk: any
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "end"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "readable"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

resume

+
    +
  • resume(): this
  • +
+
    +
  • + +

    Returns this

    +
  • +
+
+
+ +

setEncoding

+
    +
  • setEncoding(encoding: string): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      encoding: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

setMaxListeners

+
    +
  • setMaxListeners(n: number): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      n: number
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

setTimeout

+
    +
  • setTimeout(msecs: number, callback?: () => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      msecs: number
      +
    • +
    • +
      Optional callback: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

unpipe

+
    +
  • unpipe(destination?: WritableStream): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional destination: WritableStream
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

unshift

+
    +
  • unshift(chunk: any, encoding?: BufferEncoding): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      Optional encoding: BufferEncoding
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

wrap

+
    +
  • wrap(oldStream: ReadableStream): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      oldStream: ReadableStream
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

Static from

+
    +
  • from(iterable: Iterable<any> | AsyncIterable<any>, options?: ReadableOptions): Readable
  • +
+
    +
  • + +
    +
    +

    A utility method for creating Readable Streams out of iterators.

    +
    +
    +

    Parameters

    +
      +
    • +
      iterable: Iterable<any> | AsyncIterable<any>
      +
    • +
    • +
      Optional options: ReadableOptions
      +
    • +
    +

    Returns Readable

    +
  • +
+
+
+ +

Static listenerCount

+
    +
  • listenerCount(emitter: EventEmitter, event: string | symbol): number
  • +
+
    +
  • + +
    +
    +
    deprecated
    +

    since v4.0.0

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      emitter: EventEmitter
      +
    • +
    • +
      event: string | symbol
      +
    • +
    +

    Returns number

    +
  • +
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/openidresponse.html b/docs/interfaces/openidresponse.html new file mode 100644 index 00000000..b835c9e8 --- /dev/null +++ b/docs/interfaces/openidresponse.html @@ -0,0 +1,7479 @@ + + + + + + OpenidResponse | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

The Express.js Response with oidc context added by the auth middleware.

+
+
app.use(auth());
+
+app.get('/login', (req, res) => {
+  res.oidc.login();
+})
+
+
+
+

Type parameters

+
    +
  • +

    ResBody

    +
  • +
+
+
+

Hierarchy

+
    +
  • + Response +
      +
    • + OpenidResponse +
    • +
    +
  • +
+
+
+

Implements

+
    +
  • WritableStream
  • +
+
+
+

Index

+
+ +
+
+
+

Constructors

+
+ +

constructor

+ +
    +
  • + +

    Parameters

    +
      +
    • +
      req: IncomingMessage
      +
    • +
    +

    Returns OpenidResponse

    +
  • +
+
+
+
+

Properties

+
+ +

app

+
app: Application
+ +
+
+ +

charset

+
charset: string
+ +
+
+ +

chunkedEncoding

+
chunkedEncoding: boolean
+ +
+
+ +

connection

+
connection: Socket
+ +
+
+
deprecate
+

Use socket instead.

+
+
+
+
+
+ +

destroyed

+
destroyed: boolean
+ +
+
+ +

finished

+
finished: boolean
+ +
+
+ +

headersSent

+
headersSent: boolean
+ +
+
+ +

json

+
json: Send<ResBody, this>
+ +
+
+

Send JSON response.

+
+

Examples:

+
res.json(null);
+res.json({ user: 'tj' });
+res.status(500).json('oh noes!');
+res.status(404).json('I dont have that');
+
+
+
+ +

jsonp

+
jsonp: Send<ResBody, this>
+ +
+
+

Send JSON response with JSONP callback support.

+
+

Examples:

+
res.jsonp(null);
+res.jsonp({ user: 'tj' });
+res.status(500).jsonp('oh noes!');
+res.status(404).jsonp('I dont have that');
+
+
+
+ +

locals

+
locals: any
+ +
+
+ +

oidc

+ + +
+
+

Library namespace for authentication methods and data.

+
+
+
+
+ +

Optional req

+
req: Request
+ +
+
+

After middleware.init executed, Response will contain req property + See: express/lib/middleware/init.js

+
+
+
+
+ +

send

+
send: Send<ResBody, this>
+ +
+
+

Send a response.

+
+

Examples:

+
res.send(new Buffer('wahoo'));
+res.send({ some: 'json' });
+res.send('<p>some html</p>');
+res.status(404).send('Sorry, cant find that');
+
+
+
+ +

sendDate

+
sendDate: boolean
+ +
+
+ +

shouldKeepAlive

+
shouldKeepAlive: boolean
+ +
+
+ +

socket

+
socket: Socket
+ +
+
+ +

statusCode

+
statusCode: number
+ +
+
+ +

statusMessage

+
statusMessage: string
+ +
+
+ +

upgrading

+
upgrading: boolean
+ +
+
+ +

useChunkedEncodingByDefault

+
useChunkedEncodingByDefault: boolean
+ +
+
+ +

Readonly writable

+
writable: boolean
+ +
+
+ +

Readonly writableCorked

+
writableCorked: number
+ +
+
+ +

Readonly writableEnded

+
writableEnded: boolean
+ +
+
+ +

writableFinished

+
writableFinished: boolean
+ +
+
+ +

Readonly writableHighWaterMark

+
writableHighWaterMark: number
+ +
+
+ +

Readonly writableLength

+
writableLength: number
+ +
+
+ +

Readonly writableObjectMode

+
writableObjectMode: boolean
+ +
+
+ +

Static defaultMaxListeners

+
defaultMaxListeners: number
+ +
+
+
+

Methods

+
+ +

_destroy

+
    +
  • _destroy(error: Error | null, callback: (error?: Error | null) => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      error: Error | null
      +
    • +
    • +
      callback: (error?: Error | null) => void
      +
        +
      • +
          +
        • (error?: Error | null): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Optional error: Error | null
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

_final

+
    +
  • _final(callback: (error?: Error | null) => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      callback: (error?: Error | null) => void
      +
        +
      • +
          +
        • (error?: Error | null): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Optional error: Error | null
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

_write

+
    +
  • _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      encoding: string
      +
    • +
    • +
      callback: (error?: Error | null) => void
      +
        +
      • +
          +
        • (error?: Error | null): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Optional error: Error | null
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

Optional _writev

+
    +
  • _writev(chunks: Array<{ chunk: any; encoding: string }>, callback: (error?: Error | null) => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      chunks: Array<{ chunk: any; encoding: string }>
      +
    • +
    • +
      callback: (error?: Error | null) => void
      +
        +
      • +
          +
        • (error?: Error | null): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Optional error: Error | null
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

addListener

+
    +
  • addListener(event: "close", listener: () => void): this
  • +
  • addListener(event: "drain", listener: () => void): this
  • +
  • addListener(event: "error", listener: (err: Error) => void): this
  • +
  • addListener(event: "finish", listener: () => void): this
  • +
  • addListener(event: "pipe", listener: (src: Readable) => void): this
  • +
  • addListener(event: "unpipe", listener: (src: Readable) => void): this
  • +
  • addListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +
    +
    +

    Event emitter + The defined events on documents including:

    +
      +
    1. close
    2. +
    3. drain
    4. +
    5. error
    6. +
    7. finish
    8. +
    9. pipe
    10. +
    11. unpipe
    12. +
    +
    +
    +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

addTrailers

+
    +
  • addTrailers(headers: OutgoingHttpHeaders | Array<[string, string]>): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      headers: OutgoingHttpHeaders | Array<[string, string]>
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

append

+
    +
  • append(field: string, value?: string[] | string): this
  • +
+
    +
  • + +
    +
    +

    Appends the specified value to the HTTP response header field. + If the header is not already set, it creates the header with the specified value. + The value parameter can be a string or an array.

    +
    +

    Note: calling res.set() after res.append() will reset the previously-set header value.

    +
    +
    since
    +

    4.11.0

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      field: string
      +
    • +
    • +
      Optional value: string[] | string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

assignSocket

+
    +
  • assignSocket(socket: Socket): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      socket: Socket
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

attachment

+
    +
  • attachment(filename?: string): this
  • +
+
    +
  • + +
    +
    +

    Set Content-Disposition header to attachment with optional filename.

    +
    +
    +

    Parameters

    +
      +
    • +
      Optional filename: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

clearCookie

+
    +
  • clearCookie(name: string, options?: any): this
  • +
+
    +
  • + +
    +
    +

    Clear cookie name.

    +
    +
    +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    • +
      Optional options: any
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

contentType

+
    +
  • contentType(type: string): this
  • +
+
    +
  • + +
    +
    +

    Set Content-Type response header with type through mime.lookup() + when it does not contain "/", or set the Content-Type to type otherwise.

    +
    +

    Examples:

    +
    res.type('.html');
    +res.type('html');
    +res.type('json');
    +res.type('application/json');
    +res.type('png');
    +
    +

    Parameters

    +
      +
    • +
      type: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

cookie

+
    +
  • cookie(name: string, val: string, options: CookieOptions): this
  • +
  • cookie(name: string, val: any, options: CookieOptions): this
  • +
  • cookie(name: string, val: any): this
  • +
+
    +
  • + +
    +
    +

    Set cookie name to val, with the given options.

    +
    +

    Options:

    +
      +
    • maxAge max-age in milliseconds, converted to expires
    • +
    • signed sign the cookie
    • +
    • path defaults to "/"
    • +
    +

    Examples:

    +

    // "Remember Me" for 15 minutes + res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });

    +

    // save as above + res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })

    +
    +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    • +
      val: string
      +
    • +
    • +
      options: CookieOptions
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    • +
      val: any
      +
    • +
    • +
      options: CookieOptions
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    • +
      val: any
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

cork

+
    +
  • cork(): void
  • +
+
    +
  • + +

    Returns void

    +
  • +
+
+
+ +

destroy

+
    +
  • destroy(error?: Error): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional error: Error
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

detachSocket

+
    +
  • detachSocket(socket: Socket): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      socket: Socket
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

download

+
    +
  • download(path: string, fn?: Errback): void
  • +
  • download(path: string, filename: string, fn?: Errback): void
  • +
  • download(path: string, filename: string, options: any, fn?: Errback): void
  • +
+
    +
  • + +
    +
    +

    Transfer the file at the given path as an attachment.

    +
    +

    Optionally providing an alternate attachment filename, + and optional callback fn(err). The callback is invoked + when the data transfer is complete, or when an error has + ocurred. Be sure to check res.headerSent if you plan to respond.

    +

    The optional options argument passes through to the underlying + res.sendFile() call, and takes the exact same parameters.

    +

    This method uses res.sendfile().

    +
    +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      Optional fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      filename: string
      +
    • +
    • +
      Optional fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      filename: string
      +
    • +
    • +
      options: any
      +
    • +
    • +
      Optional fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

emit

+
    +
  • emit(event: "close"): boolean
  • +
  • emit(event: "drain"): boolean
  • +
  • emit(event: "error", err: Error): boolean
  • +
  • emit(event: "finish"): boolean
  • +
  • emit(event: "pipe", src: Readable): boolean
  • +
  • emit(event: "unpipe", src: Readable): boolean
  • +
  • emit(event: string | symbol, ...args: any[]): boolean
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      err: Error
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      src: Readable
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      src: Readable
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      Rest ...args: any[]
      +
    • +
    +

    Returns boolean

    +
  • +
+
+
+ +

end

+
    +
  • end(cb?: () => void): void
  • +
  • end(chunk: any, cb?: () => void): void
  • +
  • end(chunk: any, encoding: string, cb?: () => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional cb: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      Optional cb: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      encoding: string
      +
    • +
    • +
      Optional cb: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

eventNames

+
    +
  • eventNames(): Array<string | symbol>
  • +
+
    +
  • + +

    Returns Array<string | symbol>

    +
  • +
+
+
+ +

flushHeaders

+
    +
  • flushHeaders(): void
  • +
+ +
+
+ +

format

+
    +
  • format(obj: any): this
  • +
+
    +
  • + +
    +
    +

    Respond to the Acceptable formats using an obj + of mime-type callbacks.

    +
    +

    This method uses req.accepted, an array of + acceptable types ordered by their quality values. + When "Accept" is not present the first callback + is invoked, otherwise the first match is used. When + no match is performed the server responds with + 406 "Not Acceptable".

    +

    Content-Type is set for you, however if you choose + you may alter this within the callback using res.type() + or res.set('Content-Type', ...).

    +

    res.format({ + 'text/plain': function(){ + res.send('hey'); + },

    +
     'text/html': function(){
    +   res.send('<p>hey</p>');
    + },
    +
    + 'appliation/json': function(){
    +   res.send({ message: 'hey' });
    + }

    });

    +

    In addition to canonicalized MIME types you may + also use extnames mapped to these types:

    +

    res.format({ + text: function(){ + res.send('hey'); + },

    +
     html: function(){
    +   res.send('<p>hey</p>');
    + },
    +
    + json: function(){
    +   res.send({ message: 'hey' });
    + }

    });

    +

    By default Express passes an Error + with a .status of 406 to next(err) + if a match is not made. If you provide + a .default callback it will be invoked + instead.

    +
    +

    Parameters

    +
      +
    • +
      obj: any
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

get

+
    +
  • get(field: string): string
  • +
+
    +
  • + +
    +
    +

    Get value for header field.

    +
    +
    +

    Parameters

    +
      +
    • +
      field: string
      +
    • +
    +

    Returns string

    +
  • +
+
+
+ +

getHeader

+
    +
  • getHeader(name: string): number | string | string[] | undefined
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    +

    Returns number | string | string[] | undefined

    +
  • +
+
+
+ +

getHeaderNames

+
    +
  • getHeaderNames(): string[]
  • +
+ +
+
+ +

getHeaders

+
    +
  • getHeaders(): OutgoingHttpHeaders
  • +
+
    +
  • + +

    Returns OutgoingHttpHeaders

    +
  • +
+
+
+ +

getMaxListeners

+
    +
  • getMaxListeners(): number
  • +
+ +
+
+ +

hasHeader

+
    +
  • hasHeader(name: string): boolean
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    +

    Returns boolean

    +
  • +
+
+
+ +

header

+
    +
  • header(field: any): this
  • +
  • header(field: string, value?: string | string[]): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      field: any
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      field: string
      +
    • +
    • +
      Optional value: string | string[]
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

links

+
    +
  • links(links: any): this
  • +
+ +
+
+ +

listenerCount

+
    +
  • listenerCount(type: string | symbol): number
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      type: string | symbol
      +
    • +
    +

    Returns number

    +
  • +
+
+
+ +

listeners

+
    +
  • listeners(event: string | symbol): Function[]
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    +

    Returns Function[]

    +
  • +
+
+
+ +

location

+
    +
  • location(url: string): this
  • +
+
    +
  • + +
    +
    +

    Set the location header to url.

    +
    +

    The given url can also be the name of a mapped url, for + example by default express supports "back" which redirects + to the Referrer or Referer headers or "/".

    +

    Examples:

    +

    res.location('/foo/bar').; + res.location('http://example.com'); + res.location('../login'); // /blog/post/1 -> /blog/login

    +

    Mounting:

    +

    When an application is mounted and res.location() + is given a path that does not lead with "/" it becomes + relative to the mount-point. For example if the application + is mounted at "/blog", the following would become "/blog/login".

    +
     res.location('login');

    While the leading slash would result in a location of "/login":

    +
     res.location('/login');
    +
    +

    Parameters

    +
      +
    • +
      url: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

off

+
    +
  • off(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

on

+
    +
  • on(event: "close", listener: () => void): this
  • +
  • on(event: "drain", listener: () => void): this
  • +
  • on(event: "error", listener: (err: Error) => void): this
  • +
  • on(event: "finish", listener: () => void): this
  • +
  • on(event: "pipe", listener: (src: Readable) => void): this
  • +
  • on(event: "unpipe", listener: (src: Readable) => void): this
  • +
  • on(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

once

+
    +
  • once(event: "close", listener: () => void): this
  • +
  • once(event: "drain", listener: () => void): this
  • +
  • once(event: "error", listener: (err: Error) => void): this
  • +
  • once(event: "finish", listener: () => void): this
  • +
  • once(event: "pipe", listener: (src: Readable) => void): this
  • +
  • once(event: "unpipe", listener: (src: Readable) => void): this
  • +
  • once(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

pipe

+
    +
  • pipe<T>(destination: T, options?: { end?: boolean }): T
  • +
+
    +
  • + +

    Type parameters

    +
      +
    • +

      T: WritableStream

      +
    • +
    +

    Parameters

    +
      +
    • +
      destination: T
      +
    • +
    • +
      Optional options: { end?: boolean }
      +
        +
      • +
        Optional end?: boolean
        +
      • +
      +
    • +
    +

    Returns T

    +
  • +
+
+
+ +

prependListener

+
    +
  • prependListener(event: "close", listener: () => void): this
  • +
  • prependListener(event: "drain", listener: () => void): this
  • +
  • prependListener(event: "error", listener: (err: Error) => void): this
  • +
  • prependListener(event: "finish", listener: () => void): this
  • +
  • prependListener(event: "pipe", listener: (src: Readable) => void): this
  • +
  • prependListener(event: "unpipe", listener: (src: Readable) => void): this
  • +
  • prependListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

prependOnceListener

+
    +
  • prependOnceListener(event: "close", listener: () => void): this
  • +
  • prependOnceListener(event: "drain", listener: () => void): this
  • +
  • prependOnceListener(event: "error", listener: (err: Error) => void): this
  • +
  • prependOnceListener(event: "finish", listener: () => void): this
  • +
  • prependOnceListener(event: "pipe", listener: (src: Readable) => void): this
  • +
  • prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this
  • +
  • prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

rawListeners

+
    +
  • rawListeners(event: string | symbol): Function[]
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    +

    Returns Function[]

    +
  • +
+
+
+ +

redirect

+
    +
  • redirect(url: string): void
  • +
  • redirect(status: number, url: string): void
  • +
  • redirect(url: string, status: number): void
  • +
+
    +
  • + +
    +
    +

    Redirect to the given url with optional response status + defaulting to 302.

    +
    +

    The resulting url is determined by res.location(), so + it will play nicely with mounted apps, relative paths, + "back" etc.

    +

    Examples:

    +

    res.redirect('/foo/bar'); + res.redirect('http://example.com'); + res.redirect(301, 'http://example.com'); + res.redirect('http://example.com', 301); + res.redirect('../login'); // /blog/post/1 -> /blog/login

    +
    +

    Parameters

    +
      +
    • +
      url: string
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      status: number
      +
    • +
    • +
      url: string
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      url: string
      +
    • +
    • +
      status: number
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

removeAllListeners

+
    +
  • removeAllListeners(event?: string | symbol): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional event: string | symbol
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

removeHeader

+
    +
  • removeHeader(name: string): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

removeListener

+
    +
  • removeListener(event: "close", listener: () => void): this
  • +
  • removeListener(event: "drain", listener: () => void): this
  • +
  • removeListener(event: "error", listener: (err: Error) => void): this
  • +
  • removeListener(event: "finish", listener: () => void): this
  • +
  • removeListener(event: "pipe", listener: (src: Readable) => void): this
  • +
  • removeListener(event: "unpipe", listener: (src: Readable) => void): this
  • +
  • removeListener(event: string | symbol, listener: (...args: any[]) => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      event: "close"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "drain"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "error"
      +
    • +
    • +
      listener: (err: Error) => void
      +
        +
      • +
          +
        • (err: Error): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "finish"
      +
    • +
    • +
      listener: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "pipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: "unpipe"
      +
    • +
    • +
      listener: (src: Readable) => void
      +
        +
      • +
          +
        • (src: Readable): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            src: Readable
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      event: string | symbol
      +
    • +
    • +
      listener: (...args: any[]) => void
      +
        +
      • +
          +
        • (...args: any[]): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            Rest ...args: any[]
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

render

+
    +
  • render(view: string, options?: object, callback?: (err: Error, html: string) => void): void
  • +
  • render(view: string, callback?: (err: Error, html: string) => void): void
  • +
+
    +
  • + +
    +
    +

    Render view with the given options and optional callback fn. + When a callback function is given a response will not be made + automatically, otherwise a response of 200 and text/html is given.

    +
    +

    Options:

    +
      +
    • cache boolean hinting to the engine it should cache
    • +
    • filename filename of the view being rendered
    • +
    +
    +

    Parameters

    +
      +
    • +
      view: string
      +
    • +
    • +
      Optional options: object
      +
    • +
    • +
      Optional callback: (err: Error, html: string) => void
      +
        +
      • +
          +
        • (err: Error, html: string): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          • +
            html: string
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      view: string
      +
    • +
    • +
      Optional callback: (err: Error, html: string) => void
      +
        +
      • +
          +
        • (err: Error, html: string): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            err: Error
            +
          • +
          • +
            html: string
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

sendFile

+
    +
  • sendFile(path: string, fn?: Errback): void
  • +
  • sendFile(path: string, options: any, fn?: Errback): void
  • +
+
    +
  • + +
    +
    +

    Transfer the file at the given path.

    +
    +

    Automatically sets the Content-Type response header field. + The callback fn(err) is invoked when the transfer is complete + or when an error occurs. Be sure to check res.sentHeader + if you wish to attempt responding, as the header and some data + may have already been transferred.

    +

    Options:

    +
      +
    • maxAge defaulting to 0 (can be string converted by ms)
    • +
    • root root directory for relative filenames
    • +
    • headers object of headers to serve with file
    • +
    • dotfiles serve dotfiles, defaulting to false; can be "allow" to send them
    • +
    +

    Other options are passed along to send.

    +

    Examples:

    +

    The following example illustrates how res.sendFile() may + be used as an alternative for the static() middleware for + dynamic situations. The code backing res.sendFile() is actually + the same code, so HTTP cache support etc is identical.

    +
    app.get('/user/:uid/photos/:file', function(req, res){
    +  var uid = req.params.uid
    +    , file = req.params.file;
    +
    +  req.user.mayViewFilesFrom(uid, function(yes){
    +    if (yes) {
    +      res.sendFile('/uploads/' + uid + '/' + file);
    +    } else {
    +      res.send(403, 'Sorry! you cant see that.');
    +    }
    +  });
    +});
    +
    +
    api
    +

    public

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      Optional fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      options: any
      +
    • +
    • +
      Optional fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

sendStatus

+
    +
  • sendStatus(code: number): this
  • +
+
    +
  • + +
    +
    +

    Set the response HTTP status code to statusCode and send its string representation as the response body.

    +
    +
    +
    link
    +

    http://expressjs.com/4x/api.html#res.sendStatus

    +

    Examples:

    +

    res.sendStatus(200); // equivalent to res.status(200).send('OK') + res.sendStatus(403); // equivalent to res.status(403).send('Forbidden') + res.sendStatus(404); // equivalent to res.status(404).send('Not Found') + res.sendStatus(500); // equivalent to res.status(500).send('Internal Server Error')

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      code: number
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

sendfile

+
    +
  • sendfile(path: string): void
  • +
  • sendfile(path: string, options: any): void
  • +
  • sendfile(path: string, fn: Errback): void
  • +
  • sendfile(path: string, options: any, fn: Errback): void
  • +
+
    +
  • + +
    +
    +
    deprecated
    +

    Use sendFile instead.

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    +

    Returns void

    +
  • +
  • + +
    +
    +
    deprecated
    +

    Use sendFile instead.

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      options: any
      +
    • +
    +

    Returns void

    +
  • +
  • + +
    +
    +
    deprecated
    +

    Use sendFile instead.

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
  • + +
    +
    +
    deprecated
    +

    Use sendFile instead.

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      path: string
      +
    • +
    • +
      options: any
      +
    • +
    • +
      fn: Errback
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

set

+
    +
  • set(field: any): this
  • +
  • set(field: string, value?: string | string[]): this
  • +
+
    +
  • + +
    +
    +

    Set header field to val, or pass + an object of header fields.

    +
    +

    Examples:

    +

    res.set('Foo', ['bar', 'baz']); + res.set('Accept', 'application/json'); + res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });

    +

    Aliased as res.header().

    +
    +

    Parameters

    +
      +
    • +
      field: any
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      field: string
      +
    • +
    • +
      Optional value: string | string[]
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

setDefaultEncoding

+
    +
  • setDefaultEncoding(encoding: string): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      encoding: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

setHeader

+
    +
  • setHeader(name: string, value: number | string | string[]): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      name: string
      +
    • +
    • +
      value: number | string | string[]
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

setMaxListeners

+
    +
  • setMaxListeners(n: number): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      n: number
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

setTimeout

+
    +
  • setTimeout(msecs: number, callback?: () => void): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      msecs: number
      +
    • +
    • +
      Optional callback: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

status

+
    +
  • status(code: number): this
  • +
+
    +
  • + +
    +
    +

    Set status code.

    +
    +
    +

    Parameters

    +
      +
    • +
      code: number
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

type

+
    +
  • type(type: string): this
  • +
+
    +
  • + +
    +
    +

    Set Content-Type response header with type through mime.lookup() + when it does not contain "/", or set the Content-Type to type otherwise.

    +
    +

    Examples:

    +
    res.type('.html');
    +res.type('html');
    +res.type('json');
    +res.type('application/json');
    +res.type('png');
    +
    +

    Parameters

    +
      +
    • +
      type: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

uncork

+
    +
  • uncork(): void
  • +
+
    +
  • + +

    Returns void

    +
  • +
+
+
+ +

vary

+
    +
  • vary(field: string): this
  • +
+
    +
  • + +
    +
    +

    Adds the field to the Vary response header, if it is not there already. + Examples:

    +
    +
    res.vary('User-Agent').render('docs');
    +
    +

    Parameters

    +
      +
    • +
      field: string
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

write

+
    +
  • write(chunk: any, cb?: (error: Error | null | undefined) => void): boolean
  • +
  • write(chunk: any, encoding: string, cb?: (error: Error | null | undefined) => void): boolean
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      Optional cb: (error: Error | null | undefined) => void
      +
        +
      • +
          +
        • (error: Error | null | undefined): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            error: Error | null | undefined
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns boolean

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      chunk: any
      +
    • +
    • +
      encoding: string
      +
    • +
    • +
      Optional cb: (error: Error | null | undefined) => void
      +
        +
      • +
          +
        • (error: Error | null | undefined): void
        • +
        +
          +
        • +

          Parameters

          +
            +
          • +
            error: Error | null | undefined
            +
          • +
          +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns boolean

    +
  • +
+
+
+ +

writeContinue

+
    +
  • writeContinue(callback?: () => void): void
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      Optional callback: () => void
      +
        +
      • +
          +
        • (): void
        • +
        +
          +
        • +

          Returns void

          +
        • +
        +
      • +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

writeHead

+
    +
  • writeHead(statusCode: number, reasonPhrase?: string, headers?: OutgoingHttpHeaders): this
  • +
  • writeHead(statusCode: number, headers?: OutgoingHttpHeaders): this
  • +
+
    +
  • + +

    Parameters

    +
      +
    • +
      statusCode: number
      +
    • +
    • +
      Optional reasonPhrase: string
      +
    • +
    • +
      Optional headers: OutgoingHttpHeaders
      +
    • +
    +

    Returns this

    +
  • +
  • + +

    Parameters

    +
      +
    • +
      statusCode: number
      +
    • +
    • +
      Optional headers: OutgoingHttpHeaders
      +
    • +
    +

    Returns this

    +
  • +
+
+
+ +

writeProcessing

+
    +
  • writeProcessing(): void
  • +
+ +
+
+ +

Static listenerCount

+
    +
  • listenerCount(emitter: EventEmitter, event: string | symbol): number
  • +
+
    +
  • + +
    +
    +
    deprecated
    +

    since v4.0.0

    +
    +
    +
    +

    Parameters

    +
      +
    • +
      emitter: EventEmitter
      +
    • +
    • +
      event: string | symbol
      +
    • +
    +

    Returns number

    +
  • +
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/requestcontext.html b/docs/interfaces/requestcontext.html new file mode 100644 index 00000000..fb204c9b --- /dev/null +++ b/docs/interfaces/requestcontext.html @@ -0,0 +1,3001 @@ + + + + + + RequestContext | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

The request authentication context found on the Express request when + OpenID Connect auth middleware is added to your application.

+
+
app.use(auth());
+
+app.get('/profile', (req, res) => {
+  const user = req.oidc.user;
+  ...
+})
+
+
+
+

Hierarchy

+
    +
  • + RequestContext +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+

Methods

+ +
+
+
+
+
+

Properties

+
+ +

Optional accessToken

+
accessToken: AccessToken
+ +
+
+

Credentials that can be used by an application to access an API.

+
+

See: https://auth0.com/docs/protocols/oidc#access-tokens

+
+
+
+ +

Optional idToken

+
idToken: string
+ +
+
+

The OpenID Connect ID Token.

+
+

See: https://auth0.com/docs/protocols/oidc#id-tokens

+
+
+
+ +

Optional idTokenClaims

+
idTokenClaims: IdTokenClaims
+ +
+
+

An object containing all the claims of the ID Token.

+
+
+
+
+ +

isAuthenticated

+
isAuthenticated: () => boolean
+ +
+
+

Method to check the user's authenticated state, returns true if logged in.

+
+
+
+

Type declaration

+
    +
  • +
      +
    • (): boolean
    • +
    +
      +
    • +

      Returns boolean

      +
    • +
    +
  • +
+
+
+
+ +

Optional refreshToken

+
refreshToken: string
+ +
+
+

Credentials that can be used to refresh an access token.

+
+

See: https://auth0.com/docs/tokens/concepts/refresh-tokens

+
+
+
+ +

Optional user

+
user: object
+ +
+
+

An object containing all the claims of the ID Token with the claims + specified in identityClaimFilter removed.

+
+
+
+
+
+

Methods

+
+ +

fetchUserInfo

+
    +
  • fetchUserInfo(): Promise<UserinfoResponse>
  • +
+
    +
  • + +
    +
    +

    Fetches the OIDC userinfo response.

    +
    +
    app.use(auth());
    +
    +app.get('/user-info', async (req, res) => {
    +  const userInfo = await req.oidc.fetchUserInfo();
    +  res.json(userInfo);
    +})
    +
    +

    Returns Promise<UserinfoResponse>

    +
  • +
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/responsecontext.html b/docs/interfaces/responsecontext.html new file mode 100644 index 00000000..7b827d0d --- /dev/null +++ b/docs/interfaces/responsecontext.html @@ -0,0 +1,2920 @@ + + + + + + ResponseContext | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

The response authentication context found on the Express response when + OpenID Connect auth middleware is added to your application.

+
+
app.use(auth());
+
+app.get('/admin-login', (req, res) => {
+  res.openid.login({ returnTo: '/admin' })
+})
+
+
+
+

Hierarchy

+
    +
  • + ResponseContext +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

login

+
login: (opts?: LoginOptions) => Promise<void>
+ +
+
+

Provided by default via the /login route. Call this to override or have other + login routes with custom authorizationParams or returnTo

+
+
app.get('/admin-login', (req, res) => {
+  res.oidc.login({
+    returnTo: '/admin',
+    authorizationParams: {
+      scope: 'openid profile email admin:user',
+    }
+  });
+});
+
+
+

Type declaration

+
    +
  • + +
      +
    • +

      Parameters

      + +

      Returns Promise<void>

      +
    • +
    +
  • +
+
+
+
+ +

logout

+
logout: (opts?: LogoutOptions) => Promise<void>
+ +
+
+

Provided by default via the /logout route. Call this to override or have other + logout routes with custom returnTo

+
+
app.get('/admin-logout', (req, res) => {
+  res.oidc.logout({ returnTo: '/admin-welcome' })
+});
+
+
+

Type declaration

+
    +
  • + +
      +
    • +

      Parameters

      + +

      Returns Promise<void>

      +
    • +
    +
  • +
+
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/sessionconfigparams.html b/docs/interfaces/sessionconfigparams.html new file mode 100644 index 00000000..6c8f756c --- /dev/null +++ b/docs/interfaces/sessionconfigparams.html @@ -0,0 +1,3031 @@ + + + + + + SessionConfigParams | express-openid-connect + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+ +
+
+
+
+
+

Configuration parameters used for the application session.

+
+
+
+
+

Hierarchy

+
    +
  • + SessionConfigParams +
  • +
+
+
+

Index

+
+ +
+
+
+

Properties

+
+ +

Optional absoluteDuration

+
absoluteDuration: boolean | number
+ +
+
+

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).

+
+
+
+
+ +

Optional domain

+
domain: string
+ +
+
+

Domain name for the cookie. + Passed to the Response cookie as domain

+
+
+
+
+ +

Optional httpOnly

+
httpOnly: boolean
+ +
+
+

Flags the cookie to be accessible only by the web server. + Passed to the Response cookie as httponly. + Defaults to true.

+
+
+
+
+ +

Optional name

+
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.

+
+
+
+
+ +

Optional path

+
path: string
+ +
+
+

Path for the cookie. + Passed to the Response cookie as path

+
+
+
+
+ +

Optional rolling

+
rolling: boolean
+ +
+
+

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.

+
+
+
+
+ +

Optional rollingDuration

+
rollingDuration: 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).

+
+
+
+
+ +

Optional sameSite

+
sameSite: string
+ +
+
+

Value of the SameSite Set-Cookie attribute. + Passed to the Response cookie as samesite. + Defaults to "Lax" but will be adjusted based on {@link AuthorizationParameters.response_type}.

+
+
+
+
+ +

Optional secure

+
secure: boolean
+ +
+
+

Marks the cookie to be used over secure channels only. + Passed to the Response cookie as secure. + Defaults to {@link Request.secure}.

+
+
+
+
+ +

Optional transient

+
transient: boolean
+ +
+
+

Set to true to use a transient cookie (cookie without an explicit expiration). + Default is false

+
+
+
+
+
+
+

Legend

+
+
    +
  • Property
  • +
  • Method
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/end-to-end/access-an-api.js b/end-to-end/access-an-api.js new file mode 100644 index 00000000..0f87e901 --- /dev/null +++ b/end-to-end/access-an-api.js @@ -0,0 +1,87 @@ +const { assert } = require('chai'); +const sinon = require('sinon'); +const puppeteer = require('puppeteer'); +const provider = require('./fixture/oidc-provider'); +const { + baseUrl, + start, + runExample, + runApi, + stubEnv, + checkContext, + goto, + login, +} = require('./fixture/helpers'); + +describe('access an api', async () => { + let authServer; + let appServer; + let apiServer; + + beforeEach(async () => { + stubEnv(); + authServer = await start(provider, 3001); + appServer = await runExample('access-an-api'); + apiServer = await runApi(); + }); + + afterEach(async () => { + authServer.close(); + appServer.close(); + apiServer.close(); + }); + + it('should get an access token and access an api', async () => { + const browser = await puppeteer.launch({ + args: ['no-sandbox', 'disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + + const clock = sinon.useFakeTimers({ + now: Date.now(), + toFake: ['Date'], + }); + + await goto(baseUrl, page); + + await login('username', 'password', page); + + assert.equal( + page.url(), + `${baseUrl}/`, + 'User is returned to the original page' + ); + const { + accessToken: { access_token, expires_in }, + } = await checkContext(await page.cookies()); + assert.isOk(access_token); + const content = await page.content(); + assert.include( + content, + 'Products: Football boots, Running shoes, Flip flops', + 'Page should access products api and show a list of items' + ); + clock.tick(expires_in * 10000); + const { + accessToken: { isExpired }, + } = await checkContext(await page.cookies()); + assert.ok(isExpired); + + await page.reload(); + + const reloadedContent = await page.content(); + assert.include( + reloadedContent, + 'Products: Football boots, Running shoes, Flip flops', + 'Page should access products api with refreshed token and show a list of items' + ); + const { + accessToken: { access_token: new_access_token, isExpired: newIsExpired }, + } = await checkContext(await page.cookies()); + + assert.isOk(new_access_token); + assert.notOk(newIsExpired); + assert.notEqual(new_access_token, access_token); + clock.restore(); + }); +}); diff --git a/end-to-end/basic.test.js b/end-to-end/basic.test.js new file mode 100644 index 00000000..38b36df5 --- /dev/null +++ b/end-to-end/basic.test.js @@ -0,0 +1,62 @@ +const { assert } = require('chai'); +const puppeteer = require('puppeteer'); +const provider = require('./fixture/oidc-provider'); +const { + baseUrl, + start, + runExample, + stubEnv, + checkContext, + goto, + login, + logout, +} = require('./fixture/helpers'); + +describe('basic login and logout', async () => { + let authServer; + let appServer; + + beforeEach(async () => { + stubEnv(); + authServer = await start(provider, 3001); + appServer = await runExample('basic'); + }); + + afterEach(async () => { + authServer.close(); + appServer.close(); + }); + + it('should login and logout with default configuration', async () => { + const browser = await puppeteer.launch({ + args: ['no-sandbox', 'disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await goto(baseUrl, page); + assert.match( + page.url(), + /http:\/\/localhost:3001\/interaction/, + 'User should have been redirected to the auth server to login' + ); + await login('username', 'password', page); + assert.equal( + page.url(), + `${baseUrl}/`, + 'User is returned to the original page' + ); + const loggedInCookies = await page.cookies('http://localhost:3000'); + assert.ok(loggedInCookies.find(({ name }) => name === 'appSession')); + + const response = await checkContext(await page.cookies()); + assert.isOk(response.isAuthenticated); + assert.equal(response.user.sub, 'username'); + assert.empty( + response.accessToken, + "default response_type doesn't include code" + ); + await logout(page); + + const loggedOutCookies = await page.cookies('http://localhost:3000'); + assert.notOk(loggedOutCookies.find(({ name }) => name === 'appSession')); + }); +}); diff --git a/end-to-end/fixture/helpers.js b/end-to-end/fixture/helpers.js new file mode 100644 index 00000000..9a9e4ff8 --- /dev/null +++ b/end-to-end/fixture/helpers.js @@ -0,0 +1,103 @@ +const path = require('path'); +const sinon = require('sinon'); +const express = require('express'); +const request = require('request-promise-native').defaults({ json: true }); + +const baseUrl = 'http://localhost:3000'; + +const start = (app, port) => + new Promise((resolve, reject) => { + const server = app.listen(port, (err) => { + if (err) { + reject(err); + } else { + resolve(server); + } + }); + }); + +const runExample = (name) => { + const app = require(path.join('..', '..', 'examples', name)); + app.use(testMw()); + return start(app, 3000); +}; + +const runApi = () => { + const app = require(path.join('..', '..', 'examples', 'api')); + return start(app, 3002); +}; + +const stubEnv = ( + env = { + ISSUER_BASE_URL: 'http://localhost:3001', + CLIENT_ID: 'test-express-openid-connect-client-id', + BASE_URL: 'http://localhost:3000', + SECRET: 'LONG_RANDOM_VALUE', + CLIENT_SECRET: 'test-express-openid-connect-client-secret', + } +) => + sinon.stub(process, 'env').value({ + ...process.env, + ...env, + }); + +const testMw = () => { + const router = new express.Router(); + router.get('/context', (req, res) => { + res.json({ + idToken: req.oidc.idToken, + accessToken: req.oidc.accessToken + ? { + access_token: req.oidc.accessToken.access_token, + token_type: req.oidc.accessToken.token_type, + expires_in: req.oidc.accessToken.expires_in, + isExpired: req.oidc.accessToken.isExpired(), + } + : {}, + refreshToken: req.oidc.refreshToken, + idTokenClaims: req.oidc.idTokenClaims, + user: req.oidc.user, + isAuthenticated: req.oidc.isAuthenticated(), + }); + }); + return router; +}; + +const checkContext = async (cookies) => { + const jar = request.jar(); + cookies.forEach(({ name, value }) => + jar.setCookie(`${name}=${value}`, baseUrl) + ); + return request('/context', { jar, baseUrl }); +}; + +const goto = async (url, page) => + Promise.all([page.goto(url), page.waitForNavigation()]); + +const login = async (username, password, page) => { + await page.type('[name=login]', username); + await page.type('[name=password]', password); + await Promise.all([page.click('.login-submit'), page.waitForNavigation()]); + await Promise.all([page.click('.login-submit'), page.waitForNavigation()]); // consent + if (!page.url().startsWith('http://localhost:3000')) { + await page.waitForNavigation(); + } +}; + +const logout = async (page) => { + await goto(`${baseUrl}/logout`, page); + await Promise.all([page.click('[name=logout]'), page.waitForNavigation()]); +}; + +module.exports = { + baseUrl, + start, + runExample, + runApi, + stubEnv, + testMw, + checkContext, + goto, + login, + logout, +}; diff --git a/end-to-end/fixture/oidc-provider.js b/end-to-end/fixture/oidc-provider.js new file mode 100644 index 00000000..19eb6570 --- /dev/null +++ b/end-to-end/fixture/oidc-provider.js @@ -0,0 +1,49 @@ +const Provider = require('oidc-provider'); + +const config = { + clients: [ + { + client_id: 'test-express-openid-connect-client-id', + client_secret: 'test-express-openid-connect-client-secret', + token_endpoint_auth_method: 'client_secret_basic', + response_types: ['id_token', 'code', 'code id_token'], + grant_types: ['implicit', 'authorization_code', 'refresh_token'], + redirect_uris: [`http://localhost:3000/callback`], + post_logout_redirect_uris: [ + 'http://localhost:3000', + 'http://localhost:3000/custom-logout', + ], + }, + ], + formats: { + AccessToken: 'jwt', + }, + audiences() { + return 'https://api.example.com/products'; + }, + scopes: ['openid', 'offline_access', 'read:products'], + findAccount(ctx, id) { + return { + accountId: id, + claims: () => ({ sub: id }), + }; + }, +}; + +const PORT = process.env.PROVIDER_PORT || 3001; + +const provider = new Provider(`http://localhost:${PORT}`, config); + +// Monkey patch the provider to allow localhost and http redirect uris +const { invalidate: orig } = provider.Client.Schema.prototype; +provider.Client.Schema.prototype.invalidate = function invalidate( + message, + code +) { + if (code === 'implicit-force-https' || code === 'implicit-forbid-localhost') { + return; + } + orig.call(this, message); +}; + +module.exports = provider; diff --git a/end-to-end/userinfo.test.js b/end-to-end/userinfo.test.js new file mode 100644 index 00000000..7e969afc --- /dev/null +++ b/end-to-end/userinfo.test.js @@ -0,0 +1,48 @@ +const { assert } = require('chai'); +const puppeteer = require('puppeteer'); +const provider = require('./fixture/oidc-provider'); +const { + baseUrl, + start, + runExample, + stubEnv, + goto, + login, +} = require('./fixture/helpers'); + +describe('fetch userinfo', async () => { + let authServer; + let appServer; + + beforeEach(async () => { + stubEnv(); + authServer = await start(provider, 3001); + appServer = await runExample('userinfo'); + }); + + afterEach(async () => { + authServer.close(); + appServer.close(); + }); + + it('should login with hybrid flow and fetch userinfo', async () => { + const browser = await puppeteer.launch({ + args: ['no-sandbox', 'disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await goto(baseUrl, page); + assert.match( + page.url(), + /http:\/\/localhost:3001\/interaction/, + 'User should have been redirected to the auth server to login' + ); + await login('username', 'password', page); + assert.equal( + page.url(), + `${baseUrl}/`, + 'User is returned to the original page' + ); + + assert.include(await page.content(), 'hello username'); + }); +}); diff --git a/examples/.env.sample b/examples/.env.sample new file mode 100644 index 00000000..cbf8ce9d --- /dev/null +++ b/examples/.env.sample @@ -0,0 +1,9 @@ +# For the example app +PORT=3000 +# For the auth config +ISSUER_BASE_URL=https://YOUR_DOMAIN +CLIENT_ID=YOUR_CLIENT_ID +BASE_URL=https://YOUR_APPLICATION_ROOT_URL +SECRET=LONG_RANDOM_VALUE +# For response_type values that include 'code' +CLIENT_SECRET=YOUR_CLIENT_SECRET diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..c4f6b900 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,23 @@ +# Examples + +To run an example `npm run start:example -- "name of example"`. Eg to run the basic example at `./basic.js`: + +```shell script +$ npm run start:example -- basic +``` + +To run the example against your authorization server add the following items to your `./examples/.env` + +```shell script +# For the example app +PORT=3000 +# For the auth config +ISSUER_BASE_URL=https://YOUR_DOMAIN +CLIENT_ID=YOUR_CLIENT_ID +BASE_URL=https://YOUR_APPLICATION_ROOT_URL +SECRET=LONG_RANDOM_VALUE +# For response_type values that include 'code' +CLIENT_SECRET=YOUR_CLIENT_SECRET +``` + +If you do not specify an env file, we will configure one for you and start a mock authorisation server. To login to this authorisation server, use any credentials, and the username will be reflected in the `sub` claim of the ID Token. diff --git a/examples/access-an-api.js b/examples/access-an-api.js new file mode 100644 index 00000000..d077b916 --- /dev/null +++ b/examples/access-an-api.js @@ -0,0 +1,34 @@ +const express = require('express'); +const request = require('request-promise-native'); +const { auth } = require('../'); + +const app = express(); + +const { API_PORT = 3002 } = process.env; + +app.use( + auth({ + authorizationParams: { + response_type: 'code', + audience: 'https://api.example.com/products', + scope: 'openid profile email offline_access read:products', + prompt: 'consent', + }, + }) +); + +app.get('/', async (req, res) => { + let { token_type, access_token, isExpired, refresh } = req.oidc.accessToken; + if (isExpired()) { + ({ access_token } = await refresh()); + } + const products = await request.get(`http://localhost:${API_PORT}/products`, { + headers: { + Authorization: `${token_type} ${access_token}`, + }, + json: true, + }); + res.send(`Products: ${products.map(({ name }) => name).join(', ')}`); +}); + +module.exports = app; diff --git a/examples/api.js b/examples/api.js new file mode 100644 index 00000000..65c89747 --- /dev/null +++ b/examples/api.js @@ -0,0 +1,17 @@ +process.env.ALLOWED_AUDIENCES = 'https://api.example.com/products'; + +const express = require('express'); +const { auth, requiredScopes } = require('express-oauth2-bearer'); + +const app = express(); +app.use(auth()); + +app.get('/products', requiredScopes('read:products'), (req, res) => { + res.json([ + { id: 1, name: 'Football boots' }, + { id: 2, name: 'Running shoes' }, + { id: 3, name: 'Flip flops' }, + ]); +}); + +module.exports = app; diff --git a/examples/basic.js b/examples/basic.js new file mode 100644 index 00000000..9a2d56c8 --- /dev/null +++ b/examples/basic.js @@ -0,0 +1,16 @@ +const express = require('express'); +const { auth } = require('../'); + +const app = express(); + +app.use( + auth({ + idpLogout: true, + }) +); + +app.get('/', (req, res) => { + res.send(`hello ${req.oidc.user.sub}`); +}); + +module.exports = app; diff --git a/examples/custom-routes.js b/examples/custom-routes.js new file mode 100644 index 00000000..e59974cb --- /dev/null +++ b/examples/custom-routes.js @@ -0,0 +1,30 @@ +const express = require('express'); +const { auth, requiresAuth } = require('../'); + +const app = express(); + +app.use( + auth({ + idpLogout: true, + authRequired: false, + routes: { + // Pass custom options to the login method by overriding the default login route + login: false, + // Pass a custom path to the postLogoutRedirect to redirect users to a different + // path after login, this should be registered on your authorization server. + postLogoutRedirect: '/custom-logout', + }, + }) +); + +app.get('/', (req, res) => res.send('Welcome!')); + +app.get('/profile', requiresAuth(), (req, res) => + res.send(`hello ${req.oidc.user.sub}`) +); + +app.get('/login', (req, res) => res.oidc.login({ returnTo: '/profile' })); + +app.get('/custom-logout', (req, res) => res.send('Bye!')); + +module.exports = app; diff --git a/examples/routes.js b/examples/routes.js new file mode 100644 index 00000000..8ae1caf8 --- /dev/null +++ b/examples/routes.js @@ -0,0 +1,22 @@ +const express = require('express'); +const { auth, requiresAuth } = require('../'); + +const app = express(); + +app.use( + auth({ + authRequired: false + }) +); + +// Anyone can access the homepage +app.get('/', (req, res) => { + res.send('Admin Section'); +}); + +// requiresAuth checks authentication. +app.get('/admin', requiresAuth(), (req, res) => + res.send(`Hello ${req.oidc.user.sub}, this is the admin section.`) +); + +module.exports = app; diff --git a/examples/run_example.js b/examples/run_example.js new file mode 100644 index 00000000..bc51ffa3 --- /dev/null +++ b/examples/run_example.js @@ -0,0 +1,39 @@ +const path = require('path'); + +require('dotenv').config(); + +const { PORT = 3000, PROVIDER_PORT = 3001, API_PORT = 3002 } = process.env; + +const example = process.argv.pop(); + +// Configure and start a mock authorization server if no .env config is found +if (!process.env.CLIENT_ID) { + const provider = require('../end-to-end/fixture/oidc-provider'); + console.log( + 'Starting a mock authorization server. You can login with any credentials.' + ); + process.env = { + ...process.env, + ISSUER_BASE_URL: `http://localhost:${PROVIDER_PORT}`, + CLIENT_ID: 'test-express-openid-connect-client-id', + BASE_URL: `http://localhost:${PORT}`, + SECRET: 'LONG_RANDOM_VALUE', + CLIENT_SECRET: 'test-express-openid-connect-client-secret', + }; + provider.listen(PROVIDER_PORT, () => + console.log( + `Authorization server started at http://localhost:${PROVIDER_PORT}` + ) + ); +} + +const api = require(path.join(__dirname, 'api')); +api.listen(API_PORT, () => + console.log(`API started at http://localhost:${API_PORT}`) +); + +const app = require(path.join(__dirname, example)); + +app.listen(PORT, () => + console.log(`Example app started at http://localhost:${PORT}`) +); diff --git a/examples/userinfo.js b/examples/userinfo.js new file mode 100644 index 00000000..e4dbb4df --- /dev/null +++ b/examples/userinfo.js @@ -0,0 +1,20 @@ +const express = require('express'); +const { auth } = require('../'); + +const app = express(); + +app.use( + auth({ + idpLogout: true, + authorizationParams: { + response_type: 'code id_token', + }, + }) +); + +app.get('/', async (req, res) => { + const userInfo = await req.oidc.fetchUserInfo(); + res.send(`hello ${userInfo.sub}`); +}); + +module.exports = app; diff --git a/index.d.ts b/index.d.ts index 2329d09c..a50bab03 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,217 +1,625 @@ // 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; - - /** - * Decoded state for use in config.handleCallback(). - */ - openidState: object; - - /** - * Tokens for use in config.handleCallback(). - */ - openidTokens: TokenSet; + /** + * Library namespace for authentication methods and data. + */ + oidc: RequestContext; } /** - * Configuration parameters passed to the auth() middleware. + * 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 ConfigParams { - /** - * Object defining application session cookie attributes. - */ - appSession: boolean | AppSessionConfigParams; - - /** - * Boolean value to enable Auth0's logout feature. - */ - auth0Logout?: boolean; - - /** - * URL parameters used when redirecting users to the authorization server to log in. - */ - authorizationParams?: AuthorizationParameters - - /** - * REQUIRED. The root URL for the application router. - * Can use env key BASE_URL instead. - */ - baseURL?: string; - - /** - * REQUIRED. The Client ID for your application. - * Can use env key CLIENT_ID instead. - */ - clientID?: string; - - /** - * The Client Secret for your application. - * Required when requesting access tokens. - * Can use env key CLIENT_SECRET instead. - */ - clientSecret?: string; - - /** - * Integer value for the system clock's tolerance (leeway) in seconds for ID token verification. - */ - clockTolerance?: number; - - /** - * Opt-in to sending the library and node version to your authorization server - * via the `Auth0-Client` header. - */ - enableTelemetry?: boolean; - - /** - * Throw a 401 error instead of triggering the login process for routes that require authentication. - */ - errorOnRequiredAuth?: boolean; - - /** - * Function that returns a URL-safe state value for `res.openid.login()`. - */ - getLoginState?: (req: OpenidRequest, config: object) => object; - - /** - * Function that returns the profile for `req.openid.user`. - */ - getUser?: (req: OpenidRequest, config: ConfigParams) => undefined | UserinfoResponse; - - /** - * Function that runs on the callback route, after callback processing but before redirection. - */ - handleCallback?: (req: OpenidRequest, res: Response, next: NextFunction) => void; - - /** - * 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. - */ - identityClaimFilter?: string[]; +interface OpenidResponse extends Response { + /** + * Library namespace for authentication methods and data. + */ + oidc: ResponseContext; +} - /** - * Boolean value to log the user out from the identity provider on application logout. - */ - idpLogout?: boolean; +/** + * 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; +} - /** - * String value for the expected ID token algorithm. - */ - idTokenAlg?: string; +/** + * 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; + + /** + * 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; +} - /** - * REQUIRED. The root URL for the token issuer with no trailing slash. - * Can use env key ISSUER_BASE_URL instead. - */ - issuerBaseURL?: string; +/** + * 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; +} - /** - * Set a fallback cookie with no SameSite attribute when response_mode is form_post. - */ - legacySameSiteCookie?: boolean; +/** + * 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 { + /** + * 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; + + /** + * Object defining application session cookie attributes. + */ + session?: boolean | SessionConfigParams; + + /** + * Boolean value to enable Auth0's logout feature. + */ + auth0Logout?: boolean; + + /** + * 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 root URL for the application router, eg https://localhost + * Can use env key BASE_URL instead. + */ + baseURL?: string; + + /** + * REQUIRED. The Client ID for your application. + * Can use env key CLIENT_ID instead. + */ + clientID?: string; + + /** + * The Client Secret for your application. + * Required when requesting access tokens. + * Can use env key CLIENT_SECRET instead. + */ + clientSecret?: string; + + /** + * Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.` + * Default is 60 + */ + clockTolerance?: number; + + /** + * To opt-out of sending the library and node version to your authorization server + * via the `Auth0-Client` header. Default is `true + */ + enableTelemetry?: boolean; + + /** + * Throw a 401 error instead of triggering the login process for routes that require authentication. + * Default is `false` + */ + errorOnRequiredAuth?: boolean; + + /** + * 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 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; + + /** + * 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[]; + + /** + * Boolean value to log the user out from the identity provider on application logout. Default is `false` + */ + idpLogout?: boolean; + + /** + * String value for the expected ID token algorithm. Default is 'RS256' + */ + idTokenSigningAlg?: 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; + + /** + * 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; /** * Relative path to application logout. */ - logoutPath?: string; + logoutPath?: string | false; /** * Either a relative path to the application or a valid URI to an external domain. * This value must be registered on the authorization server. * The user will be redirected to this after a logout has been performed. */ - postLogoutRedirectUri?: string; + postLogoutRedirect?: string; /** * Relative path to the application callback to process the response from the authorization server. */ - 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; + }; } /** * Configuration parameters used for the application session. */ -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 value for the cookie name used for the internal session. - * This value must only include letters, numbers, and underscores. - * Default is `appSession`. - */ - name?: string; - - /** - * Integer value, in seconds, for application session duration. - * Default is 86400 seconds (1 day). - */ - duration?: number +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; + + /** + * 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 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; + + /** + * 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; + + /** + * Domain name for the cookie. + * Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `domain` + */ + domain?: string; + + /** + * Path for the cookie. + * Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `path` + */ + path?: string; + + /** + * Set to true to use a transient cookie (cookie without an explicit expiration). + * Default is `false` + */ + transient?: 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; + + /** + * 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; +} - /** - * Domain name for the cookie. - */ - cookieDomain?: 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; +} - /** - * 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; +/** + * 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; - /** - * Flags the cookie to be accessible only by the web server. - * Defaults to `true`. - */ - cookieHttpOnly?: boolean; +/** + * 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; - /** - * Path for the cookie. - */ - cookiePath?: string; +/** + * 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; - /** - * Marks the cookie to be used over secure channels only. - */ - cookieSecure?: boolean; +/** + * 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; - /** - * Value of the SameSite Set-Cookie attribute. - * Defaults to "Lax" but will be adjusted based on response_type. - */ - cookieSameSite?: string; -} +/** + * 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; -export function auth(params?: ConfigParams): RequestHandler; -export function requiresAuth(): RequestHandler; -export function unauthorizedHandler(): ErrorRequestHandler; +/** + * 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; diff --git a/index.js b/index.js index c312f461..e6b35031 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,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, }; diff --git a/lib/ResponseMode.js b/lib/ResponseMode.js deleted file mode 100644 index c6194227..00000000 --- a/lib/ResponseMode.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * - * Specifies the mode of the response according to - * [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) and - * [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html). - * - * @enum {string} - * -*/ -module.exports = { - - /** - * Uses the default mode for the response_type - * as stated in [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) - */ - Default: undefined, - - /** - * Authorization Response parameters are encoded in - * the query string added to the redirect_uri when redirecting back to the Client. - */ - Query: 'query', - - /** - * Authorization Response parameters are encoded as HTML form values - * that are auto-submitted in the User Agent, and thus are transmitted via the HTTP POST method - * to the Client, with the result parameters being encoded in the body using the application/x-www-form-urlencoded format. - * The action attribute of the form is the Client's Redirection URI and the method of the form attribute is POST. - */ - FormPost: 'form_post' -}; diff --git a/lib/appSession.js b/lib/appSession.js index 4e71c857..ac0adee5 100644 --- a/lib/appSession.js +++ b/lib/appSession.js @@ -1,27 +1,55 @@ -const { strict: assert } = require('assert'); -const { JWK, JWKS, JWE, errors: { JWEDecryptionFailed } } = 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 { sessionNameDefault, sessionDurationDefault } = require('./config'); - -const deriveKey = (secret) => hkdf(secret, 32, { info: 'JWE CEK', hash: 'SHA-256' }); -const epoch = () => Date.now() / 1000 | 0; +const COOKIES = require('./cookies'); +const { encryption: deriveKey } = require('./hkdf'); +const debug = require('./debug')('appSession'); + +const epoch = () => (Date.now() / 1000) | 0; +const CHUNK_BYTE_SIZE = 4000; + +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)); if (i === 0) { current = key; @@ -33,64 +61,178 @@ module.exports = (sessionConfig) => { keystore = current; } - function encrypt (payload, headers) { - return JWE.encrypt(payload, current, { alg, enc, zip: 'DEF', ...headers }); - } - - function decrypt (jwe) { - return JWE.decrypt(jwe, keystore, { complete: true, algorithms: [enc] }); + function encrypt(payload, headers) { + return JWE.encrypt(payload, current, { alg, enc, ...headers }); } - 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]; + function decrypt(jwe) { + return JWE.decrypt(jwe, keystore, { + complete: true, + contentEncryptionAlgorithms: [enc], + keyManagementAlgorithms: [alg], }); - const expires = cookieOptions.transient ? 0 : new Date(exp * 1000); - delete cookieOptions.transient; + } - if ((!req[sessionName] || !Object.keys(req[sessionName]).length) && sessionName in req[COOKIES]) { - res.clearCookie(sessionName, cookieOptions); - return; + function calculateExp(iat, uat) { + if (!rollingEnabled) { + return iat + absoluteDuration; } - if (req[sessionName] && Object.keys(req[sessionName]).length > 0) { - const value = encrypt(JSON.stringify(req[sessionName]), { iat, uat, exp }); + 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; - res.cookie(sessionName, value, { expires, ...cookieOptions }); + // 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, + }); + + 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); + } } } 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. + // 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(''); } - } catch (err) { - if (!(err instanceof JWEDecryptionFailed)) { - throw err; + 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)); } - } finally { - if (!req.hasOwnProperty(sessionName) || !req[sessionName]) { - req[sessionName] = {}; + } 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 })); return next(); diff --git a/lib/client.js b/lib/client.js index 27ae12d0..b3e31761 100644 --- a/lib/client.js +++ b/lib/client.js @@ -3,85 +3,115 @@ const memoize = require('p-memoize'); const url = require('url'); const urlJoin = require('url-join'); const pkg = require('../package.json'); +const debug = require('./debug')('client'); const telemetryHeader = { name: 'express-oidc', version: pkg.version, 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 ); } 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 ); } const client = new issuer.Client({ client_id: config.clientID, 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; } diff --git a/lib/config.js b/lib/config.js index d54815eb..8f0d8070 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,107 +1,170 @@ 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'), + }), + { + 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'), }), - 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 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, baseURL: process.env.BASE_URL, clientID: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, - appSession: {}, - }, config); - - if (process.env.APP_SESSION_SECRET && typeof config.appSession === 'object') { - config.appSession.secret = config.appSession.secret || process.env.APP_SESSION_SECRET; - } + ...config, + }; const paramsValidation = paramsSchema.validate(config); if (paramsValidation.error) { - throw new Error(paramsValidation.error.details[0].message); + throw new TypeError(paramsValidation.error.details[0].message); } return paramsValidation.value; }; - -module.exports.sessionDurationDefault = sessionDurationDefault; -module.exports.sessionNameDefault = sessionNameDefault; diff --git a/lib/context.js b/lib/context.js index 19b978a6..9f19a0a6 100644 --- a/lib/context.js +++ b/lib/context.js @@ -2,104 +2,259 @@ const cb = require('cb'); const url = require('url'); const urlJoin = require('url-join'); 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); } options = { authorizationParams: {}, returnTo, - ...options + ...options, }; // Ensure a redirect_uri, merge in configuration options, then passed-in options. options.authorizationParams = { redirect_uri: this.getRedirectUri(), ...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(); + } try { const authParams = { ...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); } catch (err) { next(err); @@ -107,42 +262,44 @@ class ResponseContext { } 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); if (url.parse(returnURL).host === null) { returnURL = urlJoin(config.baseURL, returnURL); } - 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 }; diff --git a/lib/cookies.js b/lib/cookies.js new file mode 100644 index 00000000..6081b290 --- /dev/null +++ b/lib/cookies.js @@ -0,0 +1,3 @@ +const COOKIES = Symbol('cookies'); + +module.exports = COOKIES; diff --git a/lib/debug.js b/lib/debug.js new file mode 100644 index 00000000..ad56624b --- /dev/null +++ b/lib/debug.js @@ -0,0 +1,2 @@ +module.exports = (name) => + require('debug')('express-openid-connect').extend(name); diff --git a/lib/hkdf.js b/lib/hkdf.js new file mode 100644 index 00000000..3bce27a3 --- /dev/null +++ b/lib/hkdf.js @@ -0,0 +1,19 @@ +const hkdf = require('futoin-hkdf'); + +const BYTE_LENGTH = 32; +const ENCRYPTION_INFO = 'JWE CEK'; +const SIGNING_INFO = 'JWS Cookie Signing'; +const options = { hash: 'SHA-256' }; + +/** + * + * Derives appropriate sized keys from the end-user provided secret random string/passphrase using + * HKDF (HMAC-based Extract-and-Expand Key Derivation Function) defined in RFC 8569 + * + * @see https://tools.ietf.org/html/rfc5869 + * + */ +module.exports.encryption = (secret) => + hkdf(secret, BYTE_LENGTH, { info: ENCRYPTION_INFO, ...options }); +module.exports.signing = (secret) => + hkdf(secret, BYTE_LENGTH, { info: SIGNING_INFO, ...options }); diff --git a/lib/hooks/getLoginState.js b/lib/hooks/getLoginState.js index 72e569d3..2c62b51f 100644 --- a/lib/hooks/getLoginState.js +++ b/lib/hooks/getLoginState.js @@ -1,11 +1,10 @@ -const { encode: base64encode, decode: base64decode } = require('base64url'); - -module.exports.defaultState = defaultState; -module.exports.encodeState = encodeState; -module.exports.decodeState = decodeState; +const base64url = require('base64url'); +const debug = require('../debug')('getLoginState'); /** - * 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. * * @param {RequestHandler} req * @param {object} options @@ -13,9 +12,9 @@ module.exports.decodeState = decodeState; * @return {object} */ 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; } /** @@ -25,8 +24,11 @@ function defaultState(req, options) { * * @return {string} */ -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)); } /** @@ -37,5 +39,9 @@ function encodeState(stateObject) { * @return {object} */ 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; diff --git a/lib/hooks/getUser.js b/lib/hooks/getUser.js deleted file mode 100644 index 8bbb3b8a..00000000 --- a/lib/hooks/getUser.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Default function for mapping a tokenSet to a user. - * This can be used for adjusting or augmenting profile data. - */ -module.exports = function(req, config) { - - if (!config.appSession || !req[config.appSession.name] || !req[config.appSession.name].claims) { - return null; - } - - return req[config.appSession.name].claims; -}; diff --git a/lib/hooks/handleCallback.js b/lib/hooks/handleCallback.js deleted file mode 100644 index f908b5dc..00000000 --- a/lib/hooks/handleCallback.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Default function for custom callback handling after receiving tokens. - * This can be used for handling token storage, making userinfo calls, etc. - */ -module.exports = function (req, res, next) { - next(); -}; diff --git a/lib/transientHandler.js b/lib/transientHandler.js index 69083264..2a856da1 100644 --- a/lib/transientHandler.js +++ b/lib/transientHandler.js @@ -1,90 +1,201 @@ -const crypto = require('crypto'); - -exports.store = store; -exports.getOnce = getOnce; -exports.createNonce = createNonce; - -/** - * 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; - - const basicAttr = { - httpOnly: true, - maxAge: 'maxAge' in opts ? parseInt(opts.maxAge, 10) : 600 * 1000 // 10 minutes - }; - - // Set the cookie with the SameSite attribute and, if needed, the Secure flag. - res.cookie(key, value, Object.assign({}, basicAttr, {sameSite: sameSiteAttr, secure: isSameSiteNone})); - - if (isSameSiteNone && fallbackCookie) { - // Set the fallback cookie with no SameSite or Secure attributes. - res.cookie('_' + key, value, basicAttr); +const { generators } = require('openid-client'); +const { JWKS, JWS, JWK } = require('jose'); +const { signing: deriveKey } = require('./hkdf'); + +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; } - return value; -} + return undefined; +}; -/** - * 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 = {}) { - - if (!req.cookies) { - return undefined; +const generateCookieValue = (cookie, value, key) => { + const signature = generateSignature(cookie, value, key); + return `${value}.${signature}`; +}; + +const COOKIES = require('./cookies'); + +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; } - let value = req.cookies[key]; - delete req.cookies[key]; - deleteCookie(key, res); + /** + * 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, + }; - if ('legacySameSiteCookie' in opts ? opts.legacySameSiteCookie : true) { - const fallbackKey = '_' + key; - value = value || req.cookies[fallbackKey]; - delete req.cookies[fallbackKey]; - deleteCookie(fallbackKey, res); + { + 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 (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; } - return value; -} + /** + * 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; + } -/** - * Generates a nonce value. - * - * @return {String} - */ -function createNonce() { - return crypto.randomBytes(16).toString('hex'); -} + 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; + } + + /** + * Generates a nonce value. + * + * @return {String} + */ + generateNonce() { + return generators.nonce(); + } -/** - * 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}); + /** + * 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, + }); + } } + +module.exports = TransientCookieHandler; diff --git a/lib/weakCache.js b/lib/weakCache.js new file mode 100644 index 00000000..cb1e169a --- /dev/null +++ b/lib/weakCache.js @@ -0,0 +1,8 @@ +const map = new WeakMap(); + +function instance(ctx) { + if (!map.has(ctx)) map.set(ctx, {}); + return map.get(ctx); +} + +module.exports = instance; diff --git a/login.png b/login.png new file mode 100644 index 00000000..01d1a116 Binary files /dev/null and b/login.png differ diff --git a/logout.png b/logout.png new file mode 100644 index 00000000..ee0e9504 Binary files /dev/null and b/logout.png differ diff --git a/middleware/attemptSilentLogin.js b/middleware/attemptSilentLogin.js new file mode 100644 index 00000000..5c698d6e --- /dev/null +++ b/middleware/attemptSilentLogin.js @@ -0,0 +1,63 @@ +const debug = require('../lib/debug')('attemptSilentLogin'); +const COOKIES = require('../lib/cookies'); +const weakRef = require('../lib/weakCache'); + +const COOKIE_NAME = 'skipSilentLogin'; + +const cancelSilentLogin = (req, res) => { + const { + config: { + session: { + cookie: { secure, domain, path }, + }, + }, + } = weakRef(req.oidc); + res.cookie(COOKIE_NAME, true, { + httpOnly: true, + secure: typeof secure === 'boolean' ? secure : req.secure, + domain, + path, + }); +}; + +const resumeSilentLogin = (req, res) => { + const { + config: { + session: { + cookie: { domain, path }, + }, + }, + } = weakRef(req.oidc); + res.clearCookie(COOKIE_NAME, { + httpOnly: true, + domain, + path, + }); +}; + +module.exports = function attemptSilentLogin() { + return (req, res, next) => { + if (!req.oidc) { + next( + new Error('req.oidc is not found, did you include the auth middleware?') + ); + return; + } + + const silentLoginAttempted = !!(req[COOKIES] || {})[COOKIE_NAME]; + + if ( + !silentLoginAttempted && + !req.oidc.isAuthenticated() && + req.accepts('html') + ) { + debug('Attempting silent login'); + cancelSilentLogin(req, res); + return res.oidc.silentLogin(); + } + next(); + }; +}; + +module.exports.cancelSilentLogin = cancelSilentLogin; +module.exports.resumeSilentLogin = resumeSilentLogin; diff --git a/middleware/auth.js b/middleware/auth.js index 85645f86..1c727531 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,137 +1,157 @@ 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'); const appSession = require('../lib/appSession'); const { decodeState } = require('../lib/hooks/getLoginState'); 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 }); - } - ); - - // Logout route, configured with logoutPath. - router.get( - enforceLeadingSlash(config.logoutPath), - (req, res) => res.openid.logout() + // 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'); } - const callbackMethod = ('form_post' === authorizeParams.response_mode ? 'post' : 'get'); - const transientOpts = { legacySameSiteCookie: config.legacySameSiteCookie }; - - // 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; + // 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 returnedState = transient.getOnce('state', req, res, transientOpts); + // 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(); + + client = + client || + (await getClient(config).catch((err) => { + next(err); + })); + + 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, + const redirectUri = res.oidc.getRedirectUri(); + + 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); + + tokenSet = await client.callback(redirectUri, callbackParams, { + max_age, + code_verifier, + nonce, + state: expectedState, + }); + } catch (err) { + throw createError.BadRequest(err.message); + } + + // 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, }); - } catch (err) { - throw createError.BadRequest(err.message); - } - req.openidState = decodeState(returnedState); - req.openidTokens = tokenSet; + attemptSilentLogin.resumeSilentLogin(req, res); - if (useAppSession) { - let identityClaims = tokenSet.claims(); - - config.identityClaimFilter.forEach(claim => { - delete identityClaims[claim]; - }); - - req[config.appSession.name].claims = identityClaims; + next(); + } catch (err) { + next(err); } - - next(); - } catch (err) { - next(err); - } - }, - config.handleCallback, - function (req, res) { - res.redirect(req.openidState.returnTo || config.baseURL); - } - ); - - 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); - } + }, + (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 + ); } - // Fail on initialization if config is invalid. - getClient(config); + 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()); + } return router; }; diff --git a/middleware/requiresAuth.js b/middleware/requiresAuth.js index a823d1b7..26b056b0 100644 --- a/middleware/requiresAuth.js +++ b/middleware/requiresAuth.js @@ -1,22 +1,133 @@ 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; - - 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.')); - } - - next(); + * 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 (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; + } + + 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); }; diff --git a/middleware/unauthorizedHandler.js b/middleware/unauthorizedHandler.js index d6d61482..85a1f670 100644 --- a/middleware/unauthorizedHandler.js +++ b/middleware/unauthorizedHandler.js @@ -4,11 +4,11 @@ * * This middleware needs to be included after your application * routes. -*/ -module.exports = function() { + */ +module.exports = function () { return (err, req, res, next) => { if (err.statusCode === 401) { - return res.openid.login(); + return res.oidc.login(); } next(err); }; diff --git a/package-lock.json b/package-lock.json index 9228b4c7..f51d48ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "express-openid-connect", - "version": "1.0.2", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14,22 +14,23 @@ } }, "@babel/core": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz", - "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", - "@babel/helpers": "^7.8.4", - "@babel/parser": "^7.8.4", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.10.2.tgz", + "integrity": "sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.2", + "@babel/helper-module-transforms": "^7.10.1", + "@babel/helpers": "^7.10.1", + "@babel/parser": "^7.10.2", + "@babel/template": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.2", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", - "json5": "^2.1.0", + "json5": "^2.1.2", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", @@ -37,40 +38,25 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", "dev": true, "requires": { - "@babel/highlight": "^7.8.3" + "@babel/highlight": "^7.10.1" } }, "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.1", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -80,12 +66,12 @@ } }, "@babel/generator": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", - "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", + "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", "dev": true, "requires": { - "@babel/types": "^7.8.3", + "@babel/types": "^7.10.2", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -100,43 +86,113 @@ } }, "@babel/helper-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", - "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", + "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" } }, "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", + "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "dev": true, + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", + "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", + "dev": true, + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz", + "integrity": "sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", + "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1", + "@babel/helper-simple-access": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz", + "integrity": "sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", + "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", + "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "dev": true, + "requires": { + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" } }, "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", + "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.10.1" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", + "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==", + "dev": true + }, "@babel/helpers": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", - "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.1.tgz", + "integrity": "sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==", "dev": true, "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3" + "@babel/template": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" } }, "@babel/highlight": { @@ -151,157 +207,146 @@ } }, "@babel/parser": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", - "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", + "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==", "dev": true }, "@babel/template": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", - "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", + "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/code-frame": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1" }, "dependencies": { "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", "dev": true, "requires": { - "@babel/highlight": "^7.8.3" + "@babel/highlight": "^7.10.1" } }, "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.1", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } } } }, "@babel/traverse": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", - "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.1.tgz", + "integrity": "sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.4", - "@babel/types": "^7.8.3", + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.1", + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" }, "dependencies": { "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", "dev": true, "requires": { - "@babel/highlight": "^7.8.3" + "@babel/highlight": "^7.10.1" } }, "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.1", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, "@babel/types": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", - "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", + "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", "dev": true, "requires": { - "esutils": "^2.0.2", + "@babel/helper-validator-identifier": "^7.10.1", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, "@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.1.tgz", + "integrity": "sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } }, "@hapi/formula": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-1.2.0.tgz", - "integrity": "sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", + "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" }, "@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz", + "integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==" }, "@hapi/joi": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-16.1.8.tgz", - "integrity": "sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.1.tgz", + "integrity": "sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==", "requires": { - "@hapi/address": "^2.1.2", - "@hapi/formula": "^1.2.0", - "@hapi/hoek": "^8.2.4", - "@hapi/pinpoint": "^1.0.2", - "@hapi/topo": "^3.1.3" + "@hapi/address": "^4.0.1", + "@hapi/formula": "^2.0.0", + "@hapi/hoek": "^9.0.0", + "@hapi/pinpoint": "^2.0.0", + "@hapi/topo": "^5.0.0" } }, "@hapi/pinpoint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-1.0.2.tgz", - "integrity": "sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" }, "@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", "requires": { - "@hapi/hoek": "^8.3.0" + "@hapi/hoek": "^9.0.0" } }, "@istanbuljs/load-nyc-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", - "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "requires": { "camelcase": "^5.3.1", "find-up": "^4.1.0", + "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" }, @@ -354,15 +399,25 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@koa/cors": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-3.1.0.tgz", + "integrity": "sha512-7ulRC1da/rBa6kj6P4g2aJfnET3z8Uf3SWu60cjbtxTA5g8lxRdX/Bd2P92EagGwwAhANeNw8T8if99rJliR6Q==", + "dev": true, + "requires": { + "vary": "^1.1.2" + } + }, "@panva/asn1.js": { "version": "1.0.0", - "resolved": "https://a0us.jfrog.io/a0us/api/npm/npm/@panva/asn1.js/-/asn1.js-1.0.0.tgz", - "integrity": "sha1-3VWue4Ep4CBJ8AlAi5fGHM+QMvY=" + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true }, "@sinonjs/commons": { "version": "1.6.0", @@ -404,20 +459,41 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, "requires": { "defer-to-connect": "^1.0.1" } }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", - "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" } }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -433,31 +509,52 @@ "@types/node": "*" } }, + "@types/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==", + "dev": true + }, + "@types/cookies": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.4.tgz", + "integrity": "sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "@types/express": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", - "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", + "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", + "@types/qs": "*", "@types/serve-static": "*" } }, "@types/express-serve-static-core": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.1.tgz", - "integrity": "sha512-9e7jj549ZI+RxY21Cl0t8uBnWyb22HzILupyHZjYEVK//5TT/1bZodU+yUbLnPdoYViBBnNWbxp4zYjGV0zUGw==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz", + "integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==", "dev": true, "requires": { "@types/node": "*", + "@types/qs": "*", "@types/range-parser": "*" } }, "@types/got": { - "version": "9.6.9", - "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.9.tgz", - "integrity": "sha512-w+ZE+Ovp6fM+1sHwJB7RN3f3pTJHZkyABuULqbtknqezQyWadFEp5BzOXaZzRqAw2md6/d3ybxQJt+BNgpvzOg==", + "version": "9.6.11", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.11.tgz", + "integrity": "sha512-dr3IiDNg5TDesGyuwTrN77E1Cd7DCdmCFtEfSGqr83jMMtcwhf/SGPbN2goY4JUWQfvxwY56+e5tjfi+oXeSdA==", + "dev": true, "requires": { "@types/node": "*", "@types/tough-cookie": "*", @@ -468,6 +565,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -476,10 +574,65 @@ } } }, + "@types/http-assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", + "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==", + "dev": true + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, + "@types/koa": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.3.tgz", + "integrity": "sha512-ABxVkrNWa4O/Jp24EYI/hRNqEVRlhB9g09p48neQp4m3xL1TJtdWk2NyNQSMCU45ejeELMQZBYyfstyVvO2H3Q==", + "dev": true, + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, "@types/mime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", + "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, "@types/node": { @@ -487,16 +640,36 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.7.tgz", "integrity": "sha512-HU0q9GXazqiKwviVxg9SI/+t/nAsGkvLDkIdxz+ObejG2nX6Si00TeLqHMoS+a/1tjH7a8YpKVQwtgHuMQsldg==" }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", + "dev": true + }, "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", "dev": true }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/serve-static": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", - "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", + "integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==", "dev": true, "requires": { "@types/express-serve-static-core": "*", @@ -504,16 +677,21 @@ } }, "@types/tough-cookie": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", - "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==" - }, - "abab": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz", - "integrity": "sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", "dev": true }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -530,26 +708,16 @@ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - } - }, "acorn-jsx": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", "dev": true }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", "dev": true }, "aggregate-error": { @@ -600,6 +768,22 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "append-transform": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", @@ -624,10 +808,10 @@ "sprintf-js": "~1.0.2" } }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", "dev": true }, "array-flatten": { @@ -642,6 +826,18 @@ "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", "dev": true }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -651,17 +847,6 @@ "safer-buffer": "~2.1.0" } }, - "asn1.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz", - "integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -680,16 +865,17 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", "dev": true }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "aws-sign2": { "version": "0.7.0", @@ -698,9 +884,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, "balanced-match": { @@ -709,6 +895,12 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, "base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -723,12 +915,23 @@ "tweetnacl": "^0.14.3" } }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "dev": true }, + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -747,6 +950,15 @@ "type-is": "~1.6.17" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -765,6 +977,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -778,11 +996,14 @@ "concat-map": "0.0.1" } }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } }, "browser-stdout": { "version": "1.3.1", @@ -790,6 +1011,22 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -802,10 +1039,26 @@ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "cacheable-lookup": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz", + "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==" + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, "requires": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -820,6 +1073,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, "requires": { "pump": "^3.0.0" } @@ -827,7 +1081,8 @@ "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true } } }, @@ -912,6 +1167,34 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -984,6 +1267,12 @@ "mimic-response": "^1.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1003,6 +1292,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -1013,6 +1303,12 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1052,37 +1348,53 @@ } }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, - "cookie-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", - "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dev": true, "requires": { - "cookie": "0.3.1", - "cookie-signature": "1.0.6" + "depd": "~2.0.0", + "keygrip": "~1.1.0" }, "dependencies": { - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true } } }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -1096,21 +1408,6 @@ "which": "^1.2.9" } }, - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "cssstyle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", - "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1120,24 +1417,12 @@ "assert-plus": "^1.0.0" } }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "decamelize": { @@ -1150,6 +1435,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, "requires": { "mimic-response": "^1.0.0" } @@ -1163,6 +1449,12 @@ "type-detect": "^4.0.0" } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1181,7 +1473,8 @@ "defer-to-connect": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true }, "define-properties": { "version": "1.1.3", @@ -1195,7 +1488,14 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true }, "depd": { "version": "1.1.2", @@ -1208,6 +1508,12 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, + "devtools-protocol": { + "version": "0.0.781568", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.781568.tgz", + "integrity": "sha512-9Uqnzy6m6zEStluH9iyJ3iHyaQziFnMnLeC8vK0eN6smiJmIx7+yB64d67C2lH/LZra+5cGscJAJsNXO+MdPMg==", + "dev": true + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -1223,19 +1529,17 @@ "esutils": "^2.0.2" } }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true }, "ecc-jsbn": { "version": "0.1.2", @@ -1262,6 +1566,15 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "ejs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.3.tgz", + "integrity": "sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg==", + "dev": true, + "requires": { + "jake": "^10.6.1" + } + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -1282,10 +1595,19 @@ "once": "^1.4.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { - "version": "1.17.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.2.tgz", - "integrity": "sha512-YoKuru3Lyoy7yVTBSH2j7UxTqe/je3dWAruC0sHvZX1GNd5zX8SSLvQqEgO9b3Ex8IW+goFI9arEEsFIbulhOw==", + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -1330,27 +1652,6 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "escodegen": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", - "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, "eslint": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", @@ -1490,6 +1791,90 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "execa": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", + "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -1528,6 +1913,27 @@ "vary": "~1.1.2" }, "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1536,6 +1942,81 @@ } } }, + "express-oauth2-bearer": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/express-oauth2-bearer/-/express-oauth2-bearer-0.4.0.tgz", + "integrity": "sha512-tnWOal4Dq0ojDNmqYFLt3za4y053Nm8Ui//VWD184jsnCCl9GOhlUuX0knnd0Q16G2q1glFGu7rnxMfaVvcONg==", + "dev": true, + "requires": { + "http-errors": "^1.7.3", + "jsonwebtoken": "^8.5.1", + "openid-client": "^3.7.2", + "p-memoize": "^2.1.0" + }, + "dependencies": { + "jose": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-1.28.0.tgz", + "integrity": "sha512-JmfDRzt/HSj8ipd9TsDtEHoLUnLYavG+7e8F6s1mx2jfVSfXOTaFQsJUydbjJpTnTDHP1+yKL9Ke7ktS/a0Eiw==", + "dev": true, + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "openid-client": { + "version": "3.15.10", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.15.10.tgz", + "integrity": "sha512-C9r6/iVzNQ7aGp0krS5mFIY5nY8AH6ajYCH0Njns6AXy2fM3Khw/dY97QlaFJWW2QLhec6xfEk23LZw9EeX66Q==", + "dev": true, + "requires": { + "@types/got": "^9.6.9", + "base64url": "^3.0.1", + "got": "^9.6.0", + "jose": "^1.27.1", + "lru-cache": "^6.0.0", + "make-error": "^1.3.6", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.0", + "p-any": "^3.0.0" + } + }, + "p-memoize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-2.1.0.tgz", + "integrity": "sha512-c6+a2iV4JyX0r4+i2IBJYO0r6LZAT2fg/tcB6GQbv1uzZsfsmKT7Ej5DRT1G6Wi7XUJSV2ZiP9+YEtluvhCmkg==", + "dev": true, + "requires": { + "mem": "^4.0.0", + "mimic-fn": "^1.0.0" + } + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1553,6 +2034,29 @@ "tmp": "^0.0.33" } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1577,6 +2081,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -1595,14 +2108,22 @@ "flat-cache": "^2.0.1" } }, - "fill-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "filelist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", + "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", "dev": true, "requires": { - "is-object": "~1.0.1", - "merge-descriptors": "~1.0.0" + "minimatch": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" } }, "finalhandler": { @@ -1618,16 +2139,33 @@ "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^3.0.0", + "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, @@ -1640,6 +2178,15 @@ "locate-path": "^3.0.0" } }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, "flat": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", @@ -1677,9 +2224,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -1754,12 +2301,36 @@ "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -1773,9 +2344,9 @@ "dev": true }, "futoin-hkdf": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.3.1.tgz", - "integrity": "sha512-k1DvCXIFAIx3hK8CSwApotX3JUDwA2Wb55zxyIgqwQpCBF2ZHgVqfHpyjG8mRpmsjRH7SWS1N/vj8EdSF9zBhw==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.3.2.tgz", + "integrity": "sha512-3EVi3ETTyJg5PSXlxLCaUVVn0pSbDf62L3Gwxne7Uq+d8adOSNWQAad4gg7WToHkcgnCJb3Wlb1P8r4Evj4GPw==" }, "gensync": { "version": "1.0.0-beta.1", @@ -1795,10 +2366,17 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, "requires": { "pump": "^3.0.0" } @@ -1826,6 +2404,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -1836,6 +2423,7 @@ "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, "requires": { "@sindresorhus/is": "^0.14.0", "@szmarczak/http-timer": "^1.1.2", @@ -1851,9 +2439,9 @@ } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, "growl": { @@ -1862,6 +2450,19 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1900,21 +2501,13 @@ "dev": true }, "hasha": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", - "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", "dev": true, "requires": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } } }, "he": { @@ -1923,21 +2516,28 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } + "highlight.js": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.1.1.tgz", + "integrity": "sha512-b4L09127uVa+9vkMgPpdUQP78ickGbHEQTWeBrQFTJZ4/n2aihWOGS0ZoUqAwjVmfjhq/C76HRzkqwZhK4sBbg==", + "dev": true }, "html-escaper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", - "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -1966,6 +2566,95 @@ "sshpk": "^1.7.0" } }, + "http2-wrapper": { + "version": "1.0.0-beta.5.2", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "requires": { + "agent-base": "5", + "debug": "4" + } + }, + "husky": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", + "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.6.0", + "cosmiconfig": "^6.0.0", + "find-versions": "^3.2.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1975,6 +2664,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2055,12 +2750,33 @@ } } }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", "dev": true }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", @@ -2079,16 +2795,37 @@ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, "is-promise": { @@ -2167,15 +2904,12 @@ } }, "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" @@ -2205,9 +2939,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -2222,9 +2956,9 @@ "dev": true }, "rimraf": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.1.tgz", - "integrity": "sha512-IQ4ikL8SjBiEDZfk+DFVwqRK8md24RWMEJkdSlgNLkyyAImcjf8SWvU1qFMDOb4igBClbTQ/ugPqXcRwdFTxZw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -2293,39 +3027,34 @@ "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } } }, "istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", "dev": true, "requires": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, + "jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "dev": true, + "requires": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, "jose": { - "version": "1.24.0", - "resolved": "https://a0us.jfrog.io/a0us/api/npm/npm/jose/-/jose-1.24.0.tgz", - "integrity": "sha1-JeTo8gNvcIhExmDLofP5Dw0CVMQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.0.tgz", + "integrity": "sha512-hubalgH1koFSAhDECNt9MooC9BKlP4iPakrOAD26518dDbMGeldS+wVvZviVINN2C/OC86OClXql7BcSdwlA2w==", "requires": { "@panva/asn1.js": "^1.0.0" } @@ -2352,40 +3081,6 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, - "jsdom": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-13.2.0.tgz", - "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^6.0.4", - "acorn-globals": "^4.3.0", - "array-equal": "^1.0.0", - "cssom": "^0.3.4", - "cssstyle": "^1.1.1", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.0", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.0.9", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.5", - "saxes": "^3.1.5", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.5.0", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^6.1.2", - "xml-name-validator": "^3.0.0" - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2395,7 +3090,14 @@ "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true }, "json-schema": { "version": "0.2.3", @@ -2422,20 +3124,21 @@ "dev": true }, "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "dev": true, "requires": { - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } + "minimist": "^1.2.5" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" } }, "jsonwebtoken": { @@ -2454,14 +3157,6 @@ "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^5.6.0" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } } }, "jsprim": { @@ -2493,22 +3188,107 @@ "safe-buffer": "^5.0.1" } }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dev": true, - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "requires": { + "tsscmp": "1.0.6" + } + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "koa": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", + "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "dev": true, + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "dev": true, "requires": { - "json-buffer": "3.0.0" + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "dev": true, + "requires": { + "any-promise": "^1.1.0" + } + } } }, "levn": { @@ -2521,6 +3301,12 @@ "type-check": "~0.3.2" } }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -2532,9 +3318,10 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true }, "lodash.flattendeep": { "version": "4.4.0", @@ -2584,19 +3371,13 @@ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", "dev": true }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "dev": true, "requires": { - "chalk": "^2.0.1" + "chalk": "^2.4.2" } }, "lolex": { @@ -2608,20 +3389,27 @@ "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true }, "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { - "yallist": "^3.0.2" + "yallist": "^4.0.0" } }, + "lunr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", + "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==", + "dev": true + }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" @@ -2648,6 +3436,12 @@ "p-defer": "^1.0.0" } }, + "marked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.0.0.tgz", + "integrity": "sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng==", + "dev": true + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2655,13 +3449,12 @@ "dev": true }, "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-6.1.0.tgz", + "integrity": "sha512-RlbnLQgRHk5lwqTtpEkBTQ2ll/CG/iB+J4Hy2Wh97PjgZgXgWJWrFF+XXujh3UUVLvR4OOTgZzcWMMwnehlEUg==", "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.0.0" } }, "merge-descriptors": { @@ -2670,6 +3463,12 @@ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2685,32 +3484,28 @@ "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true }, "mime-types": { "version": "2.1.24", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, "requires": { "mime-db": "1.40.0" } }, "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.0.0.tgz", + "integrity": "sha512-PiVO95TKvhiwgSwg1IdLYlCTdul38yZxZMIcnDSFIBUm4BNZha2qpQ4GpJ++15bHoKDtrW2D69lMfFwdFYtNZQ==" }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2735,14 +3530,21 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "mocha": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", - "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", "dev": true, "requires": { "ansi-colors": "3.2.3", "browser-stdout": "1.3.1", + "chokidar": "3.3.0", "debug": "3.2.6", "diff": "3.5.0", "escape-string-regexp": "1.0.5", @@ -2751,18 +3553,18 @@ "growl": "1.10.5", "he": "1.2.0", "js-yaml": "3.13.1", - "log-symbols": "2.2.0", + "log-symbols": "3.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", + "mkdirp": "0.5.5", "ms": "2.1.1", - "node-environment-flags": "1.0.5", + "node-environment-flags": "1.0.6", "object.assign": "4.1.0", "strip-json-comments": "2.0.1", "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", "yargs-unparser": "1.6.0" }, "dependencies": { @@ -2789,21 +3591,6 @@ "path-is-absolute": "^1.0.0" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -2821,17 +3608,29 @@ } } }, - "module-not-found-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "mri": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.5.tgz", + "integrity": "sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg==", "dev": true }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multimatch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", + "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + } }, "mute-stream": { "version": "0.0.7", @@ -2839,6 +3638,12 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nanoid": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.10.tgz", + "integrity": "sha512-iZFMXKeXWkxzlfmMfM91gw7YhN2sdJtixY+eZh9V6QWJWTOiurhpKhBMgr82pfzgSqglQgqYSCowEYsz8D++6w==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2851,6 +3656,12 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -2912,21 +3723,15 @@ } }, "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", "dev": true, "requires": { "object.getownpropertydescriptors": "^2.0.3", "semver": "^5.7.0" } }, - "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", - "dev": true - }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -2936,21 +3741,38 @@ "process-on-spawn": "^1.0.0" } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "normalize-url": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" }, - "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", - "dev": true + "npm-run-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", + "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + } + } }, "nyc": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", - "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, "requires": { "@istanbuljs/load-nyc-config": "^1.0.0", @@ -2961,6 +3783,7 @@ "find-cache-dir": "^3.2.0", "find-up": "^4.1.0", "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", "glob": "^7.1.6", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-hook": "^3.0.0", @@ -2968,10 +3791,9 @@ "istanbul-lib-processinfo": "^2.0.2", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.0", - "js-yaml": "^3.13.1", + "istanbul-reports": "^3.0.2", "make-dir": "^3.0.0", - "node-preload": "^0.2.0", + "node-preload": "^0.2.1", "p-map": "^3.0.0", "process-on-spawn": "^1.0.0", "resolve-from": "^5.0.0", @@ -2979,7 +3801,6 @@ "signal-exit": "^3.0.2", "spawn-wrap": "^2.0.0", "test-exclude": "^6.0.0", - "uuid": "^3.3.3", "yargs": "^15.0.2" }, "dependencies": { @@ -3092,9 +3913,9 @@ "dev": true }, "rimraf": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.1.tgz", - "integrity": "sha512-IQ4ikL8SjBiEDZfk+DFVwqRK8md24RWMEJkdSlgNLkyyAImcjf8SWvU1qFMDOb4igBClbTQ/ugPqXcRwdFTxZw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -3132,9 +3953,9 @@ } }, "yargs": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", - "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -3147,13 +3968,13 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^16.1.0" + "yargs-parser": "^18.1.1" } }, "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -3207,6 +4028,72 @@ "es-abstract": "^1.17.0-next.1" } }, + "oidc-provider": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-6.28.0.tgz", + "integrity": "sha512-kSg2A6KV2pFbL/vgACSZBxgZQVcpyu5J+qWLeJshSC15aGqtp/wo+BVxujuBGUAneWAbcw40WunWbDvnSWShnQ==", + "dev": true, + "requires": { + "@koa/cors": "^3.1.0", + "@types/koa": "^2.11.3", + "debug": "^4.1.1", + "ejs": "^3.1.3", + "got": "^9.6.0", + "jose": "^1.27.2", + "jsesc": "^3.0.1", + "koa": "^2.13.0", + "koa-compose": "^4.1.0", + "lru-cache": "^6.0.0", + "nanoid": "^3.1.10", + "object-hash": "^2.0.3", + "oidc-token-hash": "^5.0.0", + "raw-body": "^2.4.1" + }, + "dependencies": { + "jose": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-1.27.2.tgz", + "integrity": "sha512-zLIwnMa8dh5A2jFo56KvhiXCaW0hFjdNvG0I5GScL8Wro+/r/SnyIYTbnX3fYztPNSfgQp56sDMHUuS9c3e6bw==", + "dev": true, + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, + "jsesc": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.1.tgz", + "integrity": "sha512-w+MMxnByppM4jwskitZotEtvtO3a2C7WOz31NxJToGisHuysCAQQU7umb/pA/6soPFe8LGjXFEFbuPuLEPm7Ag==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "oidc-token-hash": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz", @@ -3251,29 +4138,133 @@ } } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", + "dev": true + }, + "opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "dev": true + }, "openid-client": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.14.1.tgz", - "integrity": "sha512-CEnl3gJFND9g86UxlJ0yfOi9AL59UXCbqoD7f0+DDbMNZK7KCcqGszzxaanO7S4C64DJALxX2FXRXfjNv+8a7Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.0.0.tgz", + "integrity": "sha512-v86SGTNw2aqhZfKIHuqRjyJJ+Ra8vnFnTaRY2z9c+vB+fyM8IlYI6G0F6ZgQDHYIAe4InMsrBNYo0C9HlLY/4w==", "requires": { - "@types/got": "^9.6.9", "base64url": "^3.0.1", - "got": "^9.6.0", - "jose": "^1.25.0", - "lodash": "^4.17.15", - "lru-cache": "^5.1.1", + "got": "^11.5.2", + "jose": "^2.0.0", + "lru-cache": "^6.0.0", "make-error": "^1.3.6", "object-hash": "^2.0.1", "oidc-token-hash": "^5.0.0", "p-any": "^3.0.0" }, "dependencies": { - "jose": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-1.25.0.tgz", - "integrity": "sha512-IaS/sJzBPIlgnSW+SnJr7We8cXrzKKwwJe765c50aSbB1u46CSaTYxgYRfFnXRZAPxGyQhkhPHUkcLvYmhiDpw==", + "@sindresorhus/is": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz", + "integrity": "sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==" + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", "requires": { - "@panva/asn1.js": "^1.0.0" + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "11.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-11.6.1.tgz", + "integrity": "sha512-6izGvOsrd/4CsIdQMgweFOTCtS4sAwJTuCzIuVoTbCDzt3+wa3eGIHhSIMgEF6gfCDenslGlMUmAdPap5DkirQ==", + "requires": { + "@sindresorhus/is": "^3.1.1", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "keyv": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz", + "integrity": "sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" } } } @@ -3317,22 +4308,30 @@ "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, "p-is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true }, "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -3357,12 +4356,12 @@ } }, "p-memoize": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-3.1.0.tgz", - "integrity": "sha512-e5tIvrsr7ydUUnxb534iQWtXxWgk/86IsH+H+nV4FHouIggBt4coXboKBt26o4lTu7JbEnGSeXdEsYR8BhAHFA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-4.0.0.tgz", + "integrity": "sha512-oMxCJKVS75Bf2RWtXJNQNaX2K1G0FYpllOh2iTsPXZqnf9dWMcis3BL+pRdLeQY8lIdwwL01k/UV5LBdcVhZzg==", "requires": { - "mem": "^4.3.0", - "mimic-fn": "^2.1.0" + "mem": "^6.0.1", + "mimic-fn": "^3.0.0" } }, "p-some": { @@ -3408,11 +4407,17 @@ "callsites": "^3.0.0" } }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } }, "parseurl": { "version": "1.3.3", @@ -3456,34 +4461,120 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "pathval": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, - "pem-jwk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pem-jwk/-/pem-jwk-2.0.0.tgz", - "integrity": "sha512-rFxu7rVoHgQ5H9YsP50dDWf0rHjreVA2z0yPiWr5WdH/UHb29hKtF7h6l8vNd1cbYR1t0QL+JKhW55a2ZV4KtA==", + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", "dev": true, "requires": { - "asn1.js": "^5.0.1" + "semver-compare": "^1.0.0" } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + }, + "pretty-quick": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-2.0.1.tgz", + "integrity": "sha512-y7bJt77XadjUr+P1uKqZxFWLddvj3SKY6EU4BuQtMxmmEFSMpbN132pUWdSG1g1mtUfO0noBvn7wBf0BVeomHg==", "dev": true, "requires": { - "find-up": "^4.0.0" + "chalk": "^2.4.2", + "execa": "^2.1.0", + "find-up": "^4.1.0", + "ignore": "^5.1.4", + "mri": "^1.1.4", + "multimatch": "^4.0.0" }, "dependencies": { "find-up": { @@ -3496,6 +4587,12 @@ "path-exists": "^4.0.0" } }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3522,23 +4619,6 @@ } } }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" - }, "process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -3570,16 +4650,11 @@ "ipaddr.js": "1.9.0" } }, - "proxyquire": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", - "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", - "dev": true, - "requires": { - "fill-keys": "^1.0.2", - "module-not-found-error": "^1.0.1", - "resolve": "^1.11.1" - } + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "psl": { "version": "1.4.0", @@ -3602,12 +4677,54 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "puppeteer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.2.1.tgz", + "integrity": "sha512-PZoZG7u+T6N1GFWBQmGVG162Ak5MAy8nYSVpeeQrwJK2oYUlDWpHEJPcd/zopyuEMTv7DiztS1blgny1txR2qw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.781568", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "mime": "^2.0.3", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "dependencies": { + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", "dev": true }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3647,6 +4764,35 @@ } } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", @@ -3663,9 +4809,9 @@ } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -3675,7 +4821,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -3685,17 +4831,11 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -3703,20 +4843,10 @@ "dev": true }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } } } }, @@ -3761,6 +4891,11 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3771,6 +4906,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, "requires": { "lowercase-keys": "^1.0.0" } @@ -3824,30 +4960,24 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "dev": true, - "requires": { - "xmlchars": "^2.1.1" - } - }, - "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", - "dev": true, - "requires": { - "node-forge": "0.9.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -3869,6 +4999,23 @@ "statuses": "~1.5.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -3915,6 +5062,17 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -3936,6 +5094,12 @@ "supports-color": "^5.5.0" } }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -3968,9 +5132,9 @@ }, "dependencies": { "rimraf": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.1.tgz", - "integrity": "sha512-IQ4ikL8SjBiEDZfk+DFVwqRK8md24RWMEJkdSlgNLkyyAImcjf8SWvU1qFMDOb4igBClbTQ/ugPqXcRwdFTxZw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -4031,24 +5195,63 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" } }, "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "strip-ansi": { @@ -4066,6 +5269,12 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -4081,12 +5290,6 @@ "has-flag": "^3.0.0" } }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -4127,6 +5330,31 @@ } } }, + "tar-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", + "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", + "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "dev": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4168,7 +5396,17 @@ "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } }, "toidentifier": { "version": "1.0.0", @@ -4185,21 +5423,18 @@ "punycode": "^2.1.1" } }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -4230,6 +5465,12 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4249,6 +5490,62 @@ "is-typedarray": "^1.0.0" } }, + "typedoc": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.17.8.tgz", + "integrity": "sha512-/OyrHCJ8jtzu+QZ+771YaxQ9s4g5Z3XsQE3Ma7q+BL392xxBn4UMvvCdVnqKC2T/dz03/VXSLVKOP3lHmDdc/w==", + "dev": true, + "requires": { + "fs-extra": "^8.1.0", + "handlebars": "^4.7.6", + "highlight.js": "^10.0.0", + "lodash": "^4.17.15", + "lunr": "^2.3.8", + "marked": "1.0.0", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shelljs": "^0.8.4", + "typedoc-default-themes": "^0.10.2" + } + }, + "typedoc-default-themes": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.2.tgz", + "integrity": "sha512-zo09yRj+xwLFE3hyhJeVHWRSPuKEIAsFK5r2u47KL/HBKqpwdUSanoaz5L34IKiSATFrjG5ywmIu98hPVMfxZg==", + "dev": true, + "requires": { + "lunr": "^2.3.8" + } + }, + "typescript": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz", + "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", + "dev": true + }, + "uglify-js": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", + "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", + "dev": true, + "optional": true + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4273,10 +5570,17 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, "requires": { "prepend-http": "^2.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4306,58 +5610,6 @@ "extsprintf": "^1.2.0" } }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4373,6 +5625,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -4442,9 +5700,9 @@ } }, "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "requires": { "imurmurhash": "^0.1.4", @@ -4454,24 +5712,9 @@ } }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", "dev": true }, "y18n": { @@ -4481,14 +5724,20 @@ "dev": true }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { "cliui": "^5.0.0", @@ -4500,7 +5749,7 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" }, "dependencies": { "ansi-regex": { @@ -4532,9 +5781,9 @@ } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -4551,6 +5800,22 @@ "lodash": "^4.17.15", "yargs": "^13.3.0" } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", + "dev": true } } } diff --git a/package.json b/package.json index 1e1fdf07..72a38082 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-openid-connect", - "version": "1.0.2", + "version": "2.0.0", "description": "Express middleware to protect web applications using OpenID Connect.", "homepage": "https://github.com/auth0/express-openid-connect", "license": "MIT", @@ -12,45 +12,64 @@ "index.d.ts" ], "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": "^2.0.0", "on-headers": "^1.0.2", - "openid-client": "^3.14.1", - "p-memoize": "^3.1.0", + "openid-client": "^4.0.0", + "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" + "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } } } diff --git a/test/appSession.tests.js b/test/appSession.tests.js index f9a6004c..bc093361 100644 --- a/test/appSession.tests.js +++ b/test/appSession.tests.js @@ -1,163 +1,408 @@ const assert = require('chai').assert; +const crypto = require('crypto'); +const request = require('request-promise-native').defaults({ + simple: false, + resolveWithFullResponse: true, +}); +const sinon = require('sinon'); + const appSession = require('../lib/appSession'); -const sessionEncryption = require('./fixture/sessionEncryption'); +const { encrypted } = require('./fixture/sessionEncryption'); +const { makeIdToken } = require('./fixture/cert'); +const { get: getConfig } = require('../lib/config'); +const { create: createServer } = require('./fixture/server'); const defaultConfig = { - name: 'appSession', + clientID: '__test_client_id__', + clientSecret: '__test_client_secret__', + issuerBaseURL: 'https://op.example.com', + baseURL: 'https://example.org', secret: '__test_secret__', - duration: 3155760000, // 100 years + errorOnRequiredAuth: true, }; -const req = { - get: (key) => key +const login = async (claims) => { + const jar = request.jar(); + await request.post('/session', { + baseUrl, + jar, + json: { + id_token: makeIdToken(claims), + }, + }); + return jar; }; -const next = () => true; -describe('appSession', function() { +const baseUrl = 'http://localhost:3000'; - describe('no session cookies, no session property', () => { - const appSessionMw = appSession(defaultConfig); - const result = appSessionMw(req, {}, next); +const HR_MS = 60 * 60 * 1000; - it('should call next', function() { - assert.ok(result); - }); +describe('appSession', () => { + let server; - it('should set an empty appSession', function() { - assert.isEmpty(req.appSession); - }); + afterEach(() => { + if (server) { + server.close(); + } }); - describe('no session cookies, existing session property', () => { - const appSessionMw = appSession(defaultConfig); - const thisReq = Object.assign({}, req, {appSession: {sub: '__test_existing_sub__'}}); - const result = appSessionMw(thisReq, {}, next); + it('should not create a session when there are no cookies', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const res = await request.get('/session', { baseUrl, json: true }); + assert.isEmpty(res.body); + }); - it('should call next', function() { - assert.ok(result); + it('should not error for malformed sessions', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const res = await request.get('/session', { + baseUrl, + json: true, + headers: { + cookie: 'appSession=__invalid_identity__', + }, }); + assert.equal(res.statusCode, 200); + assert.isEmpty(res.body); + }); - it('should keep existing appSession', function() { - assert.equal(thisReq.appSession.sub, '__test_existing_sub__'); + it('should not error with JWEDecryptionFailed when using old secrets', async () => { + server = await createServer( + appSession( + getConfig({ + ...defaultConfig, + secret: 'another secret', + }) + ) + ); + const res = await request.get('/session', { + baseUrl, + json: true, + headers: { + cookie: `appSession=${encrypted}`, + }, }); + assert.equal(res.statusCode, 200); + assert.isEmpty(res.body); }); - describe('malformed session cookies', () => { - const appSessionMw = appSession(defaultConfig); - const thisReq = {get: () => 'appSession=__invalid_identity__'}; - - it('should error with malformed appSession', function() { - assert.throws(() => appSessionMw(thisReq, {}, next), Error, 'JWE malformed or invalid serialization'); + it('should get an existing session', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const res = await request.get('/session', { + baseUrl, + json: true, + headers: { + cookie: `appSession=${encrypted}`, + }, }); + assert.equal(res.statusCode, 200); + assert.equal(res.body.sub, '__test_sub__'); }); - describe('session cookies with old secrets', () => { - const thisReq = {get: () => 'appSession=' + sessionEncryption.encrypted}; - const appSessionMw = appSession({ ...defaultConfig, secret: 'another secret' }); - - it('should not error with JWEDecryptionFailed appSession', function() { - const result = appSessionMw(thisReq, {}, next); - assert.ok(result); - assert.isEmpty(req.appSession); + it('should chunk and accept chunked cookies over 4kb', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = request.jar(); + const random = crypto.randomBytes(4000).toString('base64'); + await request.post('/session', { + baseUrl, + jar, + json: { + sub: '__test_sub__', + random, + }, + }); + assert.deepEqual( + jar.getCookies(baseUrl).map(({ key }) => key), + ['appSession.0', 'appSession.1'] + ); + const res = await request.get('/session', { baseUrl, json: true, jar }); + assert.equal(res.statusCode, 200); + assert.deepEqual(res.body, { + sub: '__test_sub__', + random, }); }); - describe('session cookies with rotated secrets', () => { - const thisReq = {get: () => 'appSession=' + sessionEncryption.encrypted}; - - it('should use the old valid secret and re-encrypt using the new one', function() { - let appSessionMw = appSession({ ...defaultConfig, secret: ['new secret', '__test_secret__'] }); - let result = appSessionMw(thisReq, {}, next); - assert.ok(result); - assert.equal(thisReq.appSession.sub, '__test_sub__'); - appSessionMw = appSession({ ...defaultConfig, secret: 'new secret' }); - result = appSessionMw(thisReq, {}, next); - assert.ok(result); - assert.equal(thisReq.appSession.sub, '__test_sub__'); + it('should handle unordered chunked cookies', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = request.jar(); + const random = crypto.randomBytes(4000).toString('base64'); + await request.post('/session', { + baseUrl, + jar, + json: { + sub: '__test_sub__', + random, + }, + }); + const newJar = request.jar(); + jar + .getCookies(baseUrl) + .reverse() + .forEach(({ key, value }) => + newJar.setCookie(`${key}=${value}`, baseUrl) + ); + assert.deepEqual( + newJar.getCookies(baseUrl).map(({ key }) => key), + ['appSession.1', 'appSession.0'] + ); + const res = await request.get('/session', { + baseUrl, + json: true, + jar: newJar, + }); + assert.equal(res.statusCode, 200); + assert.deepEqual(res.body, { + sub: '__test_sub__', + random, }); }); - describe('existing session cookies', () => { - const appSessionMw = appSession(defaultConfig); - const thisReq = {get: () => 'appSession=' + sessionEncryption.encrypted}; + it('should not throw for malformed cookie chunks', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = request.jar(); + jar.setCookie('appSession.0=foo', baseUrl); + jar.setCookie('appSession.1=bar', baseUrl); + const res = await request.get('/session', { baseUrl, json: true, jar }); + assert.equal(res.statusCode, 200); + }); - it('should set the appSession on req', function() { - const result = appSessionMw(thisReq, {}, next); - assert.ok(result); - assert.equal(thisReq.appSession.sub, '__test_sub__'); + it('should set the default cookie options', async () => { + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = request.jar(); + await request.get('/session', { + baseUrl, + json: true, + jar, + headers: { + cookie: `appSession=${encrypted}`, + }, + }); + const [cookie] = jar.getCookies(baseUrl); + assert.deepInclude(cookie, { + key: 'appSession', + domain: 'localhost', + path: '/', + httpOnly: true, + extensions: ['SameSite=Lax'], }); + const expDate = new Date(cookie.expires); + const now = Date.now(); + assert.approximately(Math.floor((expDate - now) / 1000), 86400, 5); }); - describe('sessioncookie options', () => { - let cookieArgs; - const thisRes = { - cookie: function cookie() {cookieArgs = JSON.parse(JSON.stringify(arguments)); }, - writeHead: () => null, - setHeader: () => null - }; + it('should set the custom cookie options', async () => { + server = await createServer( + appSession( + getConfig({ + ...defaultConfig, + session: { + cookie: { + httpOnly: false, + sameSite: 'Strict', + }, + }, + }) + ) + ); + const jar = request.jar(); + await request.get('/session', { + baseUrl, + json: true, + jar, + headers: { + cookie: `appSession=${encrypted}`, + }, + }); + const [cookie] = jar.getCookies(baseUrl); + assert.deepInclude(cookie, { + key: 'appSession', + httpOnly: false, + extensions: ['SameSite=Strict'], + }); + }); - beforeEach(function() { - cookieArgs = {}; + it('should use a custom cookie name', async () => { + server = await createServer( + appSession( + getConfig({ + ...defaultConfig, + session: { name: 'customName' }, + }) + ) + ); + const jar = request.jar(); + const res = await request.get('/session', { + baseUrl, + json: true, + jar, + headers: { + cookie: `customName=${encrypted}`, + }, }); + const [cookie] = jar.getCookies(baseUrl); + assert.equal(res.statusCode, 200); + assert.equal(cookie.key, 'customName'); + }); - it('should set the correct cookie by default', function() { - const thisReq = {get: () => 'appSession=' + sessionEncryption.encrypted}; - const appSessionMw = appSession(defaultConfig); - const result = appSessionMw(thisReq, thisRes, next); - thisRes.writeHead(); + it('should set an ephemeral cookie', async () => { + server = await createServer( + appSession( + getConfig({ + ...defaultConfig, + session: { cookie: { transient: true } }, + }) + ) + ); + const jar = request.jar(); + const res = await request.get('/session', { + baseUrl, + json: true, + jar, + headers: { + cookie: `appSession=${encrypted}`, + }, + }); + const [cookie] = jar.getCookies(baseUrl); + assert.equal(res.statusCode, 200); + assert.isFalse(cookie.hasOwnProperty('expires')); + }); - assert.ok(result); - assert.equal(cookieArgs['0'], 'appSession'); - assert.isNotEmpty(cookieArgs['1']); - assert.isObject(cookieArgs['2']); - assert.hasAllKeys(cookieArgs['2'], ['expires']); + it('should not throw for expired cookies', async () => { + const twoWeeks = 2 * 7 * 24 * 60 * 60 * 1000; + const clock = sinon.useFakeTimers({ + now: Date.now(), + toFake: ['Date'], + }); + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = request.jar(); + clock.tick(twoWeeks); + const res = await request.get('/session', { + baseUrl, + json: true, + jar, + headers: { + cookie: `appSession=${encrypted}`, + }, + }); + assert.equal(res.statusCode, 200); + clock.restore(); + }); - const expDate = new Date(cookieArgs['2'].expires); - assert.equal( (expDate.getFullYear() - (new Date()).getFullYear()), 100); + it('should throw for duplicate mw', async () => { + server = await createServer((req, res, next) => { + req.appSession = {}; + appSession(getConfig(defaultConfig))(req, res, next); }); + const res = await request.get('/session', { baseUrl, json: true }); + assert.equal(res.statusCode, 500); + assert.equal( + res.body.err.message, + 'req[appSession] is already set, did you run this middleware twice?' + ); + }); - it('should set the correct custom cookie name', function() { - const thisReq = {get: () => 'customName=' + sessionEncryption.encrypted}; - const customConfig = Object.assign({}, defaultConfig, {name: 'customName'}); - const appSessionMw = appSession(customConfig); - const result = appSessionMw(thisReq, thisRes, next); - thisRes.writeHead(); + it('should throw for reassigning session', async () => { + server = await createServer((req, res, next) => { + appSession(getConfig(defaultConfig))(req, res, () => { + req.appSession = {}; + next(); + }); + }); + const res = await request.get('/session', { baseUrl, json: true }); + assert.equal(res.statusCode, 500); + assert.equal(res.body.err.message, 'session object cannot be reassigned'); + }); - assert.ok(result); - assert.equal(cookieArgs['0'], 'customName'); + it('should not throw for reassigining session to empty', async () => { + server = await createServer((req, res, next) => { + appSession(getConfig(defaultConfig))(req, res, () => { + req.appSession = null; + req.appSession = undefined; + next(); + }); }); + const res = await request.get('/session', { baseUrl, json: true }); + assert.equal(res.statusCode, 200); + }); - it('should set an ephemeral cookie', function() { - const thisReq = {get: () => 'appSession=' + sessionEncryption.encrypted}; - const customConfig = Object.assign({}, defaultConfig, {cookieTransient: true}); - const appSessionMw = appSession(customConfig); - const result = appSessionMw(thisReq, thisRes, next); - thisRes.writeHead(); + it('should expire after 24hrs of inactivity by default', async () => { + const clock = sinon.useFakeTimers({ toFake: ['Date'] }); + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = await login({ sub: '__test_sub__' }); + let res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isNotEmpty(res.body); + clock.tick(23 * HR_MS); + res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isNotEmpty(res.body); + clock.tick(25 * HR_MS); + res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isEmpty(res.body); + clock.restore(); + }); - assert.ok(result); - assert.equal(cookieArgs['2'].expires, 0); - }); + it('should expire after 7days regardless of activity by default', async () => { + const clock = sinon.useFakeTimers({ toFake: ['Date'] }); + server = await createServer(appSession(getConfig(defaultConfig))); + const jar = await login({ sub: '__test_sub__' }); + let days = 7; + while (days--) { + clock.tick(23 * HR_MS); + let res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isNotEmpty(res.body); + } + clock.tick(8 * HR_MS); + let res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isEmpty(res.body); + clock.restore(); + }); - it('should pass custom cookie options', function() { - const thisReq = {get: () => 'appSession=' + sessionEncryption.encrypted}; - const cookieOptConfig = { - cookieDomain: '__test_domain__', - cookiePath: '__test_path__', - cookieSecure: true, - cookieHttpOnly: false, - cookieSameSite: '__test_samesite__', - }; - const customConfig = Object.assign({}, defaultConfig, cookieOptConfig); - const appSessionMw = appSession(customConfig); - const result = appSessionMw(thisReq, thisRes, next); - thisRes.writeHead(); + it('should expire only after defined absoluteDuration', async () => { + const clock = sinon.useFakeTimers({ toFake: ['Date'] }); + server = await createServer( + appSession( + getConfig({ + ...defaultConfig, + session: { + rolling: false, + absoluteDuration: 10 * 60 * 60, + }, + }) + ) + ); + const jar = await login({ sub: '__test_sub__' }); + clock.tick(9 * HR_MS); + let res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isNotEmpty(res.body); + clock.tick(2 * HR_MS); + res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isEmpty(res.body); + clock.restore(); + }); - assert.ok(result); - assert.equal(cookieArgs['2'].domain, '__test_domain__'); - assert.equal(cookieArgs['2'].path, '__test_path__'); - assert.equal(cookieArgs['2'].secure, true); - assert.equal(cookieArgs['2'].httpOnly, false); - assert.equal(cookieArgs['2'].sameSite, '__test_samesite__'); - }); + it('should expire only after defined rollingDuration period of inactivty', async () => { + const clock = sinon.useFakeTimers({ toFake: ['Date'] }); + server = await createServer( + appSession( + getConfig({ + ...defaultConfig, + session: { + rolling: true, + rollingDuration: 24 * 60 * 60, + absoluteDuration: false, + }, + }) + ) + ); + const jar = await login({ sub: '__test_sub__' }); + let days = 30; + while (days--) { + clock.tick(23 * HR_MS); + let res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isNotEmpty(res.body); + } + clock.tick(25 * HR_MS); + let res = await request.get('/session', { baseUrl, jar, json: true }); + assert.isEmpty(res.body); + clock.restore(); }); }); diff --git a/test/attemptSilentLogin.tests.js b/test/attemptSilentLogin.tests.js new file mode 100644 index 00000000..17fa27c7 --- /dev/null +++ b/test/attemptSilentLogin.tests.js @@ -0,0 +1,158 @@ +const { assert } = require('chai'); +const { create: createServer } = require('./fixture/server'); +const { makeIdToken } = require('./fixture/cert'); +const { auth, attemptSilentLogin } = require('./..'); +const request = require('request-promise-native').defaults({ + simple: false, + resolveWithFullResponse: true, + followRedirect: false, +}); + +const baseUrl = 'http://localhost:3000'; + +const defaultConfig = { + secret: '__test_session_secret__', + clientID: '__test_client_id__', + baseURL: 'https://example.org', + issuerBaseURL: 'https://op.example.com', +}; + +const login = async (claims) => { + const jar = request.jar(); + await request.post('/session', { + baseUrl, + jar, + json: { + id_token: makeIdToken(claims), + }, + }); + return jar; +}; + +describe('attemptSilentLogin', () => { + let server; + + afterEach(async () => { + if (server) { + server.close(); + } + }); + + it("should attempt silent login on user's first route", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + attemptSilentLogin() + ); + const jar = request.jar(); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 302); + assert.include(jar.getCookies(baseUrl)[0], { + key: 'skipSilentLogin', + value: 'true', + httpOnly: true, + }); + }); + + it('should not attempt silent login for non html requests', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + attemptSilentLogin() + ); + const jar = request.jar(); + const response = await request({ + baseUrl, + jar, + url: '/protected', + json: true, + }); + + assert.equal(response.statusCode, 200); + }); + + it("should not attempt silent login on user's subsequent routes", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + attemptSilentLogin() + ); + const jar = request.jar(); + const response = await request({ baseUrl, jar, url: '/protected' }); + assert.equal(response.statusCode, 302); + const response2 = await request({ baseUrl, jar, url: '/protected' }); + assert.equal(response2.statusCode, 200); + const response3 = await request({ baseUrl, jar, url: '/protected' }); + assert.equal(response3.statusCode, 200); + }); + + it('should not attempt silent login for authenticated user', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + attemptSilentLogin() + ); + const jar = await login(); + const response = await request({ baseUrl, jar, url: '/protected' }); + assert.equal(response.statusCode, 200); + }); + + it('should not attempt silent login after first anonymous request after logout', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + attemptSilentLogin() + ); + const jar = await login(); + await request({ baseUrl, jar, url: '/protected' }); + await request.get({ + uri: '/logout', + baseUrl, + jar, + followRedirect: false, + }); + const response = await request({ baseUrl, jar, url: '/protected' }); + assert.equal(response.statusCode, 200); + }); + + it('should not attempt silent login after first request is to logout', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + attemptSilentLogin() + ); + const jar = await login(); + await request.get({ + uri: '/logout', + baseUrl, + jar, + followRedirect: false, + }); + const response = await request({ baseUrl, jar, url: '/protected' }); + assert.equal(response.statusCode, 200); + }); + + it("should throw when there's no auth middleware", async () => { + server = await createServer(attemptSilentLogin()); + const { + body: { err }, + } = await request({ baseUrl, url: '/protected', json: true }); + assert.equal( + err.message, + 'req.oidc is not found, did you include the auth middleware?' + ); + }); +}); diff --git a/test/auth.tests.js b/test/auth.tests.js deleted file mode 100644 index dbeed79c..00000000 --- a/test/auth.tests.js +++ /dev/null @@ -1,304 +0,0 @@ -const assert = require('chai').assert; -const url = require('url'); -const request = require('request-promise-native').defaults({ - simple: false, - resolveWithFullResponse: true -}); - -const { decodeState } = require('../lib/hooks/getLoginState'); - -const expressOpenid = require('..'); -const server = require('./fixture/server'); - -const filterRoute = (method, path) => { - return r => r.route && - r.route.path === path && - r.route.methods[method.toLowerCase()]; -}; - -const getCookieFromResponse = (res, cookieName) => { - const cookieHeaders = res.headers['set-cookie']; - - const foundHeader = cookieHeaders.filter(header => header.substring(0,6) === cookieName + '=')[0]; - if (!foundHeader) { - return false; - } - - const cookieValuePart = foundHeader.split('; ')[0]; - if (!cookieValuePart) { - return false; - } - - return cookieValuePart.split('=')[1]; -}; - -const defaultConfig = { - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - required: false -}; - -function getRouter (customConfig) { - return expressOpenid.auth(Object.assign({}, defaultConfig, customConfig)); -} - -describe('auth', function() { - describe('default', () => { - let baseUrl, router; - - before(async function() { - router = getRouter(); - baseUrl = await server.create(router); - }); - - it('should contain a login route', function() { - assert.ok(router.stack.some(filterRoute('GET', '/login'))); - }); - - it('should contain a logout route', function() { - assert.ok(router.stack.some(filterRoute('GET', '/logout'))); - }); - - it('should contain a callback route', function() { - assert.ok(router.stack.some(filterRoute('POST', '/callback'))); - }); - - it('should redirect to the authorize url properly on /login', async function() { - const jar = request.jar(); - const res = await request.get('/login', { jar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - assert.equal(parsed.hostname, 'test.auth0.com'); - assert.equal(parsed.pathname, '/authorize'); - assert.equal(parsed.query.client_id, '__test_client_id__'); - assert.equal(parsed.query.scope, 'openid profile email'); - assert.equal(parsed.query.response_type, 'id_token'); - assert.equal(parsed.query.response_mode, 'form_post'); - assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); - assert.property(parsed.query, 'nonce'); - assert.property(parsed.query, 'state'); - - const cookies = jar.getCookies(baseUrl + '/login'); - - assert.equal(cookies.filter(cookie => cookie.key === '_nonce')[0].value, parsed.query.nonce); - assert.equal(cookies.filter(cookie => cookie.key === '_state')[0].value, parsed.query.state); - }); - - }); - - describe('implied response_mode', () => { - describe('response_type=none', () => { - let baseUrl, router; - - before(async function() { - router = getRouter({authorizationParams: { - response_mode: undefined, - response_type: 'none', - }}); - baseUrl = await server.create(router); - }); - - it('should redirect to the authorize url properly on /login', async function() { - const cookieJar = request.jar(); - const res = await request.get('/login', { cookieJar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - - assert.equal(parsed.hostname, 'test.auth0.com'); - assert.equal(parsed.pathname, '/authorize'); - assert.equal(parsed.query.client_id, '__test_client_id__'); - assert.equal(parsed.query.scope, 'openid profile email'); - assert.equal(parsed.query.response_type, 'none'); - assert.equal(parsed.query.response_mode, undefined); - assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); - assert.property(parsed.query, 'nonce'); - assert.property(parsed.query, 'state'); - }); - - it('should contain a callback route', function() { - assert.ok(router.stack.some(filterRoute('GET', '/callback'))); - }); - }); - - describe('response_type=code', () => { - let baseUrl, router; - - before(async function() { - router = getRouter({ - clientSecret: '__test_client_secret__', - authorizationParams: { - response_mode: undefined, - response_type: 'code', - } - }); - baseUrl = await server.create(router); - }); - - it('should redirect to the authorize url properly on /login', async function() { - const cookieJar = request.jar(); - const res = await request.get('/login', { cookieJar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - - assert.equal(parsed.hostname, 'test.auth0.com'); - assert.equal(parsed.pathname, '/authorize'); - assert.equal(parsed.query.client_id, '__test_client_id__'); - assert.equal(parsed.query.scope, 'openid profile email'); - assert.equal(parsed.query.response_type, 'code'); - assert.equal(parsed.query.response_mode, undefined); - assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); - assert.property(parsed.query, 'nonce'); - assert.property(parsed.query, 'state'); - assert.property(res.headers, 'set-cookie'); - - assert.equal(getCookieFromResponse(res, 'nonce'), parsed.query.nonce); - assert.equal(getCookieFromResponse(res, 'state'), parsed.query.state); - }); - - it('should contain a callback route', function() { - assert.ok(router.stack.some(filterRoute('GET', '/callback'))); - }); - }); - - describe('response_type=id_token', () => { - let baseUrl, router; - - before(async function() { - router = getRouter({authorizationParams: { - response_mode: undefined, - response_type: 'id_token', - }}); - baseUrl = await server.create(router); - }); - - it('should redirect to the authorize url properly on /login', async function() { - const cookieJar = request.jar(); - const res = await request.get('/login', { cookieJar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - - assert.equal(parsed.hostname, 'test.auth0.com'); - assert.equal(parsed.pathname, '/authorize'); - assert.equal(parsed.query.client_id, '__test_client_id__'); - assert.equal(parsed.query.scope, 'openid profile email'); - assert.equal(parsed.query.response_type, 'id_token'); - assert.equal(parsed.query.response_mode, 'form_post'); - assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); - assert.property(parsed.query, 'nonce'); - assert.property(parsed.query, 'state'); - }); - - it('should contain the two callbacks route', function() { - assert.ok(router.stack.some(filterRoute('POST', '/callback'))); - }); - - }); - }); - - describe('custom path values', () => { - let baseUrl, router; - - before(async function() { - router = getRouter({ - redirectUriPath: 'custom-callback', - loginPath: 'custom-login', - logoutPath: 'custom-logout', - }); - - baseUrl = await server.create(router); - }); - - it('should contain the custom login route', function() { - assert.ok(router.stack.some(filterRoute('GET', '/custom-login'))); - }); - - it('should contain the custom logout route', function() { - assert.ok(router.stack.some(filterRoute('GET', '/custom-logout'))); - }); - - it('should contain the custom callback route', function() { - assert.ok(router.stack.some(filterRoute('POST', '/custom-callback'))); - }); - - it('should redirect to the authorize url properly on /login', async function() { - const jar = request.jar(); - const res = await request.get('/custom-login', { jar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - assert.equal(parsed.hostname, 'test.auth0.com'); - assert.equal(parsed.pathname, '/authorize'); - assert.equal(parsed.query.redirect_uri, 'https://example.org/custom-callback'); - }); - - }); - - describe('custom login parameter values', () => { - - it('should redirect to the authorize url properly on /login', async function() { - const router = getRouter({routes: false}); - router.get('/login', (req, res) => { - res.openid.login({ - returnTo: 'https://example.org/custom-redirect', - authorizationParams: { - response_type: 'code', - response_mode: 'query', - scope: 'openid email', - } - }); - }); - const baseUrl = await server.create(router); - - const cookieJar = request.jar(); - const res = await request.get('/login', { cookieJar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - - assert.equal(parsed.hostname, 'test.auth0.com'); - assert.equal(parsed.pathname, '/authorize'); - assert.equal(parsed.query.scope, 'openid email'); - assert.equal(parsed.query.response_type, 'code'); - assert.equal(parsed.query.response_mode, 'query'); - assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); - assert.property(parsed.query, 'nonce'); - - const decodedState = decodeState(parsed.query.state); - - assert.equal(decodedState.returnTo, 'https://example.org/custom-redirect'); - assert.isTrue(decodedState.nonce.length >= 16); - assert.notEqual(decodedState.nonce, parsed.query.nonce); - }); - - }); - - describe('custom state building', () => { - - it('should use a custom state builder', async function() { - const router = getRouter({getLoginState: (req, opts) => { - return { - returnTo: opts.returnTo + '/custom-page', - customProp: '__test_custom_prop__', - }; - }}); - const baseUrl = await server.create(router); - - const cookieJar = request.jar(); - const res = await request.get('/login', { cookieJar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - - const parsed = url.parse(res.headers.location, true); - const decodedState = decodeState(parsed.query.state); - - assert.equal(decodedState.returnTo, 'https://example.org/custom-page'); - assert.equal(decodedState.customProp, '__test_custom_prop__'); - }); - - }); -}); diff --git a/test/callback.tests.js b/test/callback.tests.js new file mode 100644 index 00000000..18abd6e7 --- /dev/null +++ b/test/callback.tests.js @@ -0,0 +1,675 @@ +const assert = require('chai').assert; +const sinon = require('sinon'); +const jose = require('jose'); +const request = require('request-promise-native').defaults({ + simple: false, + resolveWithFullResponse: true, +}); + +const TransientCookieHandler = require('../lib/transientHandler'); +const { encodeState } = require('../lib/hooks/getLoginState'); +const { auth } = require('..'); +const { create: createServer } = require('./fixture/server'); +const { makeIdToken } = require('./fixture/cert'); +const clientID = '__test_client_id__'; +const expectedDefaultState = encodeState({ returnTo: 'https://example.org' }); +const nock = require('nock'); + +const baseUrl = 'http://localhost:3000'; +const defaultConfig = { + secret: '__test_session_secret__', + clientID: clientID, + baseURL: 'https://example.org', + issuerBaseURL: 'https://op.example.com', + authRequired: false, +}; +let server; + +const setup = async (params) => { + const authOpts = Object.assign({}, defaultConfig, params.authOpts || {}); + const router = params.router || auth(authOpts); + const transient = new TransientCookieHandler(authOpts); + + const jar = params.jar || request.jar(); + server = await createServer(router); + let tokenReqHeader; + let tokenReqBody; + + Object.keys(params.cookies).forEach(function (cookieName) { + let value; + transient.store( + cookieName, + {}, + { + cookie(key, ...args) { + if (key === cookieName) { + value = args[0]; + } + }, + }, + { value: params.cookies[cookieName] } + ); + + jar.setCookie( + `${cookieName}=${value}; Max-Age=3600; Path=/; HttpOnly;`, + baseUrl + '/callback' + ); + }); + + const { + interceptors: [interceptor], + } = nock('https://op.example.com', { allowUnmocked: true }) + .post('/oauth/token') + .reply(200, function (uri, requestBody) { + tokenReqHeader = this.req.headers; + tokenReqBody = requestBody; + return { + access_token: '__test_access_token__', + refresh_token: '__test_refresh_token__', + id_token: params.body.id_token, + token_type: 'Bearer', + expires_in: 86400, + }; + }); + + const response = await request.post('/callback', { + baseUrl, + jar, + json: params.body, + }); + const currentUser = await request + .get('/user', { baseUrl, jar, json: true }) + .then((r) => r.body); + const tokens = await request + .get('/tokens', { baseUrl, jar, json: true }) + .then((r) => r.body); + + nock.removeInterceptor(interceptor); + + return { + baseUrl, + jar, + response, + currentUser, + tokenReqHeader, + tokenReqBody, + tokens, + }; +}; + +// For the purpose of this test the fake SERVER returns the error message in the body directly +// production application should have an error middleware. +// http://expressjs.com/en/guide/error-handling.html + +describe('callback response_mode: form_post', () => { + afterEach(() => { + if (server) { + server.close(); + } + }); + + it('should error when the body is empty', async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: { + nonce: '__test_nonce__', + state: '__test_state__', + }, + body: true, + }); + assert.equal(statusCode, 400); + assert.equal(err.message, 'state missing from the response'); + }); + + it('should error when the state is missing', async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: {}, + body: { + state: '__test_state__', + id_token: '__invalid_token__', + }, + }); + assert.equal(statusCode, 400); + assert.equal(err.message, 'checks.state argument is missing'); + }); + + it("should error when state doesn't match", async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: { + nonce: '__test_nonce__', + state: '__valid_state__', + }, + body: { + state: '__invalid_state__', + }, + }); + assert.equal(statusCode, 400); + assert.match(err.message, /state mismatch/i); + }); + + it("should error when id_token can't be parsed", async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: { + nonce: '__test_nonce__', + state: '__test_state__', + }, + body: { + state: '__test_state__', + id_token: '__invalid_token__', + }, + }); + assert.equal(statusCode, 400); + assert.equal( + err.message, + 'failed to decode JWT (JWTMalformed: JWTs must have three components)' + ); + }); + + it('should error when id_token has invalid alg', async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: { + nonce: '__test_nonce__', + state: '__test_state__', + }, + body: { + state: '__test_state__', + id_token: jose.JWT.sign({ sub: '__test_sub__' }, 'secret', { + algorithm: 'HS256', + }), + }, + }); + assert.equal(statusCode, 400); + assert.match(err.message, /unexpected JWT alg received/i); + }); + + it('should error when id_token is missing issuer', async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: { + nonce: '__test_nonce__', + state: '__test_state__', + }, + body: { + state: '__test_state__', + id_token: makeIdToken({ iss: undefined }), + }, + }); + assert.equal(statusCode, 400); + assert.match(err.message, /missing required JWT property iss/i); + }); + + it('should error when nonce is missing from cookies', async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + cookies: { + state: '__test_state__', + }, + body: { + state: '__test_state__', + id_token: makeIdToken(), + }, + }); + assert.equal(statusCode, 400); + assert.match(err.message, /nonce mismatch/i); + }); + + it('should error when legacy samesite fallback is off', async () => { + const { + response: { + statusCode, + body: { err }, + }, + } = await setup({ + authOpts: { + // Do not check the fallback cookie value. + legacySameSiteCookie: false, + }, + cookies: { + // Only set the fallback cookie value. + _state: '__test_state__', + }, + body: { + state: '__test_state__', + id_token: '__invalid_token__', + }, + }); + assert.equal(statusCode, 400); + assert.equal(err.message, 'checks.state argument is missing'); + }); + + it('should not strip claims when using custom claim filtering', async () => { + const { currentUser } = await setup({ + authOpts: { + identityClaimFilter: [], + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: makeIdToken(), + }, + }); + assert.equal(currentUser.iss, 'https://op.example.com/'); + assert.equal(currentUser.aud, clientID); + assert.equal(currentUser.nonce, '__test_nonce__'); + assert.exists(currentUser.iat); + assert.exists(currentUser.exp); + }); + + it('should expose the id token when id_token is valid', async () => { + const idToken = makeIdToken(); + const { + response: { statusCode, headers }, + currentUser, + tokens, + } = await setup({ + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + }, + }); + assert.equal(statusCode, 302); + assert.equal(headers.location, 'https://example.org'); + assert.ok(currentUser); + assert.equal(currentUser.sub, '__test_sub__'); + assert.equal(currentUser.nickname, '__test_nickname__'); + assert.notExists(currentUser.iat); + assert.notExists(currentUser.iss); + assert.notExists(currentUser.aud); + assert.notExists(currentUser.exp); + assert.notExists(currentUser.nonce); + assert.equal(tokens.isAuthenticated, true); + assert.equal(tokens.idToken, idToken); + assert.isUndefined(tokens.refreshToken); + assert.isUndefined(tokens.accessToken); + assert.include(tokens.idTokenClaims, { + sub: '__test_sub__', + }); + }); + + it("should expose all tokens when id_token is valid and response_type is 'code id_token'", async () => { + const idToken = makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ', + }); + + const { tokens } = await setup({ + authOpts: { + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + code: 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y', + }, + }); + + assert.equal(tokens.isAuthenticated, true); + assert.equal(tokens.idToken, idToken); + assert.equal(tokens.refreshToken, '__test_refresh_token__'); + assert.include(tokens.accessToken, { + access_token: '__test_access_token__', + token_type: 'Bearer', + }); + assert.include(tokens.idTokenClaims, { + sub: '__test_sub__', + }); + }); + + it('should handle access token expiry', async () => { + const clock = sinon.useFakeTimers({ toFake: ['Date'] }); + const idToken = makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ', + }); + const hrSecs = 60 * 60; + const hrMs = hrSecs * 1000; + + const { tokens, jar } = await setup({ + authOpts: { + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code', + }, + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + code: 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y', + }, + }); + assert.equal(tokens.accessToken.expires_in, 24 * hrSecs); + clock.tick(4 * hrMs); + const tokens2 = await request + .get('/tokens', { baseUrl, jar, json: true }) + .then((r) => r.body); + assert.equal(tokens2.accessToken.expires_in, 20 * hrSecs); + assert.isFalse(tokens2.accessTokenExpired); + clock.tick(21 * hrMs); + const tokens3 = await request + .get('/tokens', { baseUrl, jar, json: true }) + .then((r) => r.body); + assert.isTrue(tokens3.accessTokenExpired); + clock.restore(); + }); + + it('should refresh an access token', async () => { + const idToken = makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ', + }); + + const authOpts = { + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }; + const router = auth(authOpts); + router.get('/refresh', async (req, res) => { + const accessToken = await req.oidc.accessToken.refresh(); + res.json({ + accessToken, + refreshToken: req.oidc.refreshToken, + }); + }); + + const { tokens, jar } = await setup({ + router, + authOpts: { + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + code: 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y', + }, + }); + + const reply = sinon.spy(() => ({ + access_token: '__new_access_token__', + refresh_token: '__new_refresh_token__', + id_token: tokens.idToken, + token_type: 'Bearer', + expires_in: 86400, + })); + const { + interceptors: [interceptor], + } = nock('https://op.example.com', { allowUnmocked: true }) + .post('/oauth/token') + .reply(200, reply); + + const newTokens = await request + .get('/refresh', { baseUrl, jar, json: true }) + .then((r) => r.body); + nock.removeInterceptor(interceptor); + + sinon.assert.calledWith( + reply, + '/oauth/token', + 'grant_type=refresh_token&refresh_token=__test_refresh_token__' + ); + + assert.equal(tokens.accessToken.access_token, '__test_access_token__'); + assert.equal(tokens.refreshToken, '__test_refresh_token__'); + assert.equal(newTokens.accessToken.access_token, '__new_access_token__'); + assert.equal(newTokens.refreshToken, '__new_refresh_token__'); + + const newerTokens = await request + .get('/tokens', { baseUrl, jar, json: true }) + .then((r) => r.body); + + assert.equal( + newerTokens.accessToken.access_token, + '__new_access_token__', + 'the new access token should be persisted in the session' + ); + }); + + it('should refresh an access token and keep original refresh token', async () => { + const idToken = makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ', + }); + + const authOpts = { + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }; + const router = auth(authOpts); + router.get('/refresh', async (req, res) => { + const accessToken = await req.oidc.accessToken.refresh(); + res.json({ + accessToken, + refreshToken: req.oidc.refreshToken, + }); + }); + + const { tokens, jar } = await setup({ + router, + authOpts: { + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + code: 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y', + }, + }); + + const reply = sinon.spy(() => ({ + access_token: '__new_access_token__', + id_token: tokens.id_token, + token_type: 'Bearer', + expires_in: 86400, + })); + const { + interceptors: [interceptor], + } = nock('https://op.example.com', { allowUnmocked: true }) + .post('/oauth/token') + .reply(200, reply); + + const newTokens = await request + .get('/refresh', { baseUrl, jar, json: true }) + .then((r) => r.body); + nock.removeInterceptor(interceptor); + + sinon.assert.calledWith( + reply, + '/oauth/token', + 'grant_type=refresh_token&refresh_token=__test_refresh_token__' + ); + + assert.equal(tokens.accessToken.access_token, '__test_access_token__'); + assert.equal(tokens.refreshToken, '__test_refresh_token__'); + assert.equal(newTokens.accessToken.access_token, '__new_access_token__'); + assert.equal(newTokens.refreshToken, '__test_refresh_token__'); + }); + + it('should fetch userinfo', async () => { + const idToken = makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ', + }); + + const authOpts = { + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email', + }, + }; + const router = auth(authOpts); + router.get('/user-info', async (req, res) => { + res.json(await req.oidc.fetchUserInfo()); + }); + + const { jar } = await setup({ + router, + authOpts: { + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + code: 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y', + }, + }); + + const { + interceptors: [interceptor], + } = nock('https://op.example.com', { allowUnmocked: true }) + .get('/userinfo') + .reply(200, () => ({ + userInfo: true, + sub: '__test_sub__', + })); + + const userInfo = await request + .get('/user-info', { baseUrl, jar, json: true }) + .then((r) => r.body); + + nock.removeInterceptor(interceptor); + + assert.deepEqual(userInfo, { userInfo: true, sub: '__test_sub__' }); + }); + + it('should use basic auth on token endpoint when using code flow', async () => { + const idToken = makeIdToken({ + c_hash: '77QmUPtjPfzWtF2AnpK9RQ', + }); + + const { tokenReqBody, tokenReqHeader } = await setup({ + authOpts: { + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + audience: 'https://api.example.com/', + scope: 'openid profile email read:reports offline_access', + }, + }, + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + code: 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y', + }, + }); + + const credentials = Buffer.from( + tokenReqHeader.authorization.replace('Basic ', ''), + 'base64' + ); + assert.equal(credentials, '__test_client_id__:__test_client_secret__'); + assert.match( + tokenReqBody, + /code=jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y/ + ); + }); + + it('should resume silent logins when user successfully logs in', async () => { + const idToken = makeIdToken(); + const jar = request.jar(); + jar.setCookie('skipSilentLogin=true', baseUrl); + await setup({ + cookies: { + _state: expectedDefaultState, + _nonce: '__test_nonce__', + skipSilentLogin: '1', + }, + body: { + state: expectedDefaultState, + id_token: idToken, + }, + jar, + }); + const cookies = jar.getCookies(baseUrl); + assert.notOk(cookies.find(({ key }) => key === 'skipSilentLogin')); + }); +}); diff --git a/test/callback_route_form_post.tests.js b/test/callback_route_form_post.tests.js deleted file mode 100644 index 2c762c1b..00000000 --- a/test/callback_route_form_post.tests.js +++ /dev/null @@ -1,313 +0,0 @@ -const assert = require('chai').assert; -const jwt = require('jsonwebtoken'); -const request = require('request-promise-native').defaults({ - simple: false, - resolveWithFullResponse: true -}); - -const { encodeState } = require('../lib/hooks/getLoginState'); -const expressOpenid = require('..'); -const server = require('./fixture/server'); -const cert = require('./fixture/cert'); -const clientID = '__test_client_id__'; -const expectedDefaultState = encodeState({ returnTo: 'https://example.org' }); - -function testCase(params) { - return () => { - const authOpts = Object.assign({}, { - appSession: {secret: '__test_session_secret__'}, - clientID: clientID, - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - required: false - }, params.authOpts || {}); - const router = expressOpenid.auth(authOpts); - - let baseUrl; - - const jar = request.jar(); - - before(async function() { - this.jar = jar; - this.baseUrl = baseUrl = await server.create(router); - - Object.keys(params.cookies).forEach(function(cookieName) { - jar.setCookie( - `${cookieName}=${params.cookies[cookieName]}; Max-Age=3600; Path=/; HttpOnly;`, - baseUrl + '/callback', - ); - }); - - this.response = await request.post('/callback', {baseUrl, jar, json: params.body}); - this.currentUser = await request.get('/user', {baseUrl, jar, json: true}).then(r => r.body); - }); - - params.assertions(); - }; -} - -function makeIdToken(payload) { - if (typeof payload !== 'object' ) { - payload = { - 'nickname': '__test_nickname__', - 'sub': '__test_sub__', - 'iss': 'https://test.auth0.com/', - 'aud': clientID, - 'iat': Math.round(Date.now() / 1000), - 'exp': Math.round(Date.now() / 1000) + 60000, - 'nonce': '__test_nonce__' - }; - } - - return jwt.sign(payload, cert.key, { algorithm: 'RS256', header: { kid: cert.kid } }); -} - -//For the purpose of this test the fake SERVER returns the error message in the body directly -//production application should have an error middleware. -//http://expressjs.com/en/guide/error-handling.html - - -describe('callback routes response_type: id_token, response_mode: form_post', function() { - describe('when body is empty', testCase({ - cookies: { - nonce: '__test_nonce__', - state: '__test_state__' - }, - body: true, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.equal(this.response.body.err.message, 'state missing from the response'); - }); - } - })); - - describe('when state is missing', testCase({ - cookies: {}, - body: { - state: '__test_state__', - id_token: '__invalid_token__' - }, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.equal(this.response.body.err.message, 'checks.state argument is missing'); - }); - } - })); - - describe("when state doesn't match", testCase({ - cookies: { - nonce: '__test_nonce__', - state: '__valid_state__' - }, - body: { - state: '__invalid_state__' - }, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.match(this.response.body.err.message, /state mismatch/i); - }); - } - })); - - describe("when id_token can't be parsed", testCase({ - cookies: { - nonce: '__test_nonce__', - state: '__test_state__' - }, - body: { - state: '__test_state__', - id_token: '__invalid_token__' - }, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.equal( - this.response.body.err.message, - 'failed to decode JWT (JWTMalformed: JWTs must have three components)' - ); - }); - } - })); - - describe('when id_token has invalid alg', testCase({ - cookies: { - nonce: '__test_nonce__', - state: '__test_state__' - }, - body: { - state: '__test_state__', - id_token: jwt.sign({sub: '__test_sub__'}, '__invalid_alg__') - }, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.match(this.response.body.err.message, /unexpected JWT alg received/i); - }); - } - })); - - describe('when id_token is missing issuer', testCase({ - cookies: { - nonce: '__test_nonce__', - state: '__test_state__' - }, - body: { - state: '__test_state__', - id_token: makeIdToken({sub: '__test_sub__'}) - }, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.match(this.response.body.err.message, /missing required JWT property iss/i); - }); - } - })); - - describe('when nonce is missing from cookies', testCase({ - cookies: { - state: '__test_state__' - }, - body: { - state: '__test_state__', - id_token: makeIdToken() - }, - assertions() { - it('should return the reason to the error handler', function() { - assert.match(this.response.body.err.message, /nonce mismatch/i); - }); - } - })); - - describe('when id_token is valid', testCase({ - cookies: { - _state: expectedDefaultState, - _nonce: '__test_nonce__' - }, - body: { - state: expectedDefaultState, - id_token: makeIdToken() - }, - assertions() { - it('should return 302', function() { - assert.equal(this.response.statusCode, 302); - }); - - it('should redirect to the intended url', function() { - assert.equal(this.response.headers['location'], 'https://example.org'); - }); - - it('should contain the claims in the current session', function() { - assert.ok(this.currentUser); - assert.equal(this.currentUser.sub, '__test_sub__'); - assert.equal(this.currentUser.nickname, '__test_nickname__'); - }); - - it('should strip validation claims from the ID tokens', function() { - assert.notExists(this.currentUser.iat); - assert.notExists(this.currentUser.iss); - assert.notExists(this.currentUser.aud); - assert.notExists(this.currentUser.exp); - assert.notExists(this.currentUser.nonce); - }); - - it('should expose the user in the request', async function() { - const res = await request.get('/user', { - baseUrl: this.baseUrl, - json: true, - jar: this.jar - }); - assert.equal(res.body.nickname, '__test_nickname__'); - }); - } - })); - - describe('when legacy samesite fallback is off', testCase({ - authOpts: { - // Do not check the fallback cookie value. - legacySameSiteCookie: false - }, - cookies: { - // Only set the fallback cookie value. - _state: '__test_state__' - }, - body: { - state: '__test_state__', - id_token: '__invalid_token__' - }, - assertions() { - it('should return 400', function() { - assert.equal(this.response.statusCode, 400); - }); - - it('should return the reason to the error handler', function() { - assert.equal(this.response.body.err.message, 'checks.state argument is missing'); - }); - } - })); - - describe('uses custom callback handling', testCase({ - authOpts: { - handleCallback: () => { - throw new Error('__test_callback_error__'); - } - }, - cookies: { - _state: expectedDefaultState, - _nonce: '__test_nonce__' - }, - body: { - state: expectedDefaultState, - id_token: makeIdToken() - }, - assertions() { - it('throws an error from the custom handler', function() { - assert.equal(this.response.body.err.message, '__test_callback_error__'); - }); - } - })); - - describe('uses custom claim filtering', testCase({ - authOpts: { - identityClaimFilter: [] - }, - cookies: { - _state: expectedDefaultState, - _nonce: '__test_nonce__' - }, - body: { - state: expectedDefaultState, - id_token: makeIdToken() - }, - assertions() { - it('should have previously-stripped claims', function() { - assert.equal(this.currentUser.iss, 'https://test.auth0.com/'); - assert.equal(this.currentUser.aud, clientID); - assert.equal(this.currentUser.nonce, '__test_nonce__'); - assert.exists(this.currentUser.iat); - assert.exists(this.currentUser.exp); - }); - } - })); - -}); diff --git a/test/client.tests.js b/test/client.tests.js index cec2ea47..71e9eb3f 100644 --- a/test/client.tests.js +++ b/test/client.tests.js @@ -4,166 +4,95 @@ const { get: getClient } = require('../lib/client'); const wellKnown = require('./fixture/well-known.json'); const nock = require('nock'); const pkg = require('../package.json'); -const sinon = require('sinon'); -const openidClient = require('openid-client'); -describe('client initialization', function() { - - beforeEach(async function() { - nock('https://test.auth0.com') +describe('client initialization', function () { + beforeEach(async function () { + nock('https://op.example.com') .post('/introspection') - .reply(200, function() { + .reply(200, function () { return this.req.headers; }); }); - describe('default case', function() { + describe('default case', function () { const config = getConfig({ - appSession: {secret: '__test_session_secret__'}, + secret: '__test_session_secret__', clientID: '__test_client_id__', clientSecret: '__test_client_secret__', - issuerBaseURL: 'https://test.auth0.com', + issuerBaseURL: 'https://op.example.com', baseURL: 'https://example.org', }); let client; - before(async function() { + before(async function () { client = await getClient(config); }); - it('should save the passed values', async function() { + it('should save the passed values', async function () { assert.equal('__test_client_id__', client.client_id); assert.equal('__test_client_secret__', client.client_secret); }); - it('should send the correct default headers', async function() { - const headers = await client.introspect('__test_token__', '__test_hint__'); + it('should send the correct default headers', async function () { + const headers = await client.introspect( + '__test_token__', + '__test_hint__' + ); const headerProps = Object.getOwnPropertyNames(headers); assert.include(headerProps, 'auth0-client'); - const decodedTelemetry = JSON.parse(Buffer.from(headers['auth0-client'], 'base64').toString('ascii')); + const decodedTelemetry = JSON.parse( + Buffer.from(headers['auth0-client'], 'base64').toString('ascii') + ); - assert.equal( 'express-oidc', decodedTelemetry.name ); - assert.equal( pkg.version, decodedTelemetry.version ); - assert.equal( process.version, decodedTelemetry.env.node ); + assert.equal('express-oidc', decodedTelemetry.name); + assert.equal(pkg.version, decodedTelemetry.version); + assert.equal(process.version, decodedTelemetry.env.node); - assert.include( headerProps, 'user-agent'); - assert.equal( `express-openid-connect/${pkg.version}`, headers['user-agent']); + assert.include(headerProps, 'user-agent'); + assert.equal( + `express-openid-connect/${pkg.version}`, + headers['user-agent'] + ); }); - it('should not strip new headers', async function() { - const response = await client.requestResource('https://test.auth0.com/introspection', 'token', { - method: 'POST', - headers: { - Authorization: 'Bearer foo', + it('should not strip new headers', async function () { + const response = await client.requestResource( + 'https://op.example.com/introspection', + 'token', + { + method: 'POST', + headers: { + Authorization: 'Bearer foo', + }, } - }); + ); const headerProps = Object.getOwnPropertyNames(JSON.parse(response.body)); assert.include(headerProps, 'authorization'); }); }); - describe('custom headers', function() { + describe('idTokenSigningAlg configuration is not overridden by discovery server', function () { const config = getConfig({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - clientSecret: '__test_client_secret__', - issuerBaseURL: 'https://test.auth0.com', - baseURL: 'https://example.org', - httpOptions: { - headers: { - 'User-Agent' : '__test_custom_user_agent__', - 'X-Custom-Header' : '__test_custom_header__', - 'Auth0-Client' : '__test_custom_telemetry__', - } - } - }); - - let client; - before(async function() { - client = await getClient(config); - }); - - it('should send the correct default headers', async function() { - const headers = await client.introspect('__test_token__', '__test_hint__'); - const headerProps = Object.getOwnPropertyNames(headers); - - // User agent header should be overridden. - assert.include(headerProps, 'user-agent'); - assert.equal('__test_custom_user_agent__', headers['user-agent']); - - // Custom header should be added. - assert.include(headerProps, 'x-custom-header'); - assert.equal('__test_custom_header__', headers['x-custom-header']); - - // Telemetry header should not be overridden. - assert.include(headerProps, 'auth0-client'); - assert.notEqual('__test_custom_telemetry__', headers['x-custom-header']); - }); - }); - - describe('telemetry header', function() { - const config = getConfig({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - clientSecret: '__test_client_secret__', - issuerBaseURL: 'https://test.auth0.com', - baseURL: 'https://example.org', - enableTelemetry: false - }); - - before(async function() { - sinon.spy(openidClient.custom, 'setHttpOptionsDefaults'); - await getClient(config); - }); - - after(function() { - openidClient.custom.setHttpOptionsDefaults.restore(); - }); - - it('should set the correct default headers', function() { - assert.doesNotHaveAnyKeys( - openidClient.custom.setHttpOptionsDefaults.firstCall.args[0].headers, - ['auth0-client'] - ); - }); - }); - - describe('idTokenAlg configuration is not overridden by discovery server', function() { - const config = getConfig({ - appSession: {secret: '__test_session_secret__'}, + secret: '__test_session_secret__', clientID: '__test_client_id__', clientSecret: '__test_client_secret__', issuerBaseURL: 'https://test-too.auth0.com', baseURL: 'https://example.org', - httpOptions: { - headers: { - 'User-Agent' : '__test_custom_user_agent__', - 'X-Custom-Header' : '__test_custom_header__', - 'Auth0-Client' : '__test_custom_telemetry__', - } - }, - idTokenAlg: 'RS256' - }); - - it('should fail if idTokenAlg is not supported by the idP', async function() { - nock('https://test-too.auth0.com') - .get('/.well-known/openid-configuration') - .reply(200, Object.assign({}, wellKnown, { - id_token_signing_alg_values_supported: ['none'] - })); - - await assert.isRejected(getClient(config), /^ID token algorithm "RS256" is not supported by the issuer./); + idTokenSigningAlg: 'RS256', }); - it('should prefer user configuration regardless of idP discovery', async function() { + it('should prefer user configuration regardless of idP discovery', async function () { nock('https://test-too.auth0.com') .get('/.well-known/openid-configuration') - .reply(200, Object.assign({}, wellKnown, { - id_token_signing_alg_values_supported: ['none', 'RS256'] - })); + .reply( + 200, + Object.assign({}, wellKnown, { + id_token_signing_alg_values_supported: ['none'], + }) + ); const client = await getClient(config); assert.equal(client.id_token_signed_response_alg, 'RS256'); diff --git a/test/config.tests.js b/test/config.tests.js index 56ad63f5..72904fca 100644 --- a/test/config.tests.js +++ b/test/config.tests.js @@ -1,230 +1,577 @@ const { assert } = require('chai'); +const sinon = require('sinon'); const { get: getConfig } = require('../lib/config'); const defaultConfig = { - appSession: {secret: '__test_session_secret__'}, + secret: '__test_session_secret__', clientID: '__test_client_id__', - issuerBaseURL: 'https://test.auth0.com', - baseURL: 'https://example.org' + issuerBaseURL: 'https://op.example.com', + baseURL: 'https://example.org', }; -describe('config', function() { - describe('simple case', function() { - const config = getConfig(defaultConfig); +const validateAuthorizationParams = (authorizationParams) => + getConfig({ ...defaultConfig, authorizationParams }); - it('should default to response_type=id_token', function() { - assert.equal(config.authorizationParams.response_type, 'id_token'); - }); +describe('get config', () => { + afterEach(() => sinon.restore()); - it('should default to response_mode=form_post', function() { - assert.equal(config.authorizationParams.response_mode, 'form_post'); + it('should get config for default config', () => { + const config = getConfig(defaultConfig); + assert.deepInclude(config, { + authorizationParams: { + response_type: 'id_token', + response_mode: 'form_post', + scope: 'openid profile email', + }, + authRequired: true, }); + }); - it('should default to scope=openid profile email', function() { - assert.equal(config.authorizationParams.scope, 'openid profile email'); + it('should get config for default config with environment variables', () => { + sinon.stub(process, 'env').value({ + ...process.env, + ISSUER_BASE_URL: defaultConfig.issuerBaseURL, + CLIENT_ID: defaultConfig.clientID, + SECRET: defaultConfig.secret, + BASE_URL: defaultConfig.baseURL, + }); + const config = getConfig(); + assert.deepInclude(config, { + authorizationParams: { + response_type: 'id_token', + response_mode: 'form_post', + scope: 'openid profile email', + }, + authRequired: true, }); + }); - it('should default to required true', function() { - assert.ok(config.required); + it('should get config for response_type=code', () => { + const config = getConfig({ + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code', + }, + }); + assert.deepInclude(config, { + authorizationParams: { + response_type: 'code', + scope: 'openid profile email', + }, + authRequired: true, }); }); - describe('simple case with environment variables', function() { - let config; - let env; + it('should require a fully qualified URL for issuer', () => { + const config = { + ...defaultConfig, + issuerBaseURL: 'www.example.com', + }; + assert.throws( + () => getConfig(config), + TypeError, + '"issuerBaseURL" must be a valid uri' + ); + }); - beforeEach(function() { - env = process.env; - process.env = Object.assign({}, process.env, { - ISSUER_BASE_URL: defaultConfig.issuerBaseURL, - CLIENT_ID: defaultConfig.clientID, - APP_SESSION_SECRET: defaultConfig.appSession.secret, - BASE_URL: defaultConfig.baseURL - }); - config = getConfig(); + it('should set idpLogout to true when auth0Logout is true', () => { + const config = getConfig({ + ...defaultConfig, + auth0Logout: true, }); - - afterEach(function() { - process.env = env; + assert.include(config, { + auth0Logout: true, + idpLogout: true, }); + }); - it('should default to response_type=id_token', function() { - assert.equal(config.authorizationParams.response_type, 'id_token'); + it('auth0Logout and idpLogout should default to false', () => { + const config = getConfig(defaultConfig); + assert.include(config, { + auth0Logout: false, + idpLogout: false, }); + }); - it('should default to response_mode=form_post', function() { - assert.equal(config.authorizationParams.response_mode, 'form_post'); + it('should not set auth0Logout to true when idpLogout is true', () => { + const config = getConfig({ + ...defaultConfig, + idpLogout: true, }); - - it('should default to scope=openid profile email', function() { - assert.equal(config.authorizationParams.scope, 'openid profile email'); + assert.include(config, { + auth0Logout: false, + idpLogout: true, }); + }); - it('should default to required true', function() { - assert.ok(config.required); + it('should set default route paths', () => { + const config = getConfig(defaultConfig); + assert.include(config.routes, { + callback: '/callback', + login: '/login', + logout: '/logout', }); }); - describe('when authorizationParams is response_type=code', function() { - const customConfig = Object.assign({}, defaultConfig, { - clientSecret: '__test_client_secret__', - authorizationParams: { - response_type: 'code' - } + it('should set custom route paths', () => { + const config = getConfig({ + ...defaultConfig, + routes: { + callback: '/custom-callback', + login: '/custom-login', + logout: '/custom-logout', + }, }); - const config = getConfig(customConfig); - - it('should set new response_type', function() { - assert.equal(config.authorizationParams.response_type, 'code'); + assert.include(config.routes, { + callback: '/custom-callback', + login: '/custom-login', + logout: '/custom-logout', }); + }); - it('should allow undefined response_mode', function() { - assert.equal(config.authorizationParams.response_mode, undefined); + it('should set default app session configuration', () => { + const config = getConfig(defaultConfig); + assert.deepInclude(config.session, { + rollingDuration: 86400, + name: 'appSession', + cookie: { + sameSite: 'Lax', + httpOnly: true, + transient: false, + }, }); + }); - it('should keep default scope', function() { - assert.equal(config.authorizationParams.scope, 'openid profile email'); + it('should set custom cookie configuration', () => { + const config = getConfig({ + ...defaultConfig, + secret: ['__test_session_secret_1__', '__test_session_secret_2__'], + session: { + name: '__test_custom_session_name__', + rollingDuration: 1234567890, + cookie: { + domain: '__test_custom_domain__', + transient: true, + httpOnly: false, + secure: true, + sameSite: 'Strict', + }, + }, + }); + assert.deepInclude(config, { + secret: ['__test_session_secret_1__', '__test_session_secret_2__'], + session: { + name: '__test_custom_session_name__', + rollingDuration: 1234567890, + absoluteDuration: 604800, + rolling: true, + cookie: { + domain: '__test_custom_domain__', + transient: true, + httpOnly: false, + secure: true, + sameSite: 'Strict', + }, + }, }); }); - describe('when authorizationParams response_type fuzzy matches issuer', function() { - const customConfig = Object.assign({}, defaultConfig, { - clientSecret: '__test_client_secret__', - authorizationParams: { - response_type: 'token id_token code' - } - }); - const config = getConfig(customConfig); + it('should fail when the baseURL is invalid', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + baseURL: '__invalid_url__', + }); + }, '"baseURL" must be a valid uri'); + }); - it('should keep token code', function() { - assert.equal(config.authorizationParams.response_type, 'token id_token code'); - }); + it('should fail when the clientID is not provided', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + clientID: undefined, + }); + }, '"clientID" is required'); }); - describe('with auth0Logout', function() { - const config = getConfig(Object.assign({}, defaultConfig, {auth0Logout: true})); + it('should fail when the baseURL is not provided', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + baseURL: undefined, + }); + }, '"baseURL" is required'); + }); - it('should set idpLogout to true', function() { - assert.equal(config.auth0Logout, true); - assert.equal(config.idpLogout, true); - }); + it('should fail when the secret is not provided', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + secret: undefined, + }); + }, '"secret" is required'); }); - describe('without auth0Logout nor idpLogout', function() { - const config = getConfig(defaultConfig); + it('should fail when app session length is not an integer', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + session: { + rollingDuration: 3.14159, + }, + }); + }, '"session.rollingDuration" must be an integer'); + }); - it('should set both to false', function() { - assert.equal(config.auth0Logout, false); - assert.equal(config.idpLogout, false); - }); + it('should fail when rollingDuration is defined and rolling is false', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + session: { + rolling: false, + rollingDuration: 100, + }, + }); + }, '"session.rollingDuration" must be false when "session.rolling" is disabled'); }); - describe('with idpLogout', function() { - const config = getConfig(Object.assign({}, defaultConfig, {idpLogout: true})); + it('should fail when rollingDuration is not defined and rolling is true', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + session: { + rolling: true, + rollingDuration: false, + }, + }); + }, '"session.rollingDuration" must be provided an integer value when "session.rolling" is true'); + }); - it('should set both to false', function() { - assert.equal(config.auth0Logout, false); - assert.equal(config.idpLogout, true); - }); + it('should fail when absoluteDuration is not defined and rolling is false', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + session: { + rolling: false, + absoluteDuration: false, + }, + }); + }, '"session.absoluteDuration" must be provided an integer value when "session.rolling" is false'); }); - describe('default auth paths', function() { - const config = getConfig(defaultConfig); + it('should fail when app session secret is invalid', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + secret: { key: '__test_session_secret__' }, + }); + }, '"secret" must be one of [string, binary, array]'); + }); - it('should set the default callback path', function() { - assert.equal(config.redirectUriPath, '/callback'); - }); + it('should fail when app session cookie httpOnly is not a boolean', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + session: { + cookie: { + httpOnly: '__invalid_httponly__', + }, + }, + }); + }, '"session.cookie.httpOnly" must be a boolean'); + }); - it('should set the default login path', function() { - assert.equal(config.loginPath, '/login'); - }); + it('should fail when app session cookie secure is not a boolean', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + secret: '__test_session_secret__', + session: { + cookie: { + secure: '__invalid_secure__', + }, + }, + }); + }, '"session.cookie.secure" must be a boolean'); + }); - it('should set the default logout path', function() { - assert.equal(config.logoutPath, '/logout'); - }); + it('should fail when app session cookie sameSite is invalid', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + secret: '__test_session_secret__', + session: { + cookie: { + sameSite: '__invalid_samesite__', + }, + }, + }); + }, '"session.cookie.sameSite" must be one of [Lax, Strict, None]'); }); - describe('custom auth paths', function() { - const customConfig = Object.assign({}, defaultConfig, { - redirectUriPath: '/custom-callback', - loginPath: '/custom-login', - logoutPath: '/custom-logout', - }); - const config = getConfig(customConfig); + it('should fail when app session cookie domain is invalid', function () { + assert.throws(() => { + getConfig({ + ...defaultConfig, + secret: '__test_session_secret__', + session: { + cookie: { + domain: false, + }, + }, + }); + }, '"session.cookie.domain" must be a string'); + }); - it('should accept the custom callback path', function() { - assert.equal(config.redirectUriPath, '/custom-callback'); - }); + it("shouldn't allow a secret of less than 8 chars", () => { + assert.throws( + () => getConfig({ ...defaultConfig, secret: 'short' }), + TypeError, + '"secret" does not match any of the allowed types' + ); + assert.throws( + () => getConfig({ ...defaultConfig, secret: ['short', 'too'] }), + TypeError, + '"secret[0]" does not match any of the allowed types' + ); + assert.throws( + () => getConfig({ ...defaultConfig, secret: Buffer.from('short') }), + TypeError, + '"secret" must be at least 8 bytes' + ); + }); - it('should accept the login path', function() { - assert.equal(config.loginPath, '/custom-login'); - }); + it("shouldn't allow code flow without clientSecret", () => { + const config = { + ...defaultConfig, + authorizationParams: { + response_type: 'code', + }, + }; + assert.throws( + () => getConfig(config), + TypeError, + '"clientSecret" is required for a response_type that includes code' + ); + }); - it('should accept the logout path', function() { - assert.equal(config.logoutPath, '/custom-logout'); - }); + it("shouldn't allow hybrid flow without clientSecret", () => { + const config = { + ...defaultConfig, + authorizationParams: { + response_type: 'code id_token', + }, + }; + assert.throws( + () => getConfig(config), + TypeError, + '"clientSecret" is required for a response_type that includes code' + ); }); - describe('app session default configuration', function() { - const config = getConfig(defaultConfig); + it('should not allow "none" for idTokenSigningAlg', () => { + let config = (idTokenSigningAlg) => + getConfig({ + ...defaultConfig, + idTokenSigningAlg, + }); + let expected = '"idTokenSigningAlg" contains an invalid value'; + assert.throws(() => config('none'), TypeError, expected); + assert.throws(() => config('NONE'), TypeError, expected); + assert.throws(() => config('noNE'), TypeError, expected); + }); - it('should set the app session secret', function() { - assert.equal(config.appSession.secret, '__test_session_secret__'); - }); + it('should require clientSecret for ID tokens with HMAC based algorithms', () => { + const config = { + ...defaultConfig, + idTokenSigningAlg: 'HS256', + authorizationParams: { + response_type: 'id_token', + }, + }; + assert.throws( + () => getConfig(config), + TypeError, + '"clientSecret" is required for ID tokens with HMAC based algorithms' + ); + }); - it('should set the session length to 1 day by default', function() { - assert.equal(config.appSession.duration, 86400); - }); + it('should require clientSecret for ID tokens in hybrid flow with HMAC based algorithms', () => { + const config = { + ...defaultConfig, + idTokenSigningAlg: 'HS256', + authorizationParams: { + response_type: 'code id_token', + }, + }; + assert.throws( + () => getConfig(config), + TypeError, + '"clientSecret" is required for ID tokens with HMAC based algorithms' + ); + }); - it('should set the session name to "appSession" by default', function() { - assert.equal(config.appSession.name, 'appSession'); - }); + it('should require clientSecret for ID tokens in code flow with HMAC based algorithms', () => { + const config = { + ...defaultConfig, + idTokenSigningAlg: 'HS256', + authorizationParams: { + response_type: 'code', + }, + }; + assert.throws( + () => getConfig(config), + TypeError, + '"clientSecret" is required for ID tokens with HMAC based algorithms' + ); + }); - it('should set the session cookie attributes to correct defaults', function() { - assert.notExists(config.appSession.cookieDomain); - assert.notExists(config.appSession.cookiePath); - assert.notExists(config.appSession.cookieSecure); - assert.equal(config.appSession.cookieSameSite, 'Lax'); - assert.equal(config.appSession.cookieHttpOnly, true); - }); + it('should allow empty auth params', () => { + assert.doesNotThrow(validateAuthorizationParams); + assert.doesNotThrow(() => validateAuthorizationParams({})); }); - describe('app session cookie configuration', function() { - const customConfig = Object.assign({}, defaultConfig, { - appSession: { - secret: [ '__test_session_secret_1__', '__test_session_secret_2__' ], - name: '__test_custom_session_name__', - duration: 1234567890, - cookieDomain: '__test_custom_domain__', - cookiePath: '__test_custom_path__', - cookieTransient: true, - cookieHttpOnly: false, - cookieSecure: true, - cookieSameSite: 'Strict', - } - }); - - it('should set an array of secrets', function() { - const config = getConfig(customConfig); - assert.equal(config.appSession.secret.length, 2); - assert.equal(config.appSession.secret[0], '__test_session_secret_1__'); - assert.equal(config.appSession.secret[1], '__test_session_secret_2__'); - }); - - it('should set the custom session values', function() { - const config = getConfig(customConfig); - assert.equal(config.appSession.duration, 1234567890); - assert.equal(config.appSession.name, '__test_custom_session_name__'); - }); - - it('should set the session cookie attributes to custom values', function() { - const config = getConfig(customConfig); - assert.equal(config.appSession.cookieDomain, '__test_custom_domain__'); - assert.equal(config.appSession.cookiePath, '__test_custom_path__'); - assert.equal(config.appSession.cookieTransient, true); - assert.equal(config.appSession.cookieHttpOnly, false); - assert.equal(config.appSession.cookieSecure, true); - assert.equal(config.appSession.cookieSameSite, 'Strict'); - }); + it('should not allow empty scope', () => { + assert.throws( + () => validateAuthorizationParams({ scope: null }), + TypeError, + '"authorizationParams.scope" must be a string' + ); + assert.throws( + () => validateAuthorizationParams({ scope: '' }), + TypeError, + '"authorizationParams.scope" is not allowed to be empty' + ); + }); + + it('should not allow scope without openid', () => { + assert.throws( + () => validateAuthorizationParams({ scope: 'profile email' }), + TypeError, + '"authorizationParams.scope" with value "profile email" fails to match the contains openid pattern' + ); + }); + + it('should allow scope with openid', () => { + assert.doesNotThrow(() => + validateAuthorizationParams({ scope: 'openid read:users' }) + ); + assert.doesNotThrow(() => + validateAuthorizationParams({ scope: 'read:users openid' }) + ); + assert.doesNotThrow(() => + validateAuthorizationParams({ scope: 'read:users openid profile email' }) + ); + }); + + it('should not allow empty response_type', () => { + assert.throws( + () => validateAuthorizationParams({ response_type: null }), + TypeError, + '"authorizationParams.response_type" must be one of [id_token, code id_token, code]' + ); + assert.throws( + () => validateAuthorizationParams({ response_type: '' }), + TypeError, + '"authorizationParams.response_type" must be one of [id_token, code id_token, code]' + ); + }); + + it('should not allow invalid response_types', () => { + assert.throws( + () => validateAuthorizationParams({ response_type: 'foo' }), + TypeError, + '"authorizationParams.response_type" must be one of [id_token, code id_token, code]' + ); + assert.throws( + () => validateAuthorizationParams({ response_type: 'foo id_token' }), + TypeError, + '"authorizationParams.response_type" must be one of [id_token, code id_token, code]' + ); + assert.throws( + () => validateAuthorizationParams({ response_type: 'id_token code' }), + TypeError, + '"authorizationParams.response_type" must be one of [id_token, code id_token, code]' + ); + }); + + it('should allow valid response_types', () => { + const config = (authorizationParams) => ({ + ...defaultConfig, + clientSecret: 'foo', + authorizationParams, + }); + assert.doesNotThrow(() => + validateAuthorizationParams({ response_type: 'id_token' }) + ); + assert.doesNotThrow(() => config({ response_type: 'code id_token' })); + assert.doesNotThrow(() => config({ response_type: 'code' })); + }); + + it('should not allow empty response_mode', () => { + assert.throws( + () => validateAuthorizationParams({ response_mode: null }), + TypeError, + '"authorizationParams.response_mode" must be [form_post]' + ); + assert.throws( + () => validateAuthorizationParams({ response_mode: '' }), + TypeError, + '"authorizationParams.response_mode" must be [form_post]' + ); + assert.throws( + () => + validateAuthorizationParams({ + response_type: 'code', + response_mode: '', + }), + TypeError, + '"authorizationParams.response_mode" must be one of [query, form_post]' + ); + }); + + it('should not allow response_type id_token and response_mode query', () => { + assert.throws( + () => + validateAuthorizationParams({ + response_type: 'id_token', + response_mode: 'query', + }), + TypeError, + '"authorizationParams.response_mode" must be [form_post]' + ); + assert.throws( + () => + validateAuthorizationParams({ + response_type: 'code id_token', + response_mode: 'query', + }), + TypeError, + '"authorizationParams.response_mode" must be [form_post]' + ); + }); + + it('should allow valid response_type response_mode combinations', () => { + const config = (authorizationParams) => ({ + ...defaultConfig, + clientSecret: 'foo', + authorizationParams, + }); + assert.doesNotThrow(() => + config({ response_type: 'code', response_mode: 'query' }) + ); + assert.doesNotThrow(() => + config({ response_type: 'code', response_mode: 'form_post' }) + ); + assert.doesNotThrow(() => + validateAuthorizationParams({ + response_type: 'id_token', + response_mode: 'form_post', + }) + ); + assert.doesNotThrow(() => + config({ response_type: 'code id_token', response_mode: 'form_post' }) + ); }); }); diff --git a/test/custom_redirect_uri.tests.js b/test/custom_redirect_uri.tests.js deleted file mode 100644 index 86e9ab7d..00000000 --- a/test/custom_redirect_uri.tests.js +++ /dev/null @@ -1,47 +0,0 @@ -const assert = require('chai').assert; -const url = require('url'); -const request = require('request-promise-native').defaults({ - simple: false, - resolveWithFullResponse: true -}); - -const expressOpenid = require('..'); -const server = require('./fixture/server'); -const filterRoute = (method, path) => { - return r => r.route && - r.route.path === path && - r.route.methods[method.toLowerCase()]; -}; - -describe('auth with redirectUriPath', function() { - describe('default', () => { - - let baseUrl, router; - - before(async function() { - router = expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - required: false, - redirectUriPath: '/auth-finish' - }); - baseUrl = await server.create(router); - }); - - it('should contain a callback route', function() { - assert.ok(router.stack.some(filterRoute('POST', '/auth-finish'))); - }); - - it('should have the correct redirect_uri parameter', async function() { - const jar = request.jar(); - const res = await request.get('/login', { jar, baseUrl, followRedirect: false }); - assert.equal(res.statusCode, 302); - const parsed = url.parse(res.headers.location, true); - assert.equal(parsed.query.redirect_uri, 'https://example.org/auth-finish'); - }); - - }); - -}); diff --git a/test/fixture/cert.js b/test/fixture/cert.js index d99d8244..d5c3626c 100644 --- a/test/fixture/cert.js +++ b/test/fixture/cert.js @@ -1,46 +1,47 @@ -const selfsigned = require('selfsigned'); -const { pem2jwk } = require('pem-jwk'); +const { JWK, JWKS, JWT } = require('jose'); -const attrs = [ - { - name: 'commonName', - value: 'test.auth0.com' - } -]; - -const extensions = [{ - name: 'basicConstraints', - cA: true, - critical: true -}, { - name: 'subjectKeyIdentifier' -}, { - name: 'keyUsage', - digitalSignature: true, - keyCertSign: true, - critical: true -}]; +const key = JWK.asKey({ + e: 'AQAB', + n: + 'wQrThQ9HKf8ksCQEzqOu0ofF8DtLJgexeFSQBNnMQetACzt4TbHPpjhTWUIlD8bFCkyx88d2_QV3TewMtfS649Pn5hV6adeYW2TxweAA8HVJxskcqTSa_ktojQ-cD43HIStsbqJhHoFv0UY6z5pwJrVPT-yt38ciKo9Oc9IhEl6TSw-zAnuNW0zPOhKjuiIqpAk1lT3e6cYv83ahx82vpx3ZnV83dT9uRbIbcgIpK4W64YnYb5uDH7hGI8-4GnalZDfdApTu-9Y8lg_1v5ul-eQDsLCkUCPkqBaNiCG3gfZUAKp9rrFRE_cJTv_MJn-y_XSTMWILvTY7vdSMRMo4kQ', + d: + 'EMHY1K8b1VhxndyykiGBVoM0uoLbJiT60eA9VD53za0XNSJncg8iYGJ5UcE9KF5v0lIQDIJfIN2tmpUIEW96HbbSZZWtt6xgbGaZ2eOREU6NJfVlSIbpgXOYUs5tFKiRBZ8YXY448gX4Z-k5x7W3UJTimqSH_2nw3FLuU32FI2vtf4ToUKEcoUdrIqoAwZ1et19E7Q_NCG2y1nez0LpD8PKgfeX1OVHdQm7434-9FS-R_eMcxqZ6mqZO2QDuign8SPHTR-KooAe8B-0MpZb7QF3YtMSQk8RlrMUcAYwv8R8dvFergCjauH0hOHvtKPq6Smj0VuimelEUZfp94r3pBQ', + p: + '9i2D_PLFPnFfztYccTGxzgiXezRpMsXD2Z9PA7uxw0sXnkV1TjZkSc3V_59RxyiTtvYlNCbGYShds__ogXouuYqbWaC43_zj3eGqAWL3i5C-k1u4S3ekgKn8AkGjlqCObuyLRsPvDfBkv1wo2tfIAEoNg_sHYIIRkTq68g58if8', + q: + 'yL6UUD_MB_pCHwf6LvNC2k0lfHHOxfW3lOo_XTqt9dg9yTO21OS4BF7Uce1kFJJIfuGrK6cMmusHKkSsJm1_khR3G9owokrBDFOZ_iSWvt3qIG5K3CNgl1_C8NqTeyKEVziCCiaL9CZpwfqHIVNnDCchGNkpVRqsfHmzPEnXnW8', + dp: + 'rFf3FEn9rpZ-pXYeGVzaBszbCAUMNOBhGWS_U3S-oWNb2JD169iGY2j4DWpDPTN6Hle6egU_UtuIpjBdXO_l8D1KPvgXFbCc8kQ-2ZOojAu8b7uBjUvoXa8jX40Gcrhanut5IgSfwlluns1tSLBSM2mkhqZiZr0IgWzlXfqoU48', + dq: + 'kihQC-2nO9e19Kn2OeDbt92bgXPLPM6ej0nOQK7MocaDlc6VO4QbhvMUcq6Iw4GOTvM3kVzbDKA6Y0gEnyXyUAWegyTlbARJchQcdrFlICqqoFotHwKS_SO352z9HBYRjP-TjphqJaUiMx2Y7WawDGUg79qNAW2eUDK7kRWiavk', + qi: + '8hAW25CmPjLAXpzkMpXpXsvJKdgql0Zjt-OeSVwzQN5dLYmu-Q98Xl5n8H-Nfr8aOmPfHBQ8M9FOMpxbgg8gbqixpkrxcTIGjpuH8RFYXj_0TYSBkCSOoc7tAP7YjOUOGJMqFHDYZVD-gmsCuRwWx3jKFxRrWLS5b8kWzkON0bM', + kty: 'RSA', + use: 'sig', + alg: 'RS256', +}); +module.exports.jwks = new JWKS.KeyStore(key).toJWKS(false); -const ss = selfsigned.generate(attrs, { - pkcs7: true, - days: 5000, - algorithm: 'sha256', - extensions: extensions -}); +module.exports.key = key.toPEM(true); +module.exports.kid = key.kid; +module.exports.makeIdToken = (payload) => { + payload = Object.assign( + { + nickname: '__test_nickname__', + sub: '__test_sub__', + iss: 'https://op.example.com/', + aud: '__test_client_id__', + iat: Math.round(Date.now() / 1000), + exp: Math.round(Date.now() / 1000) + 60000, + nonce: '__test_nonce__', + }, + payload + ); -module.exports.jwks = { - keys: [{ - alg: 'RS256', - kty: 'RSA', - use: 'sig', - kid: ss.fingerprint, - x5t: ss.fingerprint, - ...pem2jwk(ss.public) - }] + return JWT.sign(payload, key.toPEM(true), { + algorithm: 'RS256', + header: { kid: key.kid }, + }); }; - -module.exports.cert = ss.cert; -module.exports.key = ss.private; -module.exports.kid = ss.fingerprint; diff --git a/test/fixture/server.js b/test/fixture/server.js index 7cb5bb3c..0decf3a2 100644 --- a/test/fixture/server.js +++ b/test/fixture/server.js @@ -1,38 +1,54 @@ const express = require('express'); const bodyParser = require('body-parser'); -const http = require('http'); -module.exports.create = function(router, protect, path) { +module.exports.create = function (router, protect, path) { const app = express(); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); - app.use(router); + if (router) { + app.use(router); + } app.get('/session', (req, res) => { res.json(req.appSession); }); app.post('/session', (req, res) => { - req.appSession = req.body; + Object.keys(req.appSession).forEach((prop) => { + delete req.appSession[prop]; + }); + Object.assign(req.appSession, req.body); res.json(); }); app.get('/user', (req, res) => { - res.json(req.openid.user); + res.json(req.oidc.user); + }); + + app.get('/tokens', (req, res) => { + res.json({ + isAuthenticated: req.oidc.isAuthenticated(), + idToken: req.oidc.idToken, + refreshToken: req.oidc.refreshToken, + accessToken: req.oidc.accessToken, + accessTokenExpired: req.oidc.accessToken + ? req.oidc.accessToken.isExpired() + : undefined, + idTokenClaims: req.oidc.idTokenClaims, + }); }); if (protect) { app.get('/protected', protect, (req, res) => { - res.json(req.openid.tokens); + res.json({}); }); } // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { - res.status(err.status || 500) - .json({ err: { message: err.message }}); + res.status(err.status || 500).json({ err: { message: err.message } }); }); let mainApp; @@ -43,12 +59,7 @@ module.exports.create = function(router, protect, path) { mainApp = app; } - const server = http.createServer(mainApp); - return new Promise((resolve) => { - server.unref(); - server.listen(0, () => { - resolve(`http://localhost:${server.address().port}`); - }); + const server = mainApp.listen(3000, () => resolve(server)); }); }; diff --git a/test/fixture/sessionEncryption.js b/test/fixture/sessionEncryption.js index 66804094..7d9e68a8 100644 --- a/test/fixture/sessionEncryption.js +++ b/test/fixture/sessionEncryption.js @@ -1,27 +1,29 @@ const { JWK, JWE } = require('jose'); -const hkdf = require('futoin-hkdf'); -const deriveKey = (secret) => hkdf(secret, 32, { info: 'JWE CEK', hash: 'SHA-256' }); -const epoch = () => Date.now() / 1000 | 0; +const { encryption: deriveKey } = require('../../lib/hkdf'); +const epoch = () => (Date.now() / 1000) | 0; const key = JWK.asKey(deriveKey('__test_secret__')); -const payload = JSON.stringify({sub: '__test_sub__'}); +const payload = JSON.stringify({ sub: '__test_sub__' }); const epochNow = epoch(); const weekInSeconds = 7 * 24 * 60 * 60; const encryptOpts = { alg: 'dir', enc: 'A256GCM', - zip: 'DEF', uat: epochNow, iat: epochNow, - exp: epochNow + weekInSeconds + exp: epochNow + weekInSeconds, }; const jwe = JWE.encrypt(payload, key, encryptOpts); -const {cleartext} = JWE.decrypt(jwe, key, { complete: true, algorithms: [encryptOpts.enc] }); +const { cleartext } = JWE.decrypt(jwe, key, { + complete: true, + contentEncryptionAlgorithms: [encryptOpts.enc], + keyManagementAlgorithms: [encryptOpts.alg], +}); module.exports = { encrypted: jwe, - decrypted: cleartext + decrypted: cleartext, }; diff --git a/test/fixture/well-known.json b/test/fixture/well-known.json index b68c9ae2..41c3e537 100644 --- a/test/fixture/well-known.json +++ b/test/fixture/well-known.json @@ -1,19 +1,64 @@ { - "issuer": "https://test.auth0.com/", - "authorization_endpoint": "https://test.auth0.com/authorize", - "token_endpoint": "https://test.auth0.com/oauth/token", - "userinfo_endpoint": "https://test.auth0.com/userinfo", - "mfa_challenge_endpoint": "https://test.auth0.com/mfa/challenge", - "jwks_uri": "https://test.auth0.com/.well-known/jwks.json", - "registration_endpoint": "https://test.auth0.com/oidc/register", - "revocation_endpoint": "https://test.auth0.com/oauth/revoke", - "introspection_endpoint": "https://test.auth0.com/introspection", - "scopes_supported": ["openid", "profile", "offline_access", "name", "given_name", "family_name", "nickname", "email", "email_verified", "picture", "created_at", "identities", "phone", "address"], - "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"], + "issuer": "https://op.example.com/", + "authorization_endpoint": "https://op.example.com/authorize", + "token_endpoint": "https://op.example.com/oauth/token", + "userinfo_endpoint": "https://op.example.com/userinfo", + "mfa_challenge_endpoint": "https://op.example.com/mfa/challenge", + "jwks_uri": "https://op.example.com/.well-known/jwks.json", + "registration_endpoint": "https://op.example.com/oidc/register", + "revocation_endpoint": "https://op.example.com/oauth/revoke", + "introspection_endpoint": "https://op.example.com/introspection", + "scopes_supported": [ + "openid", + "profile", + "offline_access", + "name", + "given_name", + "family_name", + "nickname", + "email", + "email_verified", + "picture", + "created_at", + "identities", + "phone", + "address" + ], + "response_types_supported": [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token", + "none" + ], "response_modes_supported": ["query", "fragment", "form_post"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["HS256", "RS256"], - "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], - "claims_supported": ["aud", "auth_time", "created_at", "email", "email_verified", "exp", "family_name", "given_name", "iat", "identities", "iss", "name", "nickname", "phone_number", "picture", "sub"], - "request_uri_parameter_supported": false + "token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post" + ], + "claims_supported": [ + "aud", + "auth_time", + "created_at", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "identities", + "iss", + "name", + "nickname", + "phone_number", + "picture", + "sub" + ], + "request_uri_parameter_supported": false, + "end_session_endpoint": "https://op.example.com/session/end" } diff --git a/test/invalid_id_token_alg.tests.js b/test/invalid_id_token_alg.tests.js deleted file mode 100644 index d87c9e13..00000000 --- a/test/invalid_id_token_alg.tests.js +++ /dev/null @@ -1,29 +0,0 @@ -const assert = require('chai').assert; -const expressOpenid = require('..'); -const server = require('./fixture/server'); -const request = require('request-promise-native').defaults({ - simple: false, - resolveWithFullResponse: true -}); - -describe('with an invalid id token alg', function() { - - const router = expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - clientID: '123', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - idTokenAlg: '__invalid_alg__' - }); - - let baseUrl; - before(async function() { - baseUrl = await server.create(router); - }); - - it('should return an error', async function() { - const res = await request.get({ json: true, baseUrl, uri: '/login'}); - assert.equal(res.statusCode, 500); - assert.include(res.body.err.message, 'ID token algorithm "__invalid_alg__" is not supported by the issuer.'); - }); -}); diff --git a/test/invalid_params.tests.js b/test/invalid_params.tests.js deleted file mode 100644 index 289ab3dc..00000000 --- a/test/invalid_params.tests.js +++ /dev/null @@ -1,159 +0,0 @@ -const { assert } = require('chai'); -const expressOpenid = require('..'); - -const validConfiguration = { - appSession: {secret: '__test_session_secret__'}, - issuerBaseURL: 'https://test.auth0.com', - baseURL: 'https://example.org', - clientID: '__test_client_id__', -}; - -function getTestConfig(modify) { - return Object.assign({}, validConfiguration, modify); -} - -describe('invalid parameters', function() { - it('should fail when the issuerBaseURL is invalid', function() { - assert.throws(() => { - expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - baseURL: 'https://example.org', - issuerBaseURL: '__invalid_url__', - clientID: '__test_client_id__' - }); - }, '"issuerBaseURL" does not match any of the allowed types'); - }); - - it('should fail when the baseURL is invalid', function() { - assert.throws(() => { - expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - baseURL: '__invalid_url__', - issuerBaseURL: 'https://test.auth0.com', - clientID: '__test_client_id__' - }); - }, '"baseURL" must be a valid uri'); - }); - - it('should fail when the clientID is not provided', function() { - assert.throws(() => { - expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - }); - }, '"clientID" is required'); - }); - - it('should fail when the baseURL is not provided', function() { - assert.throws(() => { - expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - issuerBaseURL: 'https://test.auth0.com', - clientID: '__test_client_id__', - }); - }, '"baseURL" is required'); - }); - - it('should fail when the appSession.secret is not provided', function() { - assert.throws(() => { - expressOpenid.auth({ - issuerBaseURL: 'https://test.auth0.com', - baseURL: 'https://example.org', - clientID: '__test_client_id__', - }); - }, '"appSession.secret" is required'); - }); - - it('should fail when client secret is not provided and using the response type code in mode query', function() { - assert.throws(() => { - expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - issuerBaseURL: 'https://test.auth0.com', - baseURL: 'https://example.org', - clientID: '__test_client_id__', - authorizationParams: { - response_type: 'code id_token' - } - }); - }, '"clientSecret" is required for response_type code'); - }); - - it('should fail when client secret is not provided and using an HS256 ID token algorithm', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({idTokenAlg: 'HS256'})); - }, '"clientSecret" is required for ID tokens with HS algorithms'); - }); - - it('should fail when app session length is not an integer', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({ - appSession: { - secret: '__test_session_secret__', - duration: 3.14159 - } - })); - }, '"appSession.duration" must be an integer'); - }); - - it('should fail when app session secret is invalid', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({appSession: {secret: {key: '__test_session_secret__'}}})); - }, '"appSession.secret" must be one of [string, array]'); - }); - - it('should fail when app session cookie httpOnly is not a boolean', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({ - appSession: { - secret: '__test_session_secret__', - cookieHttpOnly: '__invalid_httponly__' - } - })); - }, '"appSession.cookieHttpOnly" must be a boolean'); - }); - - it('should fail when app session cookie secure is not a boolean', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({ - appSession: { - secret: '__test_session_secret__', - cookieSecure: '__invalid_secure__' - } - })); - }, '"appSession.cookieSecure" must be a boolean'); - }); - - it('should fail when app session cookie sameSite is invalid', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({ - appSession: { - secret: '__test_session_secret__', - cookieSameSite: '__invalid_samesite__' - } - })); - }, '"appSession.cookieSameSite" must be one of [Lax, Strict, None]'); - }); - - it('should fail when app session cookie domain is invalid', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({ - appSession: { - secret: '__test_session_secret__', - cookieDomain: false - } - })); - }, '"appSession.cookieDomain" must be a string'); - }); - - it('should fail when app session cookie sameSite is an invalid value', function() { - assert.throws(() => { - expressOpenid.auth(getTestConfig({ - appSession: { - secret: '__test_session_secret__', - cookiePath: 123 - } - })); - }, '"appSession.cookiePath" must be a string'); - }); -}); diff --git a/test/invalid_response_mode.tests.js b/test/invalid_response_mode.tests.js deleted file mode 100644 index 83511c25..00000000 --- a/test/invalid_response_mode.tests.js +++ /dev/null @@ -1,32 +0,0 @@ -const assert = require('chai').assert; -const expressOpenid = require('..'); -const server = require('./fixture/server'); -const request = require('request-promise-native').defaults({ - simple: false, - resolveWithFullResponse: true -}); - -describe('with an invalid response_mode', function() { - - const router = expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - authorizationParams: { - response_mode: '__invalid_response_mode__', - response_type: 'id_token' - } - }); - - let baseUrl; - before(async function() { - baseUrl = await server.create(router); - }); - - it('should return an error', async function() { - const res = await request.get({ json: true, baseUrl, uri: '/login'}); - assert.equal(res.statusCode, 500); - assert.include(res.body.err.message, 'Response mode "__invalid_response_mode__" is not supported by the issuer.'); - }); -}); diff --git a/test/invalid_response_type.tests.js b/test/invalid_response_type.tests.js deleted file mode 100644 index 9c573392..00000000 --- a/test/invalid_response_type.tests.js +++ /dev/null @@ -1,31 +0,0 @@ -const assert = require('chai').assert; -const expressOpenid = require('..'); -const server = require('./fixture/server'); -const request = require('request-promise-native').defaults({ - simple: false, - resolveWithFullResponse: true -}); - -describe('with an invalid response type', function() { - - const router = expressOpenid.auth({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - authorizationParams: { - response_type: '__invalid_response_type__' - } - }); - - let baseUrl; - before(async function() { - baseUrl = await server.create(router); - }); - - it('should return an error', async function() { - const res = await request.get({ json: true, baseUrl, uri: '/login'}); - assert.equal(res.statusCode, 500); - assert.include(res.body.err.message, 'Response type "__invalid_response_type__" is not supported by the issuer.'); - }); -}); diff --git a/test/login.tests.js b/test/login.tests.js new file mode 100644 index 00000000..c7052f14 --- /dev/null +++ b/test/login.tests.js @@ -0,0 +1,359 @@ +const assert = require('chai').assert; +const url = require('url'); +const request = require('request-promise-native').defaults({ + simple: false, + resolveWithFullResponse: true, +}); + +const { decodeState } = require('../lib/hooks/getLoginState'); + +const { auth } = require('..'); +const { create: createServer } = require('./fixture/server'); + +const filterRoute = (method, path) => { + return (r) => + r.route && r.route.path === path && r.route.methods[method.toLowerCase()]; +}; + +const getCookieFromResponse = (res, cookieName) => { + const cookieHeaders = res.headers['set-cookie']; + + const foundHeader = cookieHeaders.filter( + (header) => header.substring(0, 6) === cookieName + '=' + )[0]; + if (!foundHeader) { + return false; + } + + const cookieValuePart = foundHeader.split('; ')[0]; + if (!cookieValuePart) { + return false; + } + + return cookieValuePart.split('=')[1].split('.')[0]; +}; + +const defaultConfig = { + secret: '__test_session_secret__', + clientID: '__test_client_id__', + baseURL: 'https://example.org', + issuerBaseURL: 'https://op.example.com', + authRequired: false, +}; + +describe('auth', () => { + let server; + const baseUrl = 'http://localhost:3000'; + + afterEach(async () => { + if (server) { + server.close(); + } + }); + + it('should contain the default authentication routes', async () => { + const router = auth(defaultConfig); + server = await createServer(router); + assert.ok(router.stack.some(filterRoute('GET', '/login'))); + assert.ok(router.stack.some(filterRoute('GET', '/logout'))); + assert.ok(router.stack.some(filterRoute('POST', '/callback'))); + assert.ok(router.stack.some(filterRoute('GET', '/callback'))); + }); + + it('should contain custom authentication routes', async () => { + const router = auth({ + ...defaultConfig, + routes: { + callback: 'custom-callback', + login: 'custom-login', + logout: 'custom-logout', + }, + }); + server = await createServer(router); + assert.ok(router.stack.some(filterRoute('GET', '/custom-login'))); + assert.ok(router.stack.some(filterRoute('GET', '/custom-logout'))); + assert.ok(router.stack.some(filterRoute('POST', '/custom-callback'))); + assert.ok(router.stack.some(filterRoute('GET', '/custom-callback'))); + }); + + it('should redirect to the authorize url for /login', async () => { + server = await createServer(auth(defaultConfig)); + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + assert.equal(parsed.hostname, 'op.example.com'); + assert.equal(parsed.pathname, '/authorize'); + assert.equal(parsed.query.client_id, '__test_client_id__'); + assert.equal(parsed.query.scope, 'openid profile email'); + assert.equal(parsed.query.response_type, 'id_token'); + assert.equal(parsed.query.response_mode, 'form_post'); + assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); + assert.property(parsed.query, 'nonce'); + assert.property(parsed.query, 'state'); + + assert.equal(getCookieFromResponse(res, 'nonce'), parsed.query.nonce); + assert.equal(getCookieFromResponse(res, 'state'), parsed.query.state); + }); + + it('should redirect to the authorize url for any route if authRequired', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: true, + }) + ); + const res = await request.get('/session', { + baseUrl, + followRedirect: false, + }); + assert.equal(res.statusCode, 302); + }); + + it('should redirect to the authorize url for any route if attemptSilentLogin', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + attemptSilentLogin: true, + }) + ); + const res = await request.get('/session', { + baseUrl, + followRedirect: false, + }); + assert.equal(res.statusCode, 302); + }); + + it('should redirect to the authorize url for /login in code flow', async () => { + server = await createServer( + auth({ + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code', + }, + }) + ); + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + + assert.equal(parsed.hostname, 'op.example.com'); + assert.equal(parsed.pathname, '/authorize'); + assert.equal(parsed.query.client_id, '__test_client_id__'); + assert.equal(parsed.query.scope, 'openid profile email'); + assert.equal(parsed.query.response_type, 'code'); + assert.equal(parsed.query.response_mode, undefined); + assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); + assert.property(parsed.query, 'nonce'); + assert.property(parsed.query, 'state'); + assert.property(res.headers, 'set-cookie'); + + assert.equal(getCookieFromResponse(res, 'nonce'), parsed.query.nonce); + assert.equal(getCookieFromResponse(res, 'state'), parsed.query.state); + }); + + it('should redirect to the authorize url for /login in id_token flow', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authorizationParams: { + response_type: 'id_token', + }, + }) + ); + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + + assert.equal(parsed.hostname, 'op.example.com'); + assert.equal(parsed.pathname, '/authorize'); + assert.equal(parsed.query.client_id, '__test_client_id__'); + assert.equal(parsed.query.scope, 'openid profile email'); + assert.equal(parsed.query.response_type, 'id_token'); + assert.equal(parsed.query.response_mode, 'form_post'); + assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); + assert.property(parsed.query, 'nonce'); + assert.property(parsed.query, 'state'); + }); + + it('should redirect to the authorize url for /login in hybrid flow', async () => { + server = await createServer( + auth({ + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + }, + }) + ); + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + + assert.equal(parsed.hostname, 'op.example.com'); + assert.equal(parsed.pathname, '/authorize'); + assert.equal(parsed.query.client_id, '__test_client_id__'); + assert.equal(parsed.query.scope, 'openid profile email'); + assert.equal(parsed.query.response_type, 'code id_token'); + assert.equal(parsed.query.response_mode, 'form_post'); + assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); + assert.property(parsed.query, 'nonce'); + assert.property(parsed.query, 'state'); + }); + + it('should redirect to the authorize url for custom login route', async () => { + server = await createServer( + auth({ + ...defaultConfig, + routes: { + callback: 'custom-callback', + login: 'custom-login', + logout: 'custom-logout', + }, + }) + ); + const res = await request.get('/custom-login', { + baseUrl, + followRedirect: false, + }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + assert.equal(parsed.hostname, 'op.example.com'); + assert.equal(parsed.pathname, '/authorize'); + assert.equal( + parsed.query.redirect_uri, + 'https://example.org/custom-callback' + ); + }); + + it('should allow custom login route with additional login params', async () => { + const router = auth({ + ...defaultConfig, + routes: { login: false }, + }); + router.get('/login', (req, res) => { + res.oidc.login({ + returnTo: 'https://example.org/custom-redirect', + authorizationParams: { + response_type: 'code', + response_mode: 'query', + scope: 'openid email', + }, + }); + }); + server = await createServer(router); + + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + + assert.equal(parsed.hostname, 'op.example.com'); + assert.equal(parsed.pathname, '/authorize'); + assert.equal(parsed.query.scope, 'openid email'); + assert.equal(parsed.query.response_type, 'code'); + assert.equal(parsed.query.response_mode, 'query'); + assert.equal(parsed.query.redirect_uri, 'https://example.org/callback'); + assert.property(parsed.query, 'nonce'); + + const decodedState = decodeState(parsed.query.state); + + assert.equal(decodedState.returnTo, 'https://example.org/custom-redirect'); + }); + + it('should not allow removing openid from scope', async function () { + const router = auth({ ...defaultConfig, routes: { login: false } }); + router.get('/login', (req, res) => { + res.oidc.login({ + authorizationParams: { + scope: 'email', + }, + }); + }); + server = await createServer(router); + + const cookieJar = request.jar(); + const res = await request.get('/login', { + cookieJar, + baseUrl, + json: true, + followRedirect: false, + }); + assert.equal(res.statusCode, 500); + assert.equal(res.body.err.message, 'scope should contain "openid"'); + }); + + it('should not allow an invalid response_type', async function () { + const router = auth({ ...defaultConfig, routes: { login: false } }); + router.get('/login', (req, res) => { + res.oidc.login({ + authorizationParams: { + response_type: 'invalid', + }, + }); + }); + server = await createServer(router); + + const cookieJar = request.jar(); + const res = await request.get('/login', { + cookieJar, + baseUrl, + json: true, + followRedirect: false, + }); + assert.equal(res.statusCode, 500); + assert.equal( + res.body.err.message, + 'response_type should be one of id_token, code id_token, code' + ); + }); + + it('should use a custom state builder', async () => { + server = await createServer( + auth({ + ...defaultConfig, + getLoginState: (req, opts) => { + return { + returnTo: opts.returnTo + '/custom-page', + customProp: '__test_custom_prop__', + }; + }, + }) + ); + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + const decodedState = decodeState(parsed.query.state); + + assert.equal(decodedState.returnTo, 'https://example.org/custom-page'); + assert.equal(decodedState.customProp, '__test_custom_prop__'); + }); + + it('should use PKCE when response_type includes code', async () => { + server = await createServer( + auth({ + ...defaultConfig, + clientSecret: '__test_client_secret__', + authorizationParams: { + response_type: 'code id_token', + }, + }) + ); + const res = await request.get('/login', { baseUrl, followRedirect: false }); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + + assert.isDefined(parsed.query.code_challenge); + assert.equal(parsed.query.code_challenge_method, 'S256'); + + assert.isDefined(getCookieFromResponse(res, 'code_verifier')); + }); +}); diff --git a/test/logout.tests.js b/test/logout.tests.js index 4dc330dd..10083273 100644 --- a/test/logout.tests.js +++ b/test/logout.tests.js @@ -1,197 +1,212 @@ const { assert } = require('chai'); -const server = require('./fixture/server'); +const { create: createServer } = require('./fixture/server'); +const { makeIdToken } = require('./fixture/cert'); const { auth } = require('./..'); const request = require('request-promise-native').defaults({ simple: false, - resolveWithFullResponse: true + resolveWithFullResponse: true, }); -describe('logout route', function() { - describe('application only logout', function() { - let baseUrl; - let currentSession; - let logoutResponse; - const jar = request.jar(); - - before(async function() { - const middleware = auth({ - idpLogout: false, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - appSession: {secret: '__test_session_secret__'}, - required: false - }); - baseUrl = await server.create(middleware); - await request.post({ - uri: '/session', - json: { - openidTokens: { - id_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' - } - }, - baseUrl, jar - }); - logoutResponse = await request.get({uri: '/logout', baseUrl, jar, followRedirect: false}); - currentSession = (await request.get({uri: '/session', baseUrl, jar})).body; - }); - - it('should clear the session', function() { - assert.notOk(currentSession.openidTokens); - }); +const defaultConfig = { + clientID: '__test_client_id__', + baseURL: 'https://example.org', + issuerBaseURL: 'https://op.example.com', + secret: '__test_session_secret__', + authRequired: false, +}; + +const login = async (baseUrl = 'http://localhost:3000') => { + const jar = request.jar(); + await request.post({ + uri: '/session', + json: { + id_token: makeIdToken(), + }, + baseUrl, + jar, + }); - it('should redirect to the base url', function() { - assert.equal(logoutResponse.statusCode, 302); - assert.equal(logoutResponse.headers.location, 'https://example.org'); - }); + const session = ( + await request.get({ uri: '/session', baseUrl, jar, json: true }) + ).body; + return { jar, session }; +}; + +const logout = async (jar, baseUrl = 'http://localhost:3000') => { + const response = await request.get({ + uri: '/logout', + baseUrl, + jar, + followRedirect: false, + }); + const session = ( + await request.get({ uri: '/session', baseUrl, jar, json: true }) + ).body; + return { response, session }; +}; + +describe('logout route', async () => { + let server; + + afterEach(async () => { + if (server) { + server.close(); + } }); - describe('identity provider logout (auth0)', function() { - let baseUrl; - let currentSession; - let logoutResponse; - const jar = request.jar(); + it('should perform a local logout', async () => { + server = await createServer( + auth({ + ...defaultConfig, + idpLogout: false, + }) + ); + + const { jar, session: loggedInSession } = await login(); + assert.ok(loggedInSession.id_token); + const { response, session: loggedOutSession } = await logout(jar); + assert.notOk(loggedOutSession.id_token); + assert.equal(response.statusCode, 302); + assert.include( + response.headers, + { + location: 'https://example.org', + }, + 'should redirect to the base url' + ); + }); - before(async function() { - const middleware = auth({ + it('should perform a distributed logout', async () => { + server = await createServer( + auth({ + ...defaultConfig, idpLogout: true, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - appSession: {secret: '__test_session_secret__'}, - required: false - }); - baseUrl = await server.create(middleware); - await request.post({ - uri: '/session', - json: { - openidTokens: { - id_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' - } - }, - baseUrl, jar - }); - logoutResponse = await request.get({uri: '/logout', baseUrl, jar, followRedirect: false}); - currentSession = (await request.get({uri: '/session', baseUrl, jar})).body; - }); - - it('should clear the session', function() { - assert.notOk(currentSession.openidTokens); - }); - - it('should redirect to the base url', function() { - assert.equal(logoutResponse.statusCode, 302); - assert.equal(logoutResponse.headers.location, 'https://example.org'); - }); + }) + ); + + const { jar } = await login(); + const { response, session: loggedOutSession } = await logout(jar); + assert.notOk(loggedOutSession.id_token); + assert.equal(response.statusCode, 302); + assert.include( + response.headers, + { + location: `https://op.example.com/session/end?post_logout_redirect_uri=https%3A%2F%2Fexample.org&id_token_hint=${makeIdToken()}`, + }, + 'should redirect to the identity provider' + ); }); + it('should perform an auth0 logout', async () => { + server = await createServer( + auth({ + ...defaultConfig, + issuerBaseURL: 'https://test.eu.auth0.com', + idpLogout: true, + auth0Logout: true, + }) + ); + + const { jar } = await login(); + const { response, session: loggedOutSession } = await logout(jar); + assert.notOk(loggedOutSession.id_token); + assert.equal(response.statusCode, 302); + assert.include( + response.headers, + { + location: + 'https://op.example.com/v2/logout?returnTo=https%3A%2F%2Fexample.org&client_id=__test_client_id__', + }, + 'should redirect to the identity provider' + ); + }); - describe('should use postLogoutRedirectUri if present', function() { - describe('should allow relative paths, and prepend with baseURL', () => { - let baseUrl; - const jar = request.jar(); - - before(async function() { - const middleware = auth({ - idpLogout: false, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - appSession: {secret: '__test_session_secret__'}, - postLogoutRedirectUri: '/after-logout-in-auth-config', - required: false, - }); - baseUrl = await server.create(middleware); - await request.post({ - uri: '/session', - json: { - openidTokens: { - id_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' - } - }, - baseUrl, jar - }); - }); - - it('should redirect to postLogoutRedirectUri in auth() config', async function() { - const logoutResponse = await request.get({uri: '/logout', baseUrl, jar, followRedirect: false}); - assert.equal(logoutResponse.headers.location, 'https://example.org/after-logout-in-auth-config'); - }); - }); + it('should redirect to postLogoutRedirect', async () => { + server = await createServer( + auth({ + ...defaultConfig, + routes: { + postLogoutRedirect: '/after-logout-in-auth-config', + }, + }) + ); + + const { jar } = await login(); + const { response, session: loggedOutSession } = await logout(jar); + assert.notOk(loggedOutSession.id_token); + assert.equal(response.statusCode, 302); + assert.include( + response.headers, + { + location: 'https://example.org/after-logout-in-auth-config', + }, + 'should redirect to postLogoutRedirect' + ); + }); - describe('should allow absolute paths', () => { - let baseUrl; - const jar = request.jar(); - - before(async function() { - const middleware = auth({ - idpLogout: false, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - appSession: {secret: '__test_session_secret__'}, - postLogoutRedirectUri: 'https://external-domain.com/after-logout-in-auth-config', - required: false, - }); - baseUrl = await server.create(middleware); - await request.post({ - uri: '/session', - json: { - openidTokens: { - id_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' - } - }, - baseUrl, jar - }); - }); - - it('should redirect to postLogoutRedirectUri in auth() config', async function() { - const logoutResponse = await request.get({uri: '/logout', baseUrl, jar, followRedirect: false}); - assert.equal(logoutResponse.headers.location, 'https://external-domain.com/after-logout-in-auth-config'); - }); + it('should redirect to the specified returnTo', async () => { + const router = auth({ + ...defaultConfig, + routes: { + logout: false, + postLogoutRedirect: '/after-logout-in-auth-config', + }, }); + server = await createServer(router); + router.get('/logout', (req, res) => + res.oidc.logout({ returnTo: 'http://www.another-example.org/logout' }) + ); + + const { jar } = await login(); + const { response, session: loggedOutSession } = await logout(jar); + assert.notOk(loggedOutSession.id_token); + assert.equal(response.statusCode, 302); + assert.include( + response.headers, + { + location: 'http://www.another-example.org/logout', + }, + 'should redirect to params.returnTo' + ); }); - describe('logout with custom path', function() { - let baseUrl; - let currentSession; - const jar = request.jar(); - - before(async function() { - const middleware = auth({ - idpLogout: false, - clientID: '__test_client_id__', - baseURL: 'https://example.org/foo', - issuerBaseURL: 'https://test.auth0.com', - appSession: { - secret: '__test_secret__', - cookiePath: '/foo' - }, - required: false - }); - baseUrl = (await server.create(middleware, null, '/foo')) + '/foo'; - await request.post({ - uri: '/session', - json: { - openidTokens: { - id_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + it('should logout when scoped to a sub path', async () => { + server = await createServer( + auth({ + ...defaultConfig, + session: { + cookie: { + path: '/foo', }, - claims: {} }, - baseUrl, jar - }); - }); - - it('should populate the session', async function() { - currentSession = JSON.parse((await request.get({uri: '/session', baseUrl, jar})).body); - assert.ok(currentSession.openidTokens); - }); + }), + null, + '/foo' + ); + const baseUrl = 'http://localhost:3000/foo'; + + const { jar, session: loggedInSession } = await login(baseUrl); + assert.ok(loggedInSession.id_token); + const sessionCookie = jar + .getCookies('http://localhost:3000/foo') + .find(({ key }) => key === 'appSession'); + assert.equal(sessionCookie.path, '/foo'); + const { session: loggedOutSession } = await logout(jar, baseUrl); + assert.notOk(loggedOutSession.id_token); + }); - it('should clear the session', async function() { - await request.get({uri: '/logout', baseUrl, jar, followRedirect: false}); - currentSession = JSON.parse((await request.get({uri: '/session', baseUrl, jar})).body); - assert.notOk(currentSession.openidTokens); - }); + it('should cancel silent logins when user logs out', async () => { + server = await createServer(auth(defaultConfig)); + + const { jar } = await login(); + const baseUrl = 'http://localhost:3000'; + assert.notOk( + jar.getCookies(baseUrl).find(({ key }) => key === 'skipSilentLogin') + ); + await logout(jar); + assert.ok( + jar.getCookies(baseUrl).find(({ key }) => key === 'skipSilentLogin') + ); }); }); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 77042baa..00000000 --- a/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ - --timeout 10000 - --file ./test/setup.js - --exit diff --git a/test/requiresAuth.tests.js b/test/requiresAuth.tests.js index 0fbe2518..a386562c 100644 --- a/test/requiresAuth.tests.js +++ b/test/requiresAuth.tests.js @@ -1,78 +1,357 @@ const { assert } = require('chai'); -const server = require('./fixture/server'); -const { auth, requiresAuth } = require('./..'); +const sinon = require('sinon'); +const { create: createServer } = require('./fixture/server'); +const { makeIdToken } = require('./fixture/cert'); +const { + auth, + requiresAuth, + claimEquals, + claimIncludes, + claimCheck, +} = require('./..'); const request = require('request-promise-native').defaults({ simple: false, resolveWithFullResponse: true, - followRedirect: false + followRedirect: false, }); -describe('requiresAuth middleware', function() { - describe('when trying to access a protected route without being logged in', function() { - let baseUrl; - let response; - - before(async function() { - const router = auth({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - required: false - }); - baseUrl = await server.create(router, requiresAuth()); - response = await request({ baseUrl, url: '/protected' }); - }); - - it('should return a 302', function() { - assert.equal(response.statusCode, 302); - }); - it('should contain a location header to the issuer', function() { - assert.include(response.headers.location, 'https://test.auth0.com'); - }); - it('should contain a location header with state containing return url', function() { - const state = (new URL(response.headers.location)).searchParams.get('state'); - const decoded = Buffer.from(state, 'base64'); - const parsed = JSON.parse(decoded); - assert.equal(parsed.returnTo, '/protected'); - }); - }); - - describe('when removing the auth middleware', function() { - let baseUrl; - let response; - - before(async function() { - const router = (req, res, next) => next(); - baseUrl = await server.create(router, requiresAuth()); - response = await request({ baseUrl, url: '/protected' }); - }); - - it('should return a 401', function() { - assert.equal(response.statusCode, 401); - }); - }); - - describe('when requiring auth in a route', function() { - let baseUrl; - let response; - - before(async function() { - const router = auth({ - appSession: {secret: '__test_session_secret__'}, - clientID: '__test_client_id__', - baseURL: 'https://example.org', - issuerBaseURL: 'https://test.auth0.com', - required: false, - errorOnRequiredAuth: true - }); - baseUrl = await server.create(router, requiresAuth()); - response = await request({ baseUrl, url: '/protected' }); - }); - - it('should return a 401', function() { - assert.equal(response.statusCode, 401); - }); +const baseUrl = 'http://localhost:3000'; + +const defaultConfig = { + secret: '__test_session_secret__', + clientID: '__test_client_id__', + baseURL: 'https://example.org', + issuerBaseURL: 'https://op.example.com', +}; + +const login = async (claims) => { + const jar = request.jar(); + await request.post('/session', { + baseUrl, + jar, + json: { + id_token: makeIdToken(claims), + }, + }); + return jar; +}; + +describe('requiresAuth', () => { + let server; + + afterEach(async () => { + if (server) { + server.close(); + } + }); + + it('should allow logged in users to visit a protected route', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + requiresAuth() + ); + const jar = await login(); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 200); + }); + + it('should ask anonymous user to login when visiting a protected route', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + requiresAuth() + ); + const response = await request({ baseUrl, url: '/protected' }); + const state = new URL(response.headers.location).searchParams.get('state'); + const decoded = Buffer.from(state, 'base64'); + const parsed = JSON.parse(decoded); + + assert.equal(response.statusCode, 302); + assert.include(response.headers.location, 'https://op.example.com'); + assert.equal(parsed.returnTo, '/protected'); + }); + + it("should 401 for anonymous users who don't accept html", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + }), + requiresAuth() + ); + const response = await request({ baseUrl, url: '/protected', json: true }); + assert.equal(response.statusCode, 401); + }); + + it('should return 401 when anonymous user visits a protected route', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + requiresAuth() + ); + const response = await request({ baseUrl, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it("should throw when there's no auth middleware", async () => { + server = await createServer(null, requiresAuth()); + const { + body: { err }, + } = await request({ baseUrl, url: '/protected', json: true }); + assert.equal( + err.message, + 'req.oidc is not found, did you include the auth middleware?' + ); + }); + + it('should allow logged in users with the right claim', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimEquals('foo', 'bar') + ); + const jar = await login({ foo: 'bar' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 200); + }); + + it("should return 401 when logged in user doesn't have the right value for claim", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimEquals('foo', 'bar') + ); + const jar = await login({ foo: 'baz' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it("should return 401 when logged in user doesn't have the claim", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimEquals('baz', 'bar') + ); + const jar = await login({ foo: 'bar' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it("should return 401 when anonymous user doesn't have the right claim", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimEquals('foo', 'bar') + ); + const response = await request({ baseUrl, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it('should throw when claim is not a string', () => { + assert.throws( + () => claimEquals(true, 'bar'), + TypeError, + '"claim" must be a string' + ); + }); + + it('should throw when claim value is a non primitive', () => { + assert.throws( + () => claimEquals('foo', { bar: 1 }), + TypeError, + '"expected" must be a string, number, boolean or null' + ); }); + it('should allow logged in users with all of the requested claims', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimIncludes('foo', 'bar', 'baz') + ); + const jar = await login({ foo: ['baz', 'bar'] }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 200); + }); + + it('should return 401 for logged with some of the requested claims', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimIncludes('foo', 'bar', 'baz', 'qux') + ); + const jar = await login({ foo: 'baz bar' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it('should accept claim values as a space separated list', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimIncludes('foo', 'bar', 'baz') + ); + const jar = await login({ foo: 'baz bar' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 200); + }); + + it("should not accept claim values that aren't a string or array", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimIncludes('foo', 'bar', 'baz') + ); + const jar = await login({ foo: { bar: 'baz' } }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it('should throw when claim value for checking many claims is a non primitive', () => { + assert.throws( + () => claimIncludes(false, 'bar'), + TypeError, + '"claim" must be a string' + ); + }); + + it("should return 401 when checking multiple claims and the user doesn't have the claim", async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimIncludes('foo', 'bar', 'baz') + ); + const jar = await login({ bar: 'bar baz' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it('should return 401 when checking many claims with anonymous user', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimIncludes('foo', 'bar', 'baz') + ); + const response = await request({ baseUrl, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it("should throw when custom claim check doesn't get a function", async () => { + assert.throws( + () => claimCheck(null), + TypeError, + '"claimCheck" expects a function' + ); + }); + + it('should allow user when custom claim check returns truthy', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimCheck(() => true) + ); + const jar = await login(); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 200); + }); + + it('should not allow user when custom claim check returns falsey', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimCheck(() => false) + ); + const jar = await login(); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 401); + }); + + it('should make the token claims available to custom check', async () => { + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimCheck((req, claims) => claims.foo === 'some_claim') + ); + const jar = await login({ foo: 'some_claim' }); + const response = await request({ baseUrl, jar, url: '/protected' }); + + assert.equal(response.statusCode, 200); + }); + + it('should not allow anonymous users to check custom claims', async () => { + const checkSpy = sinon.spy(); + server = await createServer( + auth({ + ...defaultConfig, + authRequired: false, + errorOnRequiredAuth: true, + }), + claimCheck(checkSpy) + ); + const response = await request({ baseUrl, url: '/protected' }); + + assert.equal(response.statusCode, 401); + sinon.assert.notCalled(checkSpy); + }); }); diff --git a/test/setup.js b/test/setup.js index e73f3230..a06e18b9 100644 --- a/test/setup.js +++ b/test/setup.js @@ -2,18 +2,28 @@ const nock = require('nock'); const wellKnown = require('./fixture/well-known.json'); const certs = require('./fixture/cert'); -before(function() { - nock('https://test.auth0.com', { allowUnmocked: true }) +beforeEach(function () { + nock('https://op.example.com', { allowUnmocked: true }) .persist() .get('/.well-known/openid-configuration') .reply(200, wellKnown); - nock('https://test.auth0.com', { allowUnmocked: true }) + nock('https://op.example.com', { allowUnmocked: true }) + .persist() + .get('/.well-known/jwks.json') + .reply(200, certs.jwks); + + nock('https://test.eu.auth0.com', { allowUnmocked: true }) + .persist() + .get('/.well-known/openid-configuration') + .reply(200, { ...wellKnown, end_session_endpoint: undefined }); + + nock('https://test.eu.auth0.com', { allowUnmocked: true }) .persist() .get('/.well-known/jwks.json') .reply(200, certs.jwks); }); -after(function() { +afterEach(function () { nock.cleanAll(); }); diff --git a/test/transientHandler.tests.js b/test/transientHandler.tests.js index b5d1ff82..a3bebb58 100644 --- a/test/transientHandler.tests.js +++ b/test/transientHandler.tests.js @@ -1,138 +1,174 @@ const { assert } = require('chai'); -const transientHandler = require('../lib/transientHandler'); +const sinon = require('sinon'); +const { JWS } = require('jose'); -class ResultMock { - constructor() { - this.resetArgs(); - } +const COOKIES = require('../lib/cookies'); +const TransientCookieHandler = require('../lib/transientHandler'); - cookie() { - this.args.push(arguments); - } +const reqWithCookies = (cookies) => ({ [COOKIES]: cookies }); +const secret = '__test_session_secret__'; - resetArgs() { - this.args = []; - } -} +describe('transientHandler', function () { + let res; + let transientHandler; + let generateSignature; -describe('transientHandler', function() { - - let res = new ResultMock(); - - beforeEach(async function() { - res.resetArgs(); + beforeEach(async function () { + transientHandler = new TransientCookieHandler({ + secret, + legacySameSiteCookie: true, + }); + generateSignature = (cookie, value) => + JWS.sign.flattened( + Buffer.from(`${cookie}=${value}`), + transientHandler.keyStore, + { alg: 'HS256', b64: false, crit: ['b64'] } + ).signature; + res = { cookie: sinon.spy(), clearCookie: sinon.spy() }; }); - describe('store()', function() { - it('should use the passed-in key to set the cookie', function() { - transientHandler.store('test_key', res); - - assert.equal('test_key', res.args[0][0]); // Main cookie - assert.equal('_test_key', res.args[1][0]); // Fallback cookie + describe('store()', function () { + it('should use the passed-in key to set the cookie', function () { + transientHandler.store('test_key', {}, res); + sinon.assert.calledWith(res.cookie, 'test_key'); + sinon.assert.calledWith(res.cookie, '_test_key'); }); - it('should return the same nonce as the cookie value', function() { - const value = transientHandler.store('test_key', res, {}); - assert.equal(value, res.args[0][1]); // Main cookie - assert.equal(value, res.args[1][1]); // Fallback cookie + it('should return the same nonce as the cookie value', function () { + const value = transientHandler.store('test_key', {}, res, {}); + const re = new RegExp(`^${value}\\.`); + sinon.assert.calledWithMatch(res.cookie, 'test_key', re); + sinon.assert.calledWithMatch(res.cookie, '_test_key', re); }); - it('should set SameSite=None, secure, and fallback cookie by default', function() { - transientHandler.store('test_key', res); - - assert.equal('None', res.args[0][2].sameSite); // Main cookie - assert.equal(true, res.args[0][2].secure); // Main cookie - assert.equal(true, res.args[0][2].httpOnly); // Main cookie - assert.equal(600000, res.args[0][2].maxAge); // Main cookie + it('should use the req.secure property to automatically set cookies secure when on https', function () { + transientHandler.store('test_key', { secure: true }, res, { + sameSite: 'Lax', + }); + transientHandler.store('test_key', { secure: false }, res, { + sameSite: 'Lax', + }); + + sinon.assert.calledWithMatch(res.cookie.firstCall, 'test_key', '', { + sameSite: 'Lax', + secure: true, + }); + sinon.assert.calledWithMatch(res.cookie.secondCall, 'test_key', '', { + sameSite: 'Lax', + secure: false, + }); + }); - assert.equal('_test_key', res.args[1][0]); // Fallback cookie - assert.equal(true, res.args[1][2].httpOnly); // Fallback cookie - assert.equal(600000, res.args[1][2].maxAge); // Fallback cookie - assert.equal(undefined, res.args[1][2].sameSite); // Fallback cookie - assert.equal(undefined, res.args[1][2].secure); // Fallback cookie + it('should set SameSite=None, secure, and fallback cookie by default', function () { + transientHandler.store('test_key', {}, res); + + sinon.assert.calledWithMatch(res.cookie, 'test_key', '', { + sameSite: 'None', + secure: true, + httpOnly: true, + }); + sinon.assert.calledWithMatch(res.cookie, '_test_key', '', { + sameSite: undefined, + secure: undefined, + httpOnly: true, + }); }); - it('should turn off fallback', function() { - transientHandler.store('test_key', res, {legacySameSiteCookie: false}); + it('should turn off fallback', function () { + transientHandler = new TransientCookieHandler({ + secret, + legacySameSiteCookie: false, + }); + transientHandler.store('test_key', {}, res); - assert.equal('test_key', res.args[0][0]); // Main cookie - assert.equal(undefined, res.args[1]); // Fallback cookie + sinon.assert.calledWith(res.cookie, 'test_key'); + sinon.assert.calledOnce(res.cookie); }); - it('should set custom SameSite with no fallback', function() { - transientHandler.store('test_key', res, {sameSite: 'Lax'}); + it('should set custom SameSite with no fallback', function () { + transientHandler.store('test_key', {}, res, { sameSite: 'Lax' }); - assert.equal('Lax', res.args[0][2].sameSite); // Main cookie - assert.equal(false, res.args[0][2].secure); // Main cookie - - assert.equal(undefined, res.args[1]); // Fallback cookie + sinon.assert.calledWithMatch(res.cookie, 'test_key', '', { + sameSite: 'Lax', + }); + sinon.assert.calledOnce(res.cookie); }); - it('should use the passed-in value', function() { - const value = transientHandler.store('test_key', res, {value: '__test_value__'}); + it('should use the passed-in value', function () { + const value = transientHandler.store('test_key', {}, res, { + value: '__test_value__', + }); assert.equal('__test_value__', value); - assert.equal(value, res.args[0][1]); // Main cookie - assert.equal(value, res.args[1][1]); // Fallback cookie - }); - - it('should set a custom maxAge', function() { - transientHandler.store('test_key', res, {maxAge: 123456789}); - - assert.equal(123456789, res.args[0][2].maxAge); // Main cookie - assert.equal(123456789, res.args[1][2].maxAge); // Fallback cookie + const re = /^__test_value__\./; + sinon.assert.calledWithMatch(res.cookie, 'test_key', re); + sinon.assert.calledWithMatch(res.cookie, '_test_key', re); }); }); - describe('getOnce()', function() { - it('should return undefined if there are no cookies', function() { - assert.equal(transientHandler.getOnce('test_key', {cookies: undefined}, res), undefined); + describe('getOnce()', function () { + it('should return undefined if there are no cookies', function () { + assert.isUndefined( + transientHandler.getOnce('test_key', reqWithCookies(), res) + ); }); - it('should return main value and delete both cookies by default', function() { - const req = {cookies: {test_key: '__test_value__', _test_key: '__test_fallback_value__'}}; + it('should return main value and delete both cookies by default', function () { + const signature = generateSignature('test_key', 'foo'); + const cookies = { + test_key: `foo.${signature}`, + _test_key: `foo.${signature}`, + }; + const req = reqWithCookies(cookies); const value = transientHandler.getOnce('test_key', req, res); - assert.equal(value, '__test_value__'); + assert.equal(value, 'foo'); - assert.equal('test_key', res.args[0][0]); // Main cookie - assert.equal('', res.args[0][1]); // Main cookie - assert.equal(0, res.args[0][2].maxAge); // Main cookie - assert.equal(undefined, req.cookies.test_key); // Main cookie - - assert.equal('_test_key', res.args[1][0]); // Fallback cookie - assert.equal('', res.args[1][1]); // Fallback cookie - assert.equal(0, res.args[1][2].maxAge); // Fallback cookie - assert.equal(undefined, req.cookies._test_key); // Fallback cookie + sinon.assert.calledWith(res.clearCookie, 'test_key'); + sinon.assert.calledWith(res.clearCookie, '_test_key'); }); - it('should return fallback value and delete both cookies if main value not present', function() { - const req = {cookies: {_test_key: '__test_fallback_value__'}}; + it('should return fallback value and delete both cookies if main value not present', function () { + const cookies = { + _test_key: `foo.${generateSignature('_test_key', 'foo')}`, + }; + const req = reqWithCookies(cookies); const value = transientHandler.getOnce('test_key', req, res); - assert.equal(value, '__test_fallback_value__'); - - assert.equal('test_key', res.args[0][0]); // Main cookie - assert.equal('', res.args[0][1]); // Main cookie - assert.equal(0, res.args[0][2].maxAge); // Main cookie + assert.equal(value, 'foo'); - assert.equal('_test_key', res.args[1][0]); // Fallback cookie - assert.equal('', res.args[1][1]); // Fallback cookie - assert.equal(0, res.args[1][2].maxAge); // Fallback cookie - assert.equal(undefined, req.cookies._test_key); // Fallback cookie + sinon.assert.calledWith(res.clearCookie, 'test_key'); + sinon.assert.calledWith(res.clearCookie, '_test_key'); }); - it('should not delete fallback cookie if legacy support is off', function() { - const req = {cookies: {test_key: '__test_value__', _test_key: '__test_fallback_value__'}}; - const value = transientHandler.getOnce('test_key', req, res, {legacySameSiteCookie: false}); + it('should not delete fallback cookie if legacy support is off', function () { + const signature = generateSignature('test_key', 'foo'); + const cookies = { + test_key: `foo.${signature}`, + _test_key: `foo.${signature}`, + }; + const req = reqWithCookies(cookies); + transientHandler = new TransientCookieHandler({ + secret, + legacySameSiteCookie: false, + }); + const value = transientHandler.getOnce('test_key', req, res); + + assert.equal(value, 'foo'); - assert.equal(value, '__test_value__'); + sinon.assert.calledWith(res.clearCookie, 'test_key'); + sinon.assert.calledOnce(res.clearCookie); + }); - assert.equal('test_key', res.args[0][0]); // Main cookie - assert.equal('', res.args[0][1]); // Main cookie - assert.equal(0, res.args[0][2].maxAge); // Main cookie + it("should not throw when it can't verify the signature", function () { + const cookies = { + test_key: 'foo.bar', + _test_key: 'foo.bar', + }; + const req = reqWithCookies(cookies); + const value = transientHandler.getOnce('test_key', req, res); - assert.equal(undefined, res.args[1]); // Fallback cookie - assert.equal('__test_fallback_value__', req.cookies._test_key); // Fallback cookie + assert.isUndefined(value); + sinon.assert.calledTwice(res.clearCookie); }); }); }); diff --git a/typedoc.js b/typedoc.js new file mode 100644 index 00000000..d70ca599 --- /dev/null +++ b/typedoc.js @@ -0,0 +1,9 @@ +module.exports = { + out: './docs/', + mode: 'file', + excludeExternals: true, + includeDeclarations: true, + hideGenerator: true, + theme: 'minimal', + readme: false, +};