From 6d1001d50d055b450ed5a1a3272b6b324c0dd0ab Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Tue, 25 Aug 2020 12:45:38 +0100 Subject: [PATCH] [SDK-1876] [SDK-1726] Add samples and smoke tests (#127) * Add samples and smoke tests * Fix CI --- .circleci/config.yml | 5 +- EXAMPLES.md | 278 +++------- end-to-end/access-an-api.js | 87 ++++ end-to-end/basic.test.js | 62 +++ end-to-end/fixture/helpers.js | 103 ++++ end-to-end/fixture/oidc-provider.js | 49 ++ end-to-end/userinfo.test.js | 48 ++ examples/.env.sample | 9 + examples/README.md | 23 + examples/access-an-api.js | 34 ++ examples/api.js | 17 + examples/basic.js | 16 + examples/custom-routes.js | 30 ++ examples/routes.js | 22 + examples/run_example.js | 39 ++ examples/userinfo.js | 20 + lib/context.js | 18 +- package-lock.json | 762 ++++++++++++++++++++++++++++ package.json | 8 +- test/callback.tests.js | 12 +- 20 files changed, 1430 insertions(+), 212 deletions(-) create mode 100644 end-to-end/access-an-api.js create mode 100644 end-to-end/basic.test.js create mode 100644 end-to-end/fixture/helpers.js create mode 100644 end-to-end/fixture/oidc-provider.js create mode 100644 end-to-end/userinfo.test.js create mode 100644 examples/.env.sample create mode 100644 examples/README.md create mode 100644 examples/access-an-api.js create mode 100644 examples/api.js create mode 100644 examples/basic.js create mode 100644 examples/custom-routes.js create mode 100644 examples/routes.js create mode 100644 examples/run_example.js create mode 100644 examples/userinfo.js diff --git a/.circleci/config.yml b/.circleci/config.yml index f1b81381..dddc4f96 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:10-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/EXAMPLES.md b/EXAMPLES.md index 5f9784b8..493741d2 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -2,37 +2,38 @@ ## 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(auth()); -app.use('/', (req, res) => { - res.send(`hello ${req.openid.user.name}`); +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: @@ -42,250 +43,121 @@ const { auth, requiresAuth } = require('express-openid-connect'); app.use( auth({ - required: false, + 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: - -```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: +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({ - redirectUriPath: '/custom-callback-path', - loginPath: '/custom-login-path', - logoutPath: '/custom-logout-path', + routes: { + // Override the default login route + login: false, + // Pass a custom path to redirect users to a different + // path after login. + postLogoutRedirect: '/custom-logout', + }, }) ); -``` -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. +app.get('/login', (req, res) => res.oidc.login({ returnTo: '/profile' })); -## 4. Custom user session handling +app.get('/custom-logout', (req, res) => res.send('Bye!')); -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. - -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: - -```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; - }, - }) -); +module.exports = app; ``` -## 5. Obtaining and storing access tokens to call external APIs +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. + +Full example at [custom-routes.js](./examples/custom-routes.js), to run it: `npm run start:example -- custom-routes` -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. +## 3. Obtaining access tokens to call external APIs -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: +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({ 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(); + audience: 'https://api.example.com/products', + scope: 'openid profile email read:products', }, }) ); -``` - -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 ... +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}`); }); ``` -## 6. Obtaining and using refresh tokens +Full example at [access-an-api.js](./examples/access-an-api.js), to run it: `npm run start:example -- access-an-api` + +## 4. 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. +[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 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(); + response_type: 'code', + audience: 'https://api.example.com/products', + scope: 'openid profile email offline_access read:products', }, }) ); -``` -On a route that calls an API, check for an expired token and attempt a refresh: - -```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.get('/', async (req, res) => { + let { token_type, access_token, isExpired, refresh } = req.oidc.accessToken; + if (isExpired()) { + ({ access_token } = await refresh()); } - - // Check for and use tokenSet.access_token for the API call ... -}); -``` - -## 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); - } + const products = await request.get('https://api.example.com/products', { + headers: { + Authorization: `${token_type} ${access_token}`, }, - authorizationParams: { - response_type: 'code', - scope: 'openid profile email', - }, - }) -); + }); + res.send(`Products: ${products}`); +}); ``` -## 8. Custom state handling +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 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. +## 5. Calling userinfo -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: +If your application needs to call the `/userinfo` endpoint you can use the `fetchUserInfo` method on the request context: ```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(); - }, - }) -); +app.use(auth()); + +app.get('/', async (req, res) => { + const userInfo = await req.oidc.fetchUserInfo(); + // ... +}); ``` + +Full example at [userinfo.js](./examples/userinfo.js), to run it: `npm run start:example -- userinfo` 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/lib/context.js b/lib/context.js index 0162b3e7..9f19a0a6 100644 --- a/lib/context.js +++ b/lib/context.js @@ -20,15 +20,21 @@ async function refresh() { const client = await getClient(config); const oldTokenSet = tokenSet.call(this); const newTokenSet = await client.refresh(oldTokenSet); - // If no new refresh token assume the current refresh token is valid. - if (!newTokenSet.refresh_token) { - newTokenSet.refresh_token = oldTokenSet.refresh_token; - } - // Update the session's tokenSet + // 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); - cachedTokenSet.value = newTokenSet; + delete cachedTokenSet.value; return this.accessToken; } diff --git a/package-lock.json b/package-lock.json index 8bb8dc2b..c775b1c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -399,6 +399,15 @@ "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://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", @@ -453,6 +462,15 @@ "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.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -478,6 +496,24 @@ "@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.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", @@ -523,6 +559,42 @@ } } }, + "@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/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/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.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", @@ -573,6 +645,16 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" }, + "@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", @@ -595,6 +677,12 @@ "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", "dev": true }, + "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": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -643,6 +731,12 @@ "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", @@ -734,6 +828,12 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "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", @@ -757,6 +857,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", @@ -777,6 +883,17 @@ "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", @@ -856,12 +973,44 @@ "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", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "dev": true + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "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-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -988,6 +1137,12 @@ "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", @@ -1066,6 +1221,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", @@ -1150,6 +1311,24 @@ "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": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "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 + } + } + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1222,6 +1401,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", @@ -1256,6 +1441,12 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "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", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1267,6 +1458,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", @@ -1282,6 +1479,12 @@ "esutils": "^2.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", @@ -1297,12 +1500,30 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "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", @@ -1670,6 +1891,55 @@ } } }, + "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": { + "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 + }, + "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", @@ -1687,6 +1957,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", @@ -1711,6 +2004,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", @@ -1729,6 +2031,15 @@ "flat-cache": "^2.0.1" } }, + "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": { + "minimatch": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1913,6 +2224,12 @@ "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", @@ -2132,6 +2449,16 @@ "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", @@ -2160,6 +2487,16 @@ "sshpk": "^1.7.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", @@ -2239,6 +2576,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", @@ -2376,6 +2719,12 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "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", @@ -2602,6 +2951,18 @@ "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.27.1", "resolved": "https://registry.npmjs.org/jose/-/jose-1.27.1.tgz", @@ -2691,6 +3052,24 @@ "graceful-fs": "^4.1.6" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -2709,6 +3088,36 @@ "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", "dev": true }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "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", @@ -2717,6 +3126,81 @@ "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 + }, + "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": { + "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": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -2754,6 +3238,48 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -2910,6 +3436,12 @@ "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": "7.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", @@ -3012,6 +3544,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", @@ -3396,6 +3934,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", @@ -3440,6 +4044,12 @@ } } }, + "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", @@ -3515,6 +4125,12 @@ "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==", + "dev": true + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3660,6 +4276,12 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "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", @@ -3830,6 +4452,12 @@ "ipaddr.js": "1.9.0" } }, + "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", "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", @@ -3851,6 +4479,43 @@ "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", @@ -3896,6 +4561,17 @@ } } }, + "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", @@ -4352,6 +5028,23 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -4428,6 +5121,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", @@ -4501,6 +5219,12 @@ "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", @@ -4596,6 +5320,16 @@ "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", @@ -4630,6 +5364,12 @@ "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", @@ -4760,6 +5500,12 @@ "typedarray-to-buffer": "^3.1.5" } }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", @@ -4843,6 +5589,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 e3ed7195..2f68d1d3 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ ], "scripts": { "lint": "eslint . --ignore-path .gitignore", + "start:example": "node ./examples/run_example.js", "test": "mocha", "test:ci": "nyc --reporter=lcov npm test", - "docs": "typedoc --options typedoc.js index.d.ts" + "docs": "typedoc --options typedoc.js index.d.ts", + "test:end-to-end": "mocha end-to-end" }, "mocha": { "exit": true, @@ -40,15 +42,19 @@ "@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", + "express-oauth2-bearer": "^0.4.0", "husky": "^4.2.5", "lodash": "^4.17.15", "mocha": "^7.2.0", "nock": "^11.9.1", "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", "sinon": "^7.5.0", diff --git a/test/callback.tests.js b/test/callback.tests.js index 947a9248..18abd6e7 100644 --- a/test/callback.tests.js +++ b/test/callback.tests.js @@ -449,7 +449,7 @@ describe('callback response_mode: form_post', () => { const reply = sinon.spy(() => ({ access_token: '__new_access_token__', refresh_token: '__new_refresh_token__', - id_token: tokens.id_token, + id_token: tokens.idToken, token_type: 'Bearer', expires_in: 86400, })); @@ -474,6 +474,16 @@ describe('callback response_mode: form_post', () => { 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 () => {