From 9c3e7bf16ffc9fb4b2fe85493f7fecd2750c84dd Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 1 Aug 2021 10:47:52 +0200 Subject: [PATCH 001/154] chore: update issue form --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index d5b1eb494..2368cc373 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -35,6 +35,8 @@ body: - type: checkboxes attributes: options: + - label: I have provided a gist or a public repo which can be cloned, installed and ran locally in order to reproduce the bug in the textarea above. + required: true - label: I have searched the issues tracker and discussions for similar topics and couldn't find anything related. required: true - label: I have searched the [FAQ](https://github.com/panva/node-oidc-provider/blob/v7.x/docs/README.md#faq) and couldn't find anything related. From aca5813a5b7e669f30894102ad925b1aec3f3467 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 18 Jul 2021 17:55:28 +0200 Subject: [PATCH 002/154] feat: support v3.local, v3.public, and v4.public paseto access tokens format Note: these are only available when the Node.js runtime is >= v16.0.0 --- README.md | 7 +- docs/README.md | 5 +- lib/helpers/defaults.js | 5 +- lib/models/formats/paseto.js | 120 +++++++++++++++++++++-------- package.json | 5 +- test/formats/formats.config.js | 3 + test/formats/jwt.test.js | 2 +- test/formats/paseto.test.js | 137 +++++++++++++++++++++++---------- 8 files changed, 205 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 0b9542afb..3a6dadca4 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,14 @@ enabled by default, check the configuration section on how to enable them. - [RFC8628 - OAuth 2.0 Device Authorization Grant (Device Flow)][device-flow] - [RFC8705 - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (MTLS)][mtls] - [RFC8707 - OAuth 2.0 Resource Indicators][resource-indicators] -- [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens][jwt-at] - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi] - [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi-id2] +Supported Access Token formats: +- Opaque +- [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens][jwt-at] +- [Platform-Agnostic Security Tokens (PASETO)][paseto-at] + The following draft specifications are implemented by oidc-provider. - [JWT Response for OAuth Token Introspection - draft 10][jwt-introspection] - [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - Implementer's Draft 01][jarm] @@ -153,6 +157,7 @@ See the list of available emitted [event names](/docs/events.md) and their descr [resource-indicators]: https://tools.ietf.org/html/rfc8707 [jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html [jwt-at]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-11 +[paseto-at]: https://paseto.io [support-sponsor]: https://github.com/sponsors/panva [par]: https://tools.ietf.org/html/draft-ietf-oauth-par-08 [rpinitiated-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html diff --git a/docs/README.md b/docs/README.md index ada1fc702..0bb84148b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1819,7 +1819,7 @@ and a JWT Access Token Format. } // PASETO Access Token Format (when accessTokenFormat is 'paseto') paseto?: { - version: 1 | 2, + version: 1 | 2 | 3 | 4, purpose: 'local' | 'public', key?: crypto.KeyObject, // required when purpose is 'local' kid?: string, // OPTIONAL `kid` to aid in signing key selection or to put in the footer for 'local' @@ -2325,7 +2325,7 @@ _**default value**_: } ``` -
(Click to expand) To push a payload and a footer to a PASETO structured access token +
(Click to expand) To push a payload, a footer, and use an implicit assertion with a PASETO structured access token
```js @@ -2334,6 +2334,7 @@ _**default value**_: paseto(ctx, token, structuredToken) { structuredToken.payload.foo = 'bar'; structuredToken.footer = { foo: 'bar' }; + structuredToken.assertion = 'foo'; // v3 and v4 tokens only } } } diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 4f335390b..496c12443 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1722,7 +1722,7 @@ function getDefaults() { * * // PASETO Access Token Format (when accessTokenFormat is 'paseto') * paseto?: { - * version: 1 | 2, + * version: 1 | 2 | 3 | 4, * purpose: 'local' | 'public', * key?: crypto.KeyObject, // required when purpose is 'local' * kid?: string, // OPTIONAL `kid` to aid in signing key selection or to put in the footer for 'local' @@ -1948,13 +1948,14 @@ function getDefaults() { * } * ``` * - * example: To push a payload and a footer to a PASETO structured access token + * example: To push a payload, a footer, and use an implicit assertion with a PASETO structured access token * ```js * { * customizers: { * paseto(ctx, token, structuredToken) { * structuredToken.payload.foo = 'bar'; * structuredToken.footer = { foo: 'bar' }; + * structuredToken.assertion = 'foo'; // v3 and v4 tokens only * } * } * } diff --git a/lib/models/formats/paseto.js b/lib/models/formats/paseto.js index 7166bd231..d1b3b4e5b 100644 --- a/lib/models/formats/paseto.js +++ b/lib/models/formats/paseto.js @@ -1,7 +1,22 @@ const { strict: assert } = require('assert'); const crypto = require('crypto'); -const paseto = require('paseto'); +let paseto; +let paseto3 = process.version.substr(1).split('.').map((x) => parseInt(x, 10))[0] >= 16; + +if (paseto3) { + try { + // eslint-disable-next-line + paseto = require('paseto3'); + } catch (err) { + paseto3 = false; + } +} + +if (!paseto3) { + // eslint-disable-next-line + paseto = require('paseto2'); +} const instance = require('../../helpers/weak_cache'); const nanoid = require('../../helpers/nanoid'); @@ -18,41 +33,62 @@ module.exports = (provider, { opaque }) => { const { version, purpose } = token.resourceServer.paseto; let { key, kid } = token.resourceServer.paseto; + let alg; - if (version !== 1 && version !== 2) { - throw new Error('unsupported "paseto.version"'); + if (version > 2 && !paseto3) { + throw new Error('PASETO v3 and v4 tokens are only supported in Node.js >= 16.0.0 runtimes'); } - if (purpose === 'local' && version === 1) { - if (key === undefined) { - throw new Error('local purpose PASETO Resource Server requires a "paseto.key"'); - } - if (!(key instanceof crypto.KeyObject)) { - key = crypto.createSecretKey(key); - } - if (key.type !== 'secret' || key.symmetricKeySize !== 32) { - throw new Error('local purpose PASETO Resource Server "paseto.key" must be 256 bits long secret key'); - } - } else if (purpose === 'public') { - if (version === 1) { - [key] = keystore.selectForSign({ alg: 'PS384', kid }); - } else if (version === 2) { - [key] = keystore.selectForSign({ alg: 'EdDSA', crv: 'Ed25519', kid }); - } + switch (true) { + case version === 1 && purpose === 'local': + case version === 3 && purpose === 'local': + if (!key) { + throw new Error('local purpose PASETO Resource Server requires a "paseto.key"'); + } + if (!(key instanceof crypto.KeyObject)) { + key = crypto.createSecretKey(key); + } + if (key.type !== 'secret' || key.symmetricKeySize !== 32) { + throw new Error('local purpose PASETO Resource Server "paseto.key" must be 256 bits long secret key'); + } + break; + case version === 1 && purpose === 'public': + alg = 'PS384'; + [key] = keystore.selectForSign({ + alg, kid, kty: 'RSA', + }); + break; + case (version === 2 || version === 4) && purpose === 'public': + alg = 'EdDSA'; + [key] = keystore.selectForSign({ + alg, crv: 'Ed25519', kid, kty: 'OKP', + }); + break; + case version === 3 && purpose === 'public': + alg = 'ES384'; + [key] = keystore.selectForSign({ + alg, crv: 'P-384', kid, kty: 'EC', + }); + break; + default: + throw new Error('unsupported PASETO version and purpose'); + } + + if (purpose === 'public') { if (!key) { throw new Error('resolved Resource Server paseto configuration has no corresponding key in the provider\'s keystore'); } - kid = key.kid; - key = await keystore.getKeyObject(key, version === 1 ? 'RS384' : 'EdDSA').catch(() => { + ({ kid } = key); + // eslint-disable-next-line no-nested-ternary + key = await keystore.getKeyObject(key, alg).catch(() => { throw new Error(`provider key (kid: ${kid}) is invalid`); }); - } else { - throw new Error('unsupported PASETO version and purpose'); } if (kid !== undefined && typeof kid !== 'string') { throw new Error('paseto.kid must be a string when provided'); } + return { version, purpose, key, kid, }; @@ -104,6 +140,7 @@ module.exports = (provider, { opaque }) => { const structuredToken = { footer: undefined, payload: tokenPayload, + assertion: undefined, }; const customizer = instance(provider).configuration('formats.customizers.paseto'); @@ -112,25 +149,41 @@ module.exports = (provider, { opaque }) => { } if (!structuredToken.payload.aud) { - throw new Error('JWT Access Tokens must contain an audience, for Access Tokens without audience (only usable at the userinfo_endpoint) use an opaque format'); + throw new Error('PASETO Access Tokens must contain an audience, for Access Tokens without audience (only usable at the userinfo_endpoint) use an opaque format'); } - const config = await getResourceServerConfig(this); + const { + version, purpose, kid, key, + } = await getResourceServerConfig(this); let issue; - if (config.version === 1) { - issue = config.purpose === 'local' ? paseto.V1.encrypt : paseto.V1.sign; - } else { - issue = paseto.V2.sign; + // eslint-disable-next-line default-case + switch (version) { + case 1: + issue = purpose === 'local' ? paseto.V1.encrypt : paseto.V1.sign; + break; + case 2: + issue = paseto.V2.sign; + break; + case 3: + issue = purpose === 'local' ? paseto.V3.encrypt : paseto.V3.sign; + break; + case 4: + issue = paseto.V4.sign; + break; + } + + if (structuredToken.assertion !== undefined && version < 3) { + throw new Error('only PASETO v3 and v4 tokens support an implicit assertion'); } /* eslint-disable no-unused-expressions */ - if (config.kid) { + if (kid) { structuredToken.footer || (structuredToken.footer = {}); - structuredToken.footer.kid || (structuredToken.footer.kid = config.kid); + structuredToken.footer.kid || (structuredToken.footer.kid = kid); } - if (config.purpose === 'local') { + if (purpose === 'local') { structuredToken.footer || (structuredToken.footer = {}); structuredToken.footer.iss || (structuredToken.footer.iss = provider.issuer); structuredToken.footer.aud || (structuredToken.footer.aud = structuredToken.payload.aud); @@ -139,10 +192,11 @@ module.exports = (provider, { opaque }) => { const token = await issue( structuredToken.payload, - config.key, + key, { footer: structuredToken.footer ? JSON.stringify(structuredToken.footer) : undefined, iat: false, + assertion: structuredToken.assertion ? structuredToken.assertion : undefined, }, ); diff --git a/package.json b/package.json index c9b3d8456..602d6f2d0 100644 --- a/package.json +++ b/package.json @@ -69,10 +69,13 @@ "nanoid": "^3.1.15", "object-hash": "^2.0.3", "oidc-token-hash": "^5.0.1", - "paseto": "^2.1.0", + "paseto2": "npm:paseto@^2.1.3", "quick-lru": "^5.1.1", "raw-body": "^2.4.1" }, + "optionalDependencies": { + "paseto3": "npm:paseto@^3.0.0" + }, "devDependencies": { "@hapi/hapi": "^20.0.1", "babel-eslint": "^10.1.0", diff --git a/test/formats/formats.config.js b/test/formats/formats.config.js index 01635b987..67df6f1df 100644 --- a/test/formats/formats.config.js +++ b/test/formats/formats.config.js @@ -1,8 +1,11 @@ const cloneDeep = require('lodash/cloneDeep'); const merge = require('lodash/merge'); +const jose = require('jose2'); const config = cloneDeep(require('../default.config')); +config.jwks = global.keystore.toJWKS(true); +config.jwks.keys.push(jose.JWK.generateSync('EC', 'P-384', { use: 'sig' }).toJWK(true)); config.extraTokenClaims = () => ({ foo: 'bar' }); merge(config.features, { registration: { diff --git a/test/formats/jwt.test.js b/test/formats/jwt.test.js index 63d29d783..1cc46b146 100644 --- a/test/formats/jwt.test.js +++ b/test/formats/jwt.test.js @@ -425,7 +425,7 @@ describe('jwt format', () => { audience: 'foo', jwt: { sign: { - alg: 'ES384', + alg: 'ES512', }, }, }; diff --git a/test/formats/paseto.test.js b/test/formats/paseto.test.js index 4137a1805..78c720fd9 100644 --- a/test/formats/paseto.test.js +++ b/test/formats/paseto.test.js @@ -6,7 +6,16 @@ const util = require('util'); const sinon = require('sinon').createSandbox(); const { expect } = require('chai'); -const paseto = require('paseto'); + +let paseto; +const above16 = process.version.substr(1).split('.').map((x) => parseInt(x, 10))[0] >= 16; +if (above16) { + // eslint-disable-next-line + paseto = require('paseto3'); +} else { + // eslint-disable-next-line + paseto = require('paseto2'); +} const epochTime = require('../../lib/helpers/epoch_time'); const bootstrap = require('../test_helper'); @@ -96,6 +105,40 @@ describe('paseto format', () => { expect(await token.save()).to.match(/^v2\.public\./); }); + if (above16) { + it('v3.public', async function () { + const resourceServer = { + accessTokenFormat: 'paseto', + audience: 'foo', + paseto: { + version: 3, + purpose: 'public', + }, + }; + + const client = await this.provider.Client.find(clientId); + const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); + expect(await token.save()).to.match(/^v3\.public\./); + }); + } + + if (above16) { + it('v4.public', async function () { + const resourceServer = { + accessTokenFormat: 'paseto', + audience: 'foo', + paseto: { + version: 4, + purpose: 'public', + }, + }; + + const client = await this.provider.Client.find(clientId); + const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); + expect(await token.save()).to.match(/^v4\.public\./); + }); + } + it('v1.local', async function () { const resourceServer = { accessTokenFormat: 'paseto', @@ -128,25 +171,39 @@ describe('paseto format', () => { expect(await token.save()).to.match(/^v1\.local\./); }); - it('v2.local is not supported', async function () { - const resourceServer = { - accessTokenFormat: 'paseto', - audience: 'foo', - paseto: { - version: 2, - purpose: 'local', - key: crypto.randomBytes(32), - }, - }; + if (above16) { + it('v3.local', async function () { + const resourceServer = { + accessTokenFormat: 'paseto', + audience: 'foo', + paseto: { + version: 3, + purpose: 'local', + key: crypto.randomBytes(32), + }, + }; + + const client = await this.provider.Client.find(clientId); + const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); + expect(await token.save()).to.match(/^v3\.local\./); + }); - const client = await this.provider.Client.find(clientId); - const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); - return assert.rejects(token.save(), (err) => { - expect(err).to.be.an('error'); - expect(err.message).to.equal('unsupported PASETO version and purpose'); - return true; + it('v3.local (keyObject)', async function () { + const resourceServer = { + accessTokenFormat: 'paseto', + audience: 'foo', + paseto: { + version: 3, + purpose: 'local', + key: crypto.createSecretKey(crypto.randomBytes(32)), + }, + }; + + const client = await this.provider.Client.find(clientId); + const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); + expect(await token.save()).to.match(/^v3\.local\./); }); - }); + } it('public kid selection failing', async function () { const resourceServer = { @@ -194,8 +251,8 @@ describe('paseto format', () => { accessTokenFormat: 'paseto', audience: 'foo', paseto: { - version: 1, - purpose: 'foobar', + version: 2, + purpose: 'local', }, }; @@ -208,25 +265,6 @@ describe('paseto format', () => { }); }); - it('unsupported "paseto.version"', async function () { - const resourceServer = { - accessTokenFormat: 'paseto', - audience: 'foo', - paseto: { - version: 3, - purpose: 'foobar', - }, - }; - - const client = await this.provider.Client.find(clientId); - const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); - return assert.rejects(token.save(), (err) => { - expect(err).to.be.an('error'); - expect(err.message).to.equal('unsupported "paseto.version"'); - return true; - }); - }); - it('local paseto requires a key', async function () { const resourceServer = { accessTokenFormat: 'paseto', @@ -321,6 +359,27 @@ describe('paseto format', () => { }); }); + if (!above16) { + it('only >= 16.0.0 node supports v3 and v4', async function () { + const resourceServer = { + accessTokenFormat: 'paseto', + audience: 'foo', + paseto: { + version: 3, + purpose: 'public', + }, + }; + + const client = await this.provider.Client.find(clientId); + const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); + return assert.rejects(token.save(), (err) => { + expect(err).to.be.an('error'); + expect(err.message).to.equal('PASETO v3 and v4 tokens are only supported in Node.js >= 16.0.0 runtimes'); + return true; + }); + }); + } + it('invalid paseto configuration type', async function () { const resourceServer = { accessTokenFormat: 'paseto', From bff6c058838f5b3fea2c604c0f8466b542a83b55 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 3 Aug 2021 13:39:53 +0200 Subject: [PATCH 003/154] docs: update README.md --- README.md | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3a6dadca4..c584d04ef 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ additional features and standards implemented. ## Implemented specs & features -The following specifications are implemented by oidc-provider. Note that not all features are -enabled by default, check the configuration section on how to enable them. +The following specifications are implemented by oidc-provider: + +_Note that not all features are enabled by default, check the configuration section on how to enable them._ - [RFC6749 - OAuth 2.0][oauth2] & [OpenID Connect Core 1.0][core] - Authorization (Authorization Code Flow, Implicit Flow, Hybrid Flow) @@ -40,11 +41,13 @@ enabled by default, check the configuration section on how to enable them. - [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi-id2] Supported Access Token formats: + - Opaque - [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens][jwt-at] - [Platform-Agnostic Security Tokens (PASETO)][paseto-at] -The following draft specifications are implemented by oidc-provider. +The following draft specifications are implemented by oidc-provider: + - [JWT Response for OAuth Token Introspection - draft 10][jwt-introspection] - [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - Implementer's Draft 01][jarm] - [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba] @@ -70,7 +73,7 @@ conforms to the following profiles of the OpenID Connect™ protocol - Basic OP, Implicit OP, Hybrid OP, Config OP, Dynamic OP, Form Post OP, 3rd Party-Init OP - Back-Channel OP, RP-Initiated OP - FAPI 1.0 Advanced Final (w/ Private Key JWT, MTLS, JARM, PAR) -- FAPI 1.0 Second Implementer’s Draft (w/ Private Key JWT, MTLS, PAR) +- FAPI 1.0 Second Implementer's Draft (w/ Private Key JWT, MTLS, PAR) - FAPI-CIBA OP (w/ Private Key JWT, MTLS, Ping mode, Poll mode) ## Sponsor @@ -96,25 +99,18 @@ various ways to fit a variety of uses. See the [documentation](/docs/README.md). ```js const { Provider } = require('oidc-provider'); const configuration = { - // ... see available options /docs + // ... see /docs for available configuration clients: [{ client_id: 'foo', client_secret: 'bar', redirect_uris: ['http://lvh.me:8080/cb'], - // + other client properties + // ... other client properties }], }; const oidc = new Provider('http://localhost:3000', configuration); -// express/nodejs style application callback (req, res, next) for use with express apps, see /examples/express.js -oidc.callback() - -// koa application for use with koa apps, see /examples/koa.js -oidc.app - -// or just expose a server standalone, see /examples/standalone.js -const server = oidc.listen(3000, () => { +oidc.listen(3000, () => { console.log('oidc-provider listening on port 3000, check http://localhost:3000/.well-known/openid-configuration'); }); ``` @@ -125,10 +121,8 @@ Collection of useful configurations use cases are available over at [recipes](/r ## Events -Your oidc-provider instance is an event emitter, using event handlers you can hook into the various -actions and i.e. emit metrics or that react to specific triggers. In some scenarios you can even -change the defined behavior. -See the list of available emitted [event names](/docs/events.md) and their description. +oidc-provider instances are event emitters, using event handlers you can hook into the various +actions and i.e. emit metrics that react to specific triggers. See the list of available emitted [event names](/docs/events.md) and their description. [npm-url]: https://www.npmjs.com/package/oidc-provider From 997ece58b1ff41695f62506d1a1cecde43045142 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 3 Aug 2021 13:44:58 +0200 Subject: [PATCH 004/154] chore(release): 7.6.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77db84673..d8be566d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.6.0](https://github.com/panva/node-oidc-provider/compare/v7.5.4...v7.6.0) (2021-08-03) + + +### Features + +* support v3.local, v3.public, and v4.public paseto access tokens format ([aca5813](https://github.com/panva/node-oidc-provider/commit/aca5813a5b7e669f30894102ad925b1aec3f3467)) + ## [7.5.4](https://github.com/panva/node-oidc-provider/compare/v7.5.3...v7.5.4) (2021-07-21) diff --git a/package.json b/package.json index 602d6f2d0..4ce599bf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.5.4", + "version": "7.6.0", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 82ab069f7a372b6cf6fff1551cd2e016df447adb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 4 Aug 2021 16:12:29 +0200 Subject: [PATCH 005/154] ci: update conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3894184d..6eb124240 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.19 + VERSION: release-v4.1.21 steps: - name: Checkout uses: actions/checkout@master From 287a3665d709c0452d0e5096ebdf939265ac959e Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 11 Aug 2021 17:32:17 +0200 Subject: [PATCH 006/154] docs: update readmes --- .github/ISSUE_TEMPLATE/config.yml | 11 +++++------ README.md | 2 +- docs/README.md | 2 +- recipes/README.md | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a525de907..fc8c1d2be 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,18 +1,17 @@ blank_issues_enabled: false contact_links: + - name: Support the project + url: https://github.com/sponsors/panva + about: + To make sure you get your questions answered - name: ❓ Question url: https://github.com/panva/node-oidc-provider/discussions/894 about: - Have a question about using oidc-provider? Head over to the discussions "Q&A" Category + Have a question about using oidc-provider? Support the project and then head over to the discussions "Q&A" Category - name: 💡 Feature proposal url: https://github.com/panva/node-oidc-provider/discussions/893 about: Have a proposal for a new feature? Head over to the discussions "Ideas" Category - - name: Support the project - url: https://github.com/sponsors/panva - about: - Are you asking your nth question? Relying on oidc-provider for critical operations? Consider - supporting the project so that it may continue being maintained. - name: Report a security vulnerability url: https://en.wikipedia.org/wiki/Responsible_disclosure about: diff --git a/README.md b/README.md index c584d04ef..d274413e3 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ conforms to the following profiles of the OpenID Connect™ protocol ## Support -If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors. I make it a best effort to try and answer newcomers regardless of being a supporter or not, but if you're asking your n-th question and don't get an answer it's because I'm out of handouts and spare time to give. +If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors. ## Get started You may check the [example folder](/example) or follow a [step by step example][example-repo] to see diff --git a/docs/README.md b/docs/README.md index 0bb84148b..55b3298c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ is a good starting point to get an idea of what you should provide. ## Support -If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors. I make it a best effort to try and answer newcomers regardless of being a supporter or not, but if you're asking your n-th question and don't get an answer it's because I'm out of handouts and spare time to give. +If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors.
diff --git a/recipes/README.md b/recipes/README.md index 67d38992d..5a38fc7c4 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -5,7 +5,7 @@ This is a collection of useful configurations fitting those various use cases. ## Support -If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors. I make it a best effort to try and answer newcomers regardless of being a supporter or not, but if you're asking your n-th question and don't get an answer it's because I'm out of handouts and spare time to give. +If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors.
From 2b91e5bc2338e6b45eb73fbeb8aa045ffb1e367a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 21 Aug 2021 13:38:52 +0200 Subject: [PATCH 007/154] docs: update JAR RFC url --- README.md | 4 ++-- docs/README.md | 2 +- lib/helpers/defaults.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d274413e3..0c00d397c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ _Note that not all features are enabled by default, check the configuration sect - [RFC8628 - OAuth 2.0 Device Authorization Grant (Device Flow)][device-flow] - [RFC8705 - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (MTLS)][mtls] - [RFC8707 - OAuth 2.0 Resource Indicators][resource-indicators] +- [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][jar] - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi] - [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi-id2] @@ -53,7 +54,6 @@ The following draft specifications are implemented by oidc-provider: - [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba] - [OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 01][iss-auth-resp] - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop] -- [OAuth 2.0 JWT Secured Authorization Request (JAR) - draft 33][jar] - [OAuth 2.0 Pushed Authorization Requests (PAR) - draft 08][par] - [OpenID Connect Back-Channel Logout 1.0 - draft 06][backchannel-logout] - [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 (CIBA) - draft-03][ciba] @@ -142,7 +142,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [backchannel-logout]: https://openid.net/specs/openid-connect-backchannel-1_0-06.html [registration-management]: https://tools.ietf.org/html/rfc7592 [oauth-native-apps]: https://tools.ietf.org/html/rfc8252 -[jar]: https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-33 +[jar]: https://www.rfc-editor.org/rfc/rfc9101.html [device-flow]: https://tools.ietf.org/html/rfc8628 [jwt-introspection]: https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response-10 [sponsor-auth0]: https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=oidc-provider&utm_content=auth diff --git a/docs/README.md b/docs/README.md index 55b3298c9..fbc0725df 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1584,7 +1584,7 @@ false ### features.requestObjects -[Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) and [JWT Secured Authorization Request (JAR)](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-33) - Request Object +[Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) and [JWT Secured Authorization Request (JAR)](https://www.rfc-editor.org/rfc/rfc9101.html) - Request Object Enables the use and validations of the `request` and/or `request_uri` parameters. diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 496c12443..18a9ec192 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1736,7 +1736,7 @@ function getDefaults() { /* * features.requestObjects * - * title: [Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) and [JWT Secured Authorization Request (JAR)](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-33) - Request Object + * title: [Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) and [JWT Secured Authorization Request (JAR)](https://www.rfc-editor.org/rfc/rfc9101.html) - Request Object * * description: Enables the use and validations of the `request` and/or `request_uri` * parameters. From cc8bc0d651e8111a144cb3eeaf7f61600dd074f2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 2 Sep 2021 12:29:12 +0200 Subject: [PATCH 008/154] feat: CIBA Core 1.0 is now a stable feature --- README.md | 4 ++-- docs/README.md | 3 +-- lib/helpers/defaults.js | 3 +-- lib/helpers/features.js | 7 +------ 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0c00d397c..5fdd4ca8a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ _Note that not all features are enabled by default, check the configuration sect - [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][jar] - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi] - [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi-id2] +- [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 (CIBA)][ciba] Supported Access Token formats: @@ -56,7 +57,6 @@ The following draft specifications are implemented by oidc-provider: - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop] - [OAuth 2.0 Pushed Authorization Requests (PAR) - draft 08][par] - [OpenID Connect Back-Channel Logout 1.0 - draft 06][backchannel-logout] -- [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 (CIBA) - draft-03][ciba] - [OpenID Connect RP-Initiated Logout 1.0 - draft 01][rpinitiated-logout] Updates to draft specification versions are released as MINOR library versions, @@ -158,5 +158,5 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01 [fapi-id2]: https://openid.net/specs/openid-financial-api-part-2-ID2.html [fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html -[ciba]: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-03.html +[ciba]: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html [fapi-ciba]: https://openid.net/specs/openid-financial-api-ciba-ID1.html diff --git a/docs/README.md b/docs/README.md index fbc0725df..989c44abb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -639,7 +639,7 @@ _**default value**_: ### features.ciba -[OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 - draft-03](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-03.html) +[OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html) Enables Core CIBA Flow, when combined with `features.fapi` enables [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementer's Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) as well. @@ -648,7 +648,6 @@ Enables Core CIBA Flow, when combined with `features.fapi` enables [Financial-gr _**default value**_: ```js { - ack: undefined, deliveryModes: [ 'poll' ], diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 18a9ec192..893fe9419 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -922,14 +922,13 @@ function getDefaults() { /* * features.ciba * - * title: [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 - draft-03](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-03.html) + * title: [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html) * * description: Enables Core CIBA Flow, when combined with `features.fapi` enables [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementer's Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) as well. * */ ciba: { enabled: false, - ack: undefined, /* * features.ciba.deliveryModes diff --git a/lib/helpers/features.js b/lib/helpers/features.js index 372ac2231..e4db70adc 100644 --- a/lib/helpers/features.js +++ b/lib/helpers/features.js @@ -1,4 +1,5 @@ const STABLE = new Set([ + 'ciba', 'claimsParameter', 'clientCredentials', 'deviceFlow', @@ -60,12 +61,6 @@ const DRAFTS = new Map(Object.entries({ url: 'https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01', version: ['draft-00', 'draft-01'], }, - ciba: { - name: 'OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 - draft 03', - type: 'OIDF MODRNA Working Group draft', - url: 'https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-03.html', - version: ['draft-03'], - }, })); module.exports = { From 6f6d6fbb0f041b551b2007b8492593247d7c26eb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 2 Sep 2021 12:31:13 +0200 Subject: [PATCH 009/154] ci: update conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6eb124240..69a584f94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.21 + VERSION: release-v4.1.26 steps: - name: Checkout uses: actions/checkout@master From 721ad5e53060419ee07e1f75412ebfc4cf145c53 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 2 Sep 2021 12:44:21 +0200 Subject: [PATCH 010/154] docs: update README.md --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 5fdd4ca8a..01ef8342c 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,6 @@ The following specifications are implemented by oidc-provider: _Note that not all features are enabled by default, check the configuration section on how to enable them._ - [RFC6749 - OAuth 2.0][oauth2] & [OpenID Connect Core 1.0][core] - - Authorization (Authorization Code Flow, Implicit Flow, Hybrid Flow) - - UserInfo Endpoint and ID Tokens including Signing and Encryption - - Passing a Request Object by Value or Reference including Signing and Encryption - - Public and Pairwise Subject Identifier Types - - Offline Access / Refresh Token Grant - - Client Credentials Grant - - Client Authentication incl. client_secret_jwt and private_key_jwt methods - [OpenID Connect Discovery 1.0][discovery] - [OpenID Connect Dynamic Client Registration 1.0][registration] and [RFC7591 - OAuth 2.0 Dynamic Client Registration Protocol][oauth2-registration] - [OAuth 2.0 Form Post Response Mode][form-post] @@ -39,7 +32,6 @@ _Note that not all features are enabled by default, check the configuration sect - [RFC8707 - OAuth 2.0 Resource Indicators][resource-indicators] - [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][jar] - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi] -- [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi-id2] - [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 (CIBA)][ciba] Supported Access Token formats: @@ -156,7 +148,6 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [par]: https://tools.ietf.org/html/draft-ietf-oauth-par-08 [rpinitiated-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html [iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01 -[fapi-id2]: https://openid.net/specs/openid-financial-api-part-2-ID2.html [fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html [ciba]: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html [fapi-ciba]: https://openid.net/specs/openid-financial-api-ciba-ID1.html From 2f9bce96d52caa3f93b91ab3d3ea004f394fcef7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 2 Sep 2021 12:45:55 +0200 Subject: [PATCH 011/154] chore(release): 7.7.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8be566d3..0f634fab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.7.0](https://github.com/panva/node-oidc-provider/compare/v7.6.0...v7.7.0) (2021-09-02) + + +### Features + +* CIBA Core 1.0 is now a stable feature ([cc8bc0d](https://github.com/panva/node-oidc-provider/commit/cc8bc0d651e8111a144cb3eeaf7f61600dd074f2)) + ## [7.6.0](https://github.com/panva/node-oidc-provider/compare/v7.5.4...v7.6.0) (2021-08-03) diff --git a/package.json b/package.json index 4ce599bf0..1a9e30222 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.6.0", + "version": "7.7.0", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 90234c09e6d72f7874eade4a3988d840855fc950 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 7 Sep 2021 11:28:35 +0200 Subject: [PATCH 012/154] chore: rm .github/workflows/label-sponsors.yml --- .github/workflows/label-sponsors.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .github/workflows/label-sponsors.yml diff --git a/.github/workflows/label-sponsors.yml b/.github/workflows/label-sponsors.yml deleted file mode 100644 index 1e18063b6..000000000 --- a/.github/workflows/label-sponsors.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Label sponsors -on: - issues: - types: [opened] -jobs: - build: - name: is-sponsor-label - runs-on: ubuntu-latest - steps: - - uses: JasonEtco/is-sponsor-label-action@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 221e2498439d18f28ac2734f2ca62451d5248500 Mon Sep 17 00:00:00 2001 From: Brandon Sheehy Date: Fri, 10 Sep 2021 14:28:41 -0500 Subject: [PATCH 013/154] example: certification code typo --- certification/oidc/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certification/oidc/index.js b/certification/oidc/index.js index 6fe0d48a7..f9ff125c8 100644 --- a/certification/oidc/index.js +++ b/certification/oidc/index.js @@ -105,7 +105,7 @@ let server; break; } case 'device_authorization': { - if (ctx.stats === 200) { + if (ctx.status === 200) { ctx.body.verification_uri = ctx.body.verification_uri.replace('https://mtls.', 'https://'); ctx.body.verification_uri_complete = ctx.body.verification_uri_complete.replace('https://mtls.', 'https://'); } From 3c54d8ddb85d72fc9432c283b3bea417a895afca Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 15 Sep 2021 23:06:21 +0200 Subject: [PATCH 014/154] feat: OAuth 2.0 Pushed Authorization Requests (PAR) is now a stable feature --- README.md | 4 ++-- docs/README.md | 8 ++------ lib/helpers/defaults.js | 12 ++---------- lib/helpers/features.js | 9 ++------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 01ef8342c..48ae62ff5 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ _Note that not all features are enabled by default, check the configuration sect - [RFC8705 - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (MTLS)][mtls] - [RFC8707 - OAuth 2.0 Resource Indicators][resource-indicators] - [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][jar] +- [RFC9126 - OAuth 2.0 Pushed Authorization Requests (PAR) - draft 08][par] - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi] - [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 (CIBA)][ciba] @@ -47,7 +48,6 @@ The following draft specifications are implemented by oidc-provider: - [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba] - [OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 01][iss-auth-resp] - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop] -- [OAuth 2.0 Pushed Authorization Requests (PAR) - draft 08][par] - [OpenID Connect Back-Channel Logout 1.0 - draft 06][backchannel-logout] - [OpenID Connect RP-Initiated Logout 1.0 - draft 01][rpinitiated-logout] @@ -145,7 +145,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [jwt-at]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-11 [paseto-at]: https://paseto.io [support-sponsor]: https://github.com/sponsors/panva -[par]: https://tools.ietf.org/html/draft-ietf-oauth-par-08 +[par]: https://www.rfc-editor.org/rfc/rfc9126.html [rpinitiated-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html [iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01 [fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html diff --git a/docs/README.md b/docs/README.md index 989c44abb..17a0632ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1351,18 +1351,14 @@ false ### features.pushedAuthorizationRequests -[draft-ietf-oauth-par-08](https://tools.ietf.org/html/draft-ietf-oauth-par-08) - OAuth 2.0 Pushed Authorization Requests (PAR) +[RFC9126](https://www.rfc-editor.org/rfc/rfc9126.html) - OAuth 2.0 Pushed Authorization Requests (PAR) -Enables the use of `pushed_authorization_request_endpoint` defined by the Pushed Authorization Requests draft. - - -_**recommendation**_: Updates to draft specification versions are released as MINOR library versions, if you utilize these specification implementations consider using the tilde `~` operator in your package.json since breaking changes may be introduced as part of these version updates. Alternatively, [acknowledge](#features) the version and be notified of breaking changes as part of your CI. +Enables the use of `pushed_authorization_request_endpoint` defined by the Pushed Authorization Requests RFC. _**default value**_: ```js { - ack: undefined, enabled: false, requirePushedAuthorizationRequests: false } diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 893fe9419..db06926d9 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1368,22 +1368,14 @@ function getDefaults() { /* * features.pushedAuthorizationRequests * - * title: [draft-ietf-oauth-par-08](https://tools.ietf.org/html/draft-ietf-oauth-par-08) - OAuth 2.0 Pushed Authorization Requests (PAR) + * title: [RFC9126](https://www.rfc-editor.org/rfc/rfc9126.html) - OAuth 2.0 Pushed Authorization Requests (PAR) * * description: Enables the use of `pushed_authorization_request_endpoint` defined by the Pushed - * Authorization Requests draft. - * - * recommendation: Updates to draft specification versions are released as MINOR library versions, - * if you utilize these specification implementations consider using the tilde `~` operator - * in your package.json since breaking changes may be introduced as part of these version - * updates. Alternatively, [acknowledge](#features) the version and be notified of breaking - * changes as part of your CI. + * Authorization Requests RFC. */ pushedAuthorizationRequests: { enabled: false, - ack: undefined, - /* * features.pushedAuthorizationRequests.requirePushedAuthorizationRequests * diff --git a/lib/helpers/features.js b/lib/helpers/features.js index e4db70adc..0aaba3cb2 100644 --- a/lib/helpers/features.js +++ b/lib/helpers/features.js @@ -5,10 +5,11 @@ const STABLE = new Set([ 'deviceFlow', 'devInteractions', 'encryption', + 'fapi', 'introspection', 'jwtUserinfo', 'mTLS', - 'fapi', + 'pushedAuthorizationRequests', 'registration', 'registrationManagement', 'requestObjects', @@ -43,12 +44,6 @@ const DRAFTS = new Map(Object.entries({ url: 'https://openid.net/specs/openid-financial-api-jarm-ID1.html', version: [1, 2, 'draft-02', 'implementers-draft-01'], }, - pushedAuthorizationRequests: { - name: 'OAuth 2.0 Pushed Authorization Requests - draft 08', - type: 'IETF OAuth Working Group draft', - url: 'https://tools.ietf.org/html/draft-ietf-oauth-par-08', - version: [0, 'individual-draft-01', 'draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07', 'draft-08'], - }, webMessageResponseMode: { name: 'OAuth 2.0 Web Message Response Mode - draft 00', type: 'Individual draft', From 0477791a48c648b223f7b7f4b6508159a8d9d37b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 15 Sep 2021 23:14:53 +0200 Subject: [PATCH 015/154] chore(release): 7.8.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f634fab1..a2e55c17d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.8.0](https://github.com/panva/node-oidc-provider/compare/v7.7.0...v7.8.0) (2021-09-15) + + +### Features + +* OAuth 2.0 Pushed Authorization Requests (PAR) is now a stable feature ([3c54d8d](https://github.com/panva/node-oidc-provider/commit/3c54d8ddb85d72fc9432c283b3bea417a895afca)) + ## [7.7.0](https://github.com/panva/node-oidc-provider/compare/v7.6.0...v7.7.0) (2021-09-02) diff --git a/package.json b/package.json index 1a9e30222..51ec13e2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.7.0", + "version": "7.8.0", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 06c2bdeedd08a0fdb82c70d5daa36f96397aee96 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 22 Sep 2021 16:41:31 +0200 Subject: [PATCH 016/154] ci: bump conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69a584f94..56967c188 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.26 + VERSION: release-v4.1.28 steps: - name: Checkout uses: actions/checkout@master From c8fc11eb1b8a0002569f39b181706919e3b3ee23 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 27 Sep 2021 16:03:02 +0100 Subject: [PATCH 017/154] docs: fix issueRegistrationAccessToken description (#1119) --- docs/README.md | 4 ++-- lib/helpers/defaults.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 17a0632ff..0bfa1b608 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1436,8 +1436,8 @@ new (provider.InitialAccessToken)({}).save().then(console.log); #### issueRegistrationAccessToken Boolean or a function used to decide whether a registration access token will be issued or not. Supported values are - - `false` registration access tokens is issued - - `true` registration access tokens is not issued + - `true` registration access tokens is issued + - `false` registration access tokens is not issued - function returning true/false, true when token should be issued, false when it shouldn't diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index db06926d9..98ff96ff3 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1491,8 +1491,8 @@ function getDefaults() { * description: Boolean or a function used to decide whether a registration access token will be * issued or not. Supported * values are - * - `false` registration access tokens is issued - * - `true` registration access tokens is not issued + * - `true` registration access tokens is issued + * - `false` registration access tokens is not issued * - function returning true/false, true when token should be issued, false when it shouldn't * * example: To determine if a registration access token should be issued dynamically From 2b3870f2ab2d31b9fe7a12a6f48a856ce84f9df5 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 27 Sep 2021 16:08:32 +0100 Subject: [PATCH 018/154] test: fix test descriptions (#1120) --- test/dynamic_registration/dynamic_registration.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dynamic_registration/dynamic_registration.test.js b/test/dynamic_registration/dynamic_registration.test.js index 6f8772ed1..03c54f99d 100644 --- a/test/dynamic_registration/dynamic_registration.test.js +++ b/test/dynamic_registration/dynamic_registration.test.js @@ -119,7 +119,7 @@ describe('registration features', () => { i(this.provider).configuration('features.registration').issueRegistrationAccessToken = this.orig; }); - it('omits issuing a registration access token and does not return registration_client_uri', function () { + it('issues a registration access token and does return registration_client_uri', function () { return this.agent.post('/reg') .send({ redirect_uris: ['https://client.example.com/cb'], @@ -154,7 +154,7 @@ describe('registration features', () => { i(this.provider).configuration('features.registration').issueRegistrationAccessToken = this.orig; }); - it('omits issuing a registration access token and does not return registration_client_uri', function () { + it('issues a registration access token and does return registration_client_uri', function () { return this.agent.post('/reg') .send({ redirect_uris: ['https://client.example.com/cb'], From 7c217378a64b8de13277be37b127c50d7bee00b8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 Sep 2021 17:40:44 +0200 Subject: [PATCH 019/154] chore: update deps --- lib/helpers/validate_dpop.js | 2 +- package.json | 50 ++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/helpers/validate_dpop.js b/lib/helpers/validate_dpop.js index c15f6294a..0abfb8763 100644 --- a/lib/helpers/validate_dpop.js +++ b/lib/helpers/validate_dpop.js @@ -34,7 +34,7 @@ module.exports = async (ctx, accessToken) => { return EmbeddedJWK(...args); }, { - maxTokenAge: `${dPoPConfig.iatTolerance} seconds`, + maxTokenAge: dPoPConfig.iatTolerance, clockTolerance, algorithms: dPoPSigningAlgValues, typ: 'dpop+jwt', diff --git a/package.json b/package.json index 51ec13e2a..ab9efced3 100644 --- a/package.json +++ b/package.json @@ -58,16 +58,16 @@ }, "dependencies": { "@koa/cors": "^3.1.0", - "cacheable-lookup": "^6.0.0", - "debug": "^4.1.1", - "ejs": "^3.1.5", - "got": "^11.7.0", - "jose": "^3.12.2", - "jsesc": "^3.0.1", - "koa": "^2.13.0", + "cacheable-lookup": "^6.0.1", + "debug": "^4.3.2", + "ejs": "^3.1.6", + "got": "^11.8.2", + "jose": "^3.19.0", + "jsesc": "^3.0.2", + "koa": "^2.13.3", "koa-compose": "^4.1.0", - "nanoid": "^3.1.15", - "object-hash": "^2.0.3", + "nanoid": "^3.1.28", + "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.1", "paseto2": "npm:paseto@^2.1.3", "quick-lru": "^5.1.1", @@ -77,33 +77,33 @@ "paseto3": "npm:paseto@^3.0.0" }, "devDependencies": { - "@hapi/hapi": "^20.0.1", + "@hapi/hapi": "^20.2.0", "babel-eslint": "^10.1.0", "base64url": "^3.0.1", - "c8": "^7.7.2", - "chai": "^4.2.0", + "c8": "^7.9.0", + "chai": "^4.3.4", "clear-module": "^4.1.1", "connect": "^3.7.0", - "eslint": "^7.11.0", - "eslint-config-airbnb-base": "^14.2.0", - "eslint-plugin-import": "^2.22.1", + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.24.2", "express": "^4.17.1", - "fastify": "^3.7.0", - "helmet": "^4.4.1", + "fastify": "^3.21.6", + "helmet": "^4.6.0", "https-pem": "^2.0.0", "jose2": "npm:jose@^2.0.4", "koa-body": "^4.2.0", "koa-ejs": "^4.3.0", "koa-mount": "^4.0.0", - "koa-router": "^10.0.0", - "lodash": "^4.17.20", - "middie": "^5.2.0", - "mocha": "^8.2.0", - "mocha.parallel": "panva/mocha.parallel", + "koa-router": "^10.1.1", + "lodash": "^4.17.21", + "middie": "^5.3.0", + "mocha": "^8.4.0", + "mocha.parallel": "github:panva/mocha.parallel", "moment": "^2.29.1", - "nock": "^13.0.4", - "sinon": "^11.0.0", - "supertest": "^6.1.3", + "nock": "^13.1.3", + "sinon": "^11.1.2", + "supertest": "^6.1.6", "timekeeper": "^2.2.0" }, "engines": { From 3b2b8b61c81126a939e072ce030b266812713543 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 4 Oct 2021 18:02:40 +0200 Subject: [PATCH 020/154] ci: bump conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56967c188..d8dec8b44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.28 + VERSION: release-v4.1.29 steps: - name: Checkout uses: actions/checkout@master From 2d3cae003448ce2e9f49ee0a4f7a725557aa3982 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 11 Oct 2021 12:30:04 +0200 Subject: [PATCH 021/154] docs: update events.md --- docs/events.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/events.md b/docs/events.md index 42266489e..be3f7c770 100644 --- a/docs/events.md +++ b/docs/events.md @@ -1,6 +1,7 @@ # Events -Your oidc-provider instance is an event emitter, `this` is always the instance. In events where +Your oidc-provider instance is an event emitter, in the event handlers `this` is always the +Provider instance. In events where `ctx` (request context) is passed to the listener `ctx.oidc` [OIDCContext](/lib/helpers/oidc_context.js) holds additional details like recognized parameters, loaded client or session. From ba8a8f0188c9a73a0ab0f8b974bea49feb2a87a6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 12 Oct 2021 13:44:57 +0200 Subject: [PATCH 022/154] fix: use insufficient_scope instead of invalid_scope at userinfo_endpoint --- lib/actions/userinfo.js | 12 ++++++------ lib/helpers/errors.js | 9 +++++++++ test/userinfo/userinfo.test.js | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/actions/userinfo.js b/lib/actions/userinfo.js index 8faa9abe5..37eb94806 100644 --- a/lib/actions/userinfo.js +++ b/lib/actions/userinfo.js @@ -1,4 +1,4 @@ -const { InvalidDpopProof, InvalidToken, InvalidScope } = require('../helpers/errors'); +const { InvalidDpopProof, InvalidToken, InsufficientScope } = require('../helpers/errors'); const difference = require('../helpers/_/difference'); const setWWWAuthenticate = require('../helpers/set_www_authenticate'); const bodyParser = require('../shared/conditional_body'); @@ -74,8 +74,9 @@ module.exports = [ ctx.oidc.entity('AccessToken', accessToken); - if (!accessToken.scope || !accessToken.scope.split(' ').includes('openid')) { - throw new InvalidToken('access token missing openid scope'); + const { scopes } = accessToken; + if (!scopes.size || !scopes.has('openid')) { + throw new InsufficientScope('access token missing openid scope', 'openid'); } if (accessToken['x5t#S256']) { @@ -115,11 +116,10 @@ module.exports = [ async function validateScope(ctx, next) { if (ctx.oidc.params.scope) { - const accessTokenScopes = ctx.oidc.accessToken.scope.split(' '); - const missing = difference(ctx.oidc.params.scope.split(' '), accessTokenScopes); + const missing = difference(ctx.oidc.params.scope.split(' '), [...ctx.oidc.accessToken.scopes]); if (missing.length !== 0) { - throw new InvalidScope('access token missing requested scope', missing.join(' ')); + throw new InsufficientScope('access token missing requested scope', missing.join(' ')); } } await next(); diff --git a/lib/helpers/errors.js b/lib/helpers/errors.js index 111b92f80..7d2caf1c1 100644 --- a/lib/helpers/errors.js +++ b/lib/helpers/errors.js @@ -54,6 +54,14 @@ class InvalidScope extends OIDCProviderError { } } +class InsufficientScope extends OIDCProviderError { + constructor(description, scope, detail) { + super(403, 'insufficient_scope'); + Error.captureStackTrace(this, this.constructor); + Object.assign(this, { scope, error_description: description, error_detail: detail }); + } +} + class InvalidRequest extends OIDCProviderError { constructor(description, code = 400, detail) { super(code, 'invalid_request'); @@ -168,6 +176,7 @@ module.exports.InvalidGrant = InvalidGrant; module.exports.InvalidRedirectUri = InvalidRedirectUri; module.exports.InvalidRequest = InvalidRequest; module.exports.InvalidScope = InvalidScope; +module.exports.InsufficientScope = InsufficientScope; module.exports.InvalidToken = InvalidToken; module.exports.OIDCProviderError = OIDCProviderError; module.exports.SessionNotFound = SessionNotFound; diff --git a/test/userinfo/userinfo.test.js b/test/userinfo/userinfo.test.js index b7255a7ef..a9d3155a5 100644 --- a/test/userinfo/userinfo.test.js +++ b/test/userinfo/userinfo.test.js @@ -77,9 +77,23 @@ describe('userinfo /me', () => { .expect(this.failWith(400, 'invalid_request', 'no access token provided')); }); + it('validates the openid scope is present', async function () { + const at = await new this.provider.AccessToken({ + client: await this.provider.Client.find('client'), + }).save(); + sinon.stub(this.provider.Client, 'find').callsFake(async () => undefined); + return this.agent.get('/me') + .auth(at, { type: 'bearer' }) + .expect(() => { + this.provider.Client.find.restore(); + }) + .expect(this.failWith(403, 'insufficient_scope', 'access token missing openid scope', 'openid')); + }); + it('validates a client is still valid for a found token', async function () { const at = await new this.provider.AccessToken({ client: await this.provider.Client.find('client'), + scope: 'openid', }).save(); sinon.stub(this.provider.Client, 'find').callsFake(async () => undefined); return this.agent.get('/me') @@ -93,6 +107,7 @@ describe('userinfo /me', () => { it('validates an account still valid for a found token', async function () { const at = await new this.provider.AccessToken({ client: await this.provider.Client.find('client'), + scope: 'openid', accountId: 'notfound', }).save(); return this.agent.get('/me') @@ -119,6 +134,6 @@ describe('userinfo /me', () => { scope: 'openid profile', }) .auth(this.access_token, { type: 'bearer' }) - .expect(this.failWith(400, 'invalid_scope', 'access token missing requested scope', 'profile')); + .expect(this.failWith(403, 'insufficient_scope', 'access token missing requested scope', 'profile')); }); }); From e593a284f647f0ceace5851e3793f237f9b9347c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 12 Oct 2021 13:47:38 +0200 Subject: [PATCH 023/154] chore(release): 7.8.1 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e55c17d..87581a2a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.8.1](https://github.com/panva/node-oidc-provider/compare/v7.8.0...v7.8.1) (2021-10-12) + + +### Bug Fixes + +* use insufficient_scope instead of invalid_scope at userinfo_endpoint ([ba8a8f0](https://github.com/panva/node-oidc-provider/commit/ba8a8f0188c9a73a0ab0f8b974bea49feb2a87a6)) + ## [7.8.0](https://github.com/panva/node-oidc-provider/compare/v7.7.0...v7.8.0) (2021-09-15) diff --git a/package.json b/package.json index ab9efced3..7fa9cdda8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.8.0", + "version": "7.8.1", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From b867fb1481e009a3fee775f2c0fdb6a3043beeaa Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 14 Oct 2021 18:46:36 +0200 Subject: [PATCH 024/154] chore: upgrade jose to v4.x --- .../pushed_authorization_request_response.js | 2 +- lib/helpers/jwt.js | 16 ++++++++++------ lib/helpers/keystore.js | 4 ++-- lib/helpers/validate_dpop.js | 10 ++++++---- package.json | 2 +- test/client_auth/client_auth.test.js | 6 +++--- .../pushed_authorization_requests.test.js | 4 ++-- test/request/jwt_request.test.js | 14 +++++++------- test/request/uri_request.test.js | 8 ++++---- 9 files changed, 36 insertions(+), 30 deletions(-) diff --git a/lib/actions/authorization/pushed_authorization_request_response.js b/lib/actions/authorization/pushed_authorization_request_response.js index b2aaa0bea..4666d1806 100644 --- a/lib/actions/authorization/pushed_authorization_request_response.js +++ b/lib/actions/authorization/pushed_authorization_request_response.js @@ -1,4 +1,4 @@ -const { UnsecuredJWT } = require('jose/jwt/unsecured'); // eslint-disable-line import/no-unresolved +const { UnsecuredJWT } = require('jose'); const { PUSHED_REQUEST_URN } = require('../../consts'); const epochTime = require('../../helpers/epoch_time'); diff --git a/lib/helpers/jwt.js b/lib/helpers/jwt.js index a2094fdf1..4e0df7965 100644 --- a/lib/helpers/jwt.js +++ b/lib/helpers/jwt.js @@ -1,11 +1,15 @@ const { strict: assert } = require('assert'); -const { CompactEncrypt } = require('jose/jwe/compact/encrypt'); // eslint-disable-line import/no-unresolved -const { CompactSign } = require('jose/jws/compact/sign'); // eslint-disable-line import/no-unresolved -const { compactDecrypt } = require('jose/jwe/compact/decrypt'); // eslint-disable-line import/no-unresolved -const { compactVerify } = require('jose/jws/compact/verify'); // eslint-disable-line import/no-unresolved -const { decodeProtectedHeader } = require('jose/util/decode_protected_header'); // eslint-disable-line import/no-unresolved -const { JWEDecryptionFailed, JWKSNoMatchingKey, JWSSignatureVerificationFailed } = require('jose/util/errors'); // eslint-disable-line import/no-unresolved +const { + CompactEncrypt, + CompactSign, + compactDecrypt, + compactVerify, + decodeProtectedHeader, + errors, +} = require('jose'); + +const { JWEDecryptionFailed, JWKSNoMatchingKey, JWSSignatureVerificationFailed } = errors; const base64url = require('./base64url'); const epochTime = require('./epoch_time'); diff --git a/lib/helpers/keystore.js b/lib/helpers/keystore.js index d7b759723..5368769bb 100644 --- a/lib/helpers/keystore.js +++ b/lib/helpers/keystore.js @@ -1,5 +1,5 @@ /* eslint-disable no-plusplus, no-restricted-syntax */ -const { parseJwk } = require('jose/jwk/parse'); // eslint-disable-line import/no-unresolved +const { importJWK } = require('jose'); const keyscore = (key, { alg, use }) => { let score = 0; @@ -201,7 +201,7 @@ class KeyStore { return cached; } - const keyObject = await parseJwk({ ...jwk, alg }); + const keyObject = await importJWK({ ...jwk, alg }); this.#cached.set(jwk, keyObject); return keyObject; } diff --git a/lib/helpers/validate_dpop.js b/lib/helpers/validate_dpop.js index 0abfb8763..99c883106 100644 --- a/lib/helpers/validate_dpop.js +++ b/lib/helpers/validate_dpop.js @@ -1,8 +1,10 @@ const { createHash } = require('crypto'); -const { jwtVerify } = require('jose/jwt/verify'); // eslint-disable-line import/no-unresolved -const { EmbeddedJWK } = require('jose/jwk/embedded'); // eslint-disable-line import/no-unresolved -const { calculateThumbprint } = require('jose/jwk/thumbprint'); // eslint-disable-line import/no-unresolved +const { + jwtVerify, + EmbeddedJWK, + calculateJwkThumbprint, +} = require('jose'); const { InvalidDpopProof } = require('./errors'); const instance = require('./weak_cache'); @@ -60,7 +62,7 @@ module.exports = async (ctx, accessToken) => { } } - const thumbprint = await calculateThumbprint(jwk); + const thumbprint = await calculateJwkThumbprint(jwk); return { thumbprint, jti: payload.jti, iat: payload.iat }; } catch (err) { diff --git a/package.json b/package.json index 7fa9cdda8..347ebf1df 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "debug": "^4.3.2", "ejs": "^3.1.6", "got": "^11.8.2", - "jose": "^3.19.0", + "jose": "^4.0.0", "jsesc": "^3.0.2", "koa": "^2.13.3", "koa-compose": "^4.1.0", diff --git a/test/client_auth/client_auth.test.js b/test/client_auth/client_auth.test.js index de88631db..a4f044aa3 100644 --- a/test/client_auth/client_auth.test.js +++ b/test/client_auth/client_auth.test.js @@ -3,7 +3,7 @@ const { readFileSync } = require('fs'); const got = require('got'); const nock = require('nock'); const jose = require('jose2'); -const { parseJwk } = require('jose/jwk/parse'); // eslint-disable-line import/no-unresolved +const { importJWK } = require('jose'); const sinon = require('sinon'); const { expect } = require('chai'); const cloneDeep = require('lodash/cloneDeep'); @@ -525,7 +525,7 @@ describe('client authentication options', () => { describe('client_secret_jwt auth', () => { before(async function () { - this.key = await parseJwk((await this.provider.Client.find('client-jwt-secret')).symmetricKeyStore.selectForSign({ alg: 'HS256' })[0]); + this.key = await importJWK((await this.provider.Client.find('client-jwt-secret')).symmetricKeyStore.selectForSign({ alg: 'HS256' })[0]); }); it('accepts the auth', function () { @@ -1048,7 +1048,7 @@ describe('client authentication options', () => { }); it('rejects assertions when the secret is expired', async function () { - const key = await parseJwk((await this.provider.Client.find('secret-expired-jwt')).symmetricKeyStore.selectForSign({ alg: 'HS256' })[0]); + const key = await importJWK((await this.provider.Client.find('secret-expired-jwt')).symmetricKeyStore.selectForSign({ alg: 'HS256' })[0]); return JWT.sign({ jti: nanoid(), aud: this.provider.issuer + this.suitePath('/token'), diff --git a/test/pushed_authorization_requests/pushed_authorization_requests.test.js b/test/pushed_authorization_requests/pushed_authorization_requests.test.js index 90416eb76..54fa8c53e 100644 --- a/test/pushed_authorization_requests/pushed_authorization_requests.test.js +++ b/test/pushed_authorization_requests/pushed_authorization_requests.test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const sinon = require('sinon'); const jose = require('jose2'); -const { parseJwk } = require('jose/jwk/parse'); // eslint-disable-line import/no-unresolved +const { importJWK } = require('jose'); const JWT = require('../../lib/helpers/jwt'); const bootstrap = require('../test_helper'); @@ -11,7 +11,7 @@ describe('Pushed Request Object', () => { before(async function () { const client = await this.provider.Client.find('client'); - this.key = await parseJwk(client.symmetricKeyStore.selectForSign({ alg: 'HS256' })[0]); + this.key = await importJWK(client.symmetricKeyStore.selectForSign({ alg: 'HS256' })[0]); }); describe('discovery', () => { diff --git a/test/request/jwt_request.test.js b/test/request/jwt_request.test.js index 5bd99a918..cf379e745 100644 --- a/test/request/jwt_request.test.js +++ b/test/request/jwt_request.test.js @@ -1,7 +1,7 @@ const { createSecretKey, randomBytes } = require('crypto'); const { parse } = require('url'); -const { parseJwk } = require('jose/jwk/parse'); // eslint-disable-line import/no-unresolved +const { importJWK } = require('jose'); const sinon = require('sinon'); const { expect } = require('chai'); @@ -489,7 +489,7 @@ describe('request parameter features', () => { it('can accept Request Objects issued within acceptable system clock skew', async function () { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); i(this.provider).configuration().clockTolerance = 10; return JWT.sign({ iat: Math.ceil(Date.now() / 1000) + 5, @@ -514,7 +514,7 @@ describe('request parameter features', () => { it('works with signed by an actual DSA', async function () { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); return JWT.sign({ client_id: 'client-with-HS-sig', response_type: 'code', @@ -537,7 +537,7 @@ describe('request parameter features', () => { it('rejects HMAC based requests when signed with an expired secret', async function () { const client = await this.provider.Client.find('client-with-HS-sig-expired'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); const spy = sinon.spy(); this.provider.once(errorEvt, spy); @@ -571,7 +571,7 @@ describe('request parameter features', () => { it('supports optional replay prevention', async function () { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); const request = await JWT.sign({ response_type: 'code', @@ -941,7 +941,7 @@ describe('request parameter features', () => { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); return JWT.sign({ client_id: 'client', response_type: 'code', @@ -999,7 +999,7 @@ describe('request parameter features', () => { it('handles unrecognized parameters', async function () { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); return JWT.sign({ client_id: 'client-with-HS-sig', unrecognized: true, diff --git a/test/request/uri_request.test.js b/test/request/uri_request.test.js index a0f0f19a2..a35c39740 100644 --- a/test/request/uri_request.test.js +++ b/test/request/uri_request.test.js @@ -1,7 +1,7 @@ const { createSecretKey, randomBytes } = require('crypto'); const { parse } = require('url'); -const { parseJwk } = require('jose/jwk/parse'); // eslint-disable-line import/no-unresolved +const { importJWK } = require('jose'); const sinon = require('sinon').createSandbox(); const nock = require('nock'); const { expect } = require('chai'); @@ -68,7 +68,7 @@ describe('request Uri features', () => { it('works with signed by an actual alg (https)', async function () { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); const request = await JWT.sign({ client_id: 'client-with-HS-sig', response_type: 'code', @@ -97,7 +97,7 @@ describe('request Uri features', () => { it('works with signed by an actual alg (http)', async function () { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); const request = await JWT.sign({ client_id: 'client-with-HS-sig', response_type: 'code', @@ -601,7 +601,7 @@ describe('request Uri features', () => { const client = await this.provider.Client.find('client-with-HS-sig'); let [key] = client.symmetricKeyStore.selectForSign({ alg: 'HS256' }); - key = await parseJwk(key); + key = await importJWK(key); const request = await JWT.sign({ client_id: 'client', response_type: 'code', From 5b1435d93b6e4bc487c443c2bf99ba3a28cba43c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 18 Oct 2021 10:04:39 +0200 Subject: [PATCH 025/154] ci: bump conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8dec8b44..df6f18943 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.29 + VERSION: release-v4.1.30 steps: - name: Checkout uses: actions/checkout@master From 28ac600cedf8727106b695b00ae6fa1bc5ad6940 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 15 Oct 2021 23:18:55 +0200 Subject: [PATCH 026/154] chore: update auth0 sponsorship --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48ae62ff5..e9a3b568e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ conforms to the following profiles of the OpenID Connect™ protocol ## Sponsor -[auth0-logo][sponsor-auth0] If you want to quickly add OpenID Connect authentication to Node.js apps, feel free to check out Auth0's Node.js SDK and free plan at [auth0.com/developers][sponsor-auth0].

+[auth0-logo][sponsor-auth0] If you want to quickly add OpenID Connect authentication to Node.js apps, feel free to check out Auth0's Node.js SDK and free plan. [Create an Auth0 account; it's free!][sponsor-auth0]

## Support @@ -137,7 +137,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [jar]: https://www.rfc-editor.org/rfc/rfc9101.html [device-flow]: https://tools.ietf.org/html/rfc8628 [jwt-introspection]: https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response-10 -[sponsor-auth0]: https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=oidc-provider&utm_content=auth +[sponsor-auth0]: https://a0.to/try-auth0 [mtls]: https://tools.ietf.org/html/rfc8705 [dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03 [resource-indicators]: https://tools.ietf.org/html/rfc8707 From dedc47c64f29f120cc534b4dbc607821065738a7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Oct 2021 18:21:35 +0200 Subject: [PATCH 027/154] chore: update auth0 sponsorship --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9a3b568e..fa3bcedd8 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ conforms to the following profiles of the OpenID Connect™ protocol ## Sponsor -[auth0-logo][sponsor-auth0] If you want to quickly add OpenID Connect authentication to Node.js apps, feel free to check out Auth0's Node.js SDK and free plan. [Create an Auth0 account; it's free!][sponsor-auth0]

+[auth0-logo][sponsor-auth0] If you want to quickly add OpenID Connect authentication to Node.js apps, feel free to check out Auth0's Node.js SDK and free plan. [Create an Auth0 account; it's free!][sponsor-auth0]

## Support From 665d0d591d74faa21d36ea903eba7e5f98f12376 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 22 Oct 2021 21:46:27 +0200 Subject: [PATCH 028/154] ci: update download logs and job dependency --- .github/workflows/test.yml | 1 + certification/runner/api.js | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df6f18943..394c1e574 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -115,6 +115,7 @@ jobs: conformance-suite: runs-on: ubuntu-latest needs: + - test - build-conformance-suite env: SUITE_BASE_URL: https://localhost.emobix.co.uk:8443 diff --git a/certification/runner/api.js b/certification/runner/api.js index 6f9b910d7..0b3d8af4c 100644 --- a/certification/runner/api.js +++ b/certification/runner/api.js @@ -1,10 +1,14 @@ /* eslint-disable no-await-in-loop */ const { strict: assert } = require('assert'); const { createWriteStream } = require('fs'); +const stream = require('stream'); +const { promisify } = require('util'); const Got = require('got'); const ms = require('ms'); +const pipeline = promisify(stream.pipeline); + const debug = require('./debug'); const FINISHED = new Set(['FINISHED']); @@ -109,19 +113,14 @@ class API { async downloadArtifact({ planId } = {}) { assert(planId, 'argument property "planId" missing'); - await new Promise((resolve) => { - const download = this.stream(`api/plan/exporthtml/${planId}`, { + const filename = `export-${planId}.zip`; + return pipeline( + this.stream(`api/plan/exporthtml/${planId}`, { headers: { accept: 'application/zip' }, responseType: 'buffer', - }); - - const filename = `export-${planId}.zip`; - download.pipe(createWriteStream(filename)); - download.on('close', () => { - console.log(`Logs in ${filename}.`); // eslint-disable-line no-console - resolve(); - }); - }); + }), + createWriteStream(filename), + ); } async waitForState({ moduleId, timeout = ms('4m') } = {}) { From 178f678b705c3941a65e574cb2328b1d858b1843 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 22 Oct 2021 21:48:45 +0200 Subject: [PATCH 029/154] style: lint-fix --- certification/runner/api.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/certification/runner/api.js b/certification/runner/api.js index 0b3d8af4c..12a854095 100644 --- a/certification/runner/api.js +++ b/certification/runner/api.js @@ -31,7 +31,10 @@ class API { timeout: 10000, }); - const { stream } = Got.extend({ + this.get = get; + this.post = post; + + this.stream = Got.extend({ prefixUrl: baseUrl, throwHttpErrors: false, followRedirect: false, @@ -40,11 +43,7 @@ class API { 'content-type': 'application/json', }, retry: 0, - }); - - this.get = get; - this.stream = stream; - this.post = post; + }).stream; } async getAllTestModules() { From 19b4d0daa4ca1e05acd2b5651545251fe937ff39 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Oct 2021 18:58:02 +0200 Subject: [PATCH 030/154] feat: add LTS Gallium as a supported runtime version --- .github/workflows/test.yml | 4 +++- lib/provider.js | 3 ++- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 394c1e574..0de544695 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,12 +49,14 @@ jobs: - 12 - 14.15.0 - 14 + - 16.13.0 + - 16 os: - ubuntu-latest - windows-latest include: - experimental: true - node-version: '>=15' + node-version: '>=17' os: ubuntu-latest steps: diff --git a/lib/provider.js b/lib/provider.js index 182609472..27110f12a 100644 --- a/lib/provider.js +++ b/lib/provider.js @@ -9,8 +9,9 @@ const [major, minor] = process.version if ( !(major === 12 && minor >= 19) && !(major === 14 && minor >= 15) + && !(major === 16 && minor >= 13) ) { - attention.warn('Unsupported Node.js runtime version. Use ^12.19.0 or ^14.15.0'); + attention.warn('Unsupported Node.js runtime version. Use ^12.19.0, ^14.15.0, or ^16.13.0'); } const url = require('url'); diff --git a/package.json b/package.json index 347ebf1df..4be47db87 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,6 @@ "timekeeper": "^2.2.0" }, "engines": { - "node": "^12.19.0 || ^14.15.0" + "node": "^12.19.0 || ^14.15.0 || ^16.13.0" } } From 88e961f3b347bfd2f35c2add587a0d2370df38ad Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Oct 2021 19:00:57 +0200 Subject: [PATCH 031/154] ci: update conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0de544695..31d6f350c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.30 + VERSION: release-v4.1.35 steps: - name: Checkout uses: actions/checkout@master From 807fbebe04bd6e1beece99ced4a3857b686397f6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Oct 2021 19:02:40 +0200 Subject: [PATCH 032/154] chore(release): 7.9.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87581a2a0..2202323af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.9.0](https://github.com/panva/node-oidc-provider/compare/v7.8.1...v7.9.0) (2021-10-26) + + +### Features + +* add LTS Gallium as a supported runtime version ([19b4d0d](https://github.com/panva/node-oidc-provider/commit/19b4d0daa4ca1e05acd2b5651545251fe937ff39)) + ## [7.8.1](https://github.com/panva/node-oidc-provider/compare/v7.8.0...v7.8.1) (2021-10-12) diff --git a/package.json b/package.json index 4be47db87..dd426dcbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.8.1", + "version": "7.9.0", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 08e87812cee2bed816646663b36afc723fa76df7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 1 Nov 2021 17:18:28 +0100 Subject: [PATCH 033/154] test: fix fastify in node >=17 --- test/run.js | 2 +- test/test_helper.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/run.js b/test/run.js index 2f0d02216..0d8ae32ca 100644 --- a/test/run.js +++ b/test/run.js @@ -35,7 +35,7 @@ async function run() { const { MOUNT_VIA: via, MOUNT_TO: to } = process.env; await new Promise((resolve) => { - global.server = createServer().listen(0); + global.server = createServer().listen(0, '::'); global.server.once('listening', resolve); }); await new Promise((resolve, reject) => { diff --git a/test/test_helper.js b/test/test_helper.js index 42a7470f2..9bc1739fb 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -452,11 +452,11 @@ module.exports = function testHelper(dir, { await app.register(middie); app.use(mountTo, provider.callback()); await new Promise((resolve) => global.server.close(resolve)); - await app.listen(port); + await app.listen(port, '::'); global.server = app.server; afterPromises.push(async () => { await app.close(); - global.server = createServer().listen(port); + global.server = createServer().listen(port, '::'); await new Promise((resolve) => global.server.once('listening', resolve)); }); break; @@ -489,7 +489,7 @@ module.exports = function testHelper(dir, { global.server = app.listener; afterPromises.push(async () => { await app.stop(); - global.server = createServer().listen(port); + global.server = createServer().listen(port, '::'); await new Promise((resolve) => global.server.once('listening', resolve)); }); break; From 6c7d20f2bbdb73755c17ea1bfa2c984a2580c14c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 1 Nov 2021 18:54:06 +0100 Subject: [PATCH 034/154] chore: bump jose --- lib/models/client.js | 10 +--------- package.json | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/models/client.js b/lib/models/client.js index 2df1c0353..e46590d1a 100644 --- a/lib/models/client.js +++ b/lib/models/client.js @@ -337,16 +337,8 @@ module.exports = function getClient(provider) { // eslint-disable-next-line no-restricted-syntax for (const alg of algs) { if (alg.startsWith('HS')) { - // eslint-disable-next-line no-bitwise - const length = parseInt(alg.substr(-3), 10) >> 3; - let secret = Buffer.from(client.clientSecret); - if (secret.byteLength < length) { - const padded = Buffer.alloc(length); - padded.set(secret); - secret = padded; - } client.symmetricKeyStore.add({ - alg, use: 'sig', kty: 'oct', k: base64url.encodeBuffer(secret), + alg, use: 'sig', kty: 'oct', k: base64url.encode(client.clientSecret), }); } else if (/^A(\d{3})(?:GCM)?KW$/.test(alg)) { const len = parseInt(RegExp.$1, 10) / 8; diff --git a/package.json b/package.json index dd426dcbd..64c995ed3 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "debug": "^4.3.2", "ejs": "^3.1.6", "got": "^11.8.2", - "jose": "^4.0.0", + "jose": "^4.1.4", "jsesc": "^3.0.2", "koa": "^2.13.3", "koa-compose": "^4.1.0", From b26ea4465b3e45b8e63e69bd08c5de525494dea8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 3 Nov 2021 13:05:11 +0100 Subject: [PATCH 035/154] feat: duplicate iss and aud as JWE Header Parameters --- lib/models/id_token.js | 2 ++ test/encryption/encryption.test.js | 29 +++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/models/id_token.js b/lib/models/id_token.js index a3abe9157..81b3df179 100644 --- a/lib/models/id_token.js +++ b/lib/models/id_token.js @@ -210,6 +210,8 @@ module.exports = function getIdToken(provider) { fields: { cty: alg ? 'JWT' : 'json', // if there's no signing alg the cty is json, else jwt kid, + iss: signOptions.issuer, + aud: signOptions.audience, }, }); } diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index 2492a1fc6..7b5c9590d 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -1,9 +1,9 @@ const url = require('url'); const { expect } = require('chai'); -const base64url = require('base64url'); const sinon = require('sinon'); const jose = require('jose2'); +const { decodeProtectedHeader } = require('jose'); const bootstrap = require('../test_helper'); const JWT = require('../../lib/helpers/jwt'); @@ -89,6 +89,12 @@ describe('encryption', () => { expect(JWT.decode(result)).to.be.ok; }); + it('duplicates iss and aud as JWE Header Parameters in an encrypted ID Token', function () { + const header = decodeProtectedHeader(this.id_token); + expect(header).to.have.property('iss').eql(this.provider.issuer); + expect(header).to.have.property('aud').eql('client'); + }); + it('responds with an encrypted userinfo JWT', function (done) { this.agent.get('/me') .auth(this.access_token, { type: 'bearer' }) @@ -99,6 +105,11 @@ describe('encryption', () => { }) .end((err, response) => { if (err) throw err; + + const header = decodeProtectedHeader(response.text); + expect(header).to.have.property('iss').eql(this.provider.issuer); + expect(header).to.have.property('aud').eql('client'); + const result = jose.JWE.decrypt(response.text, this.keystore); expect(result).to.be.ok; expect(JSON.parse(result)).to.have.keys('sub'); @@ -127,6 +138,11 @@ describe('encryption', () => { }) .end((err, response) => { if (err) throw err; + + const header = decodeProtectedHeader(response.text); + expect(header).to.have.property('iss').eql(this.provider.issuer); + expect(header).to.have.property('aud').eql('client'); + const result = jose.JWE.decrypt(response.text, this.keystore); expect(result).to.be.ok; expect(result.toString().split('.')).to.have.lengthOf(3); @@ -606,7 +622,10 @@ describe('encryption', () => { it('responds encrypted with i.e. PBES2 password derived key id_token', function () { expect(this.id_token).to.be.ok; expect(this.id_token.split('.')).to.have.lengthOf(5); - expect(JSON.parse(base64url.decode(this.id_token.split('.')[0]))).to.have.property('alg', 'PBES2-HS384+A192KW'); + const header = decodeProtectedHeader(this.id_token); + expect(header).to.have.property('alg', 'PBES2-HS384+A192KW'); + expect(header).to.have.property('iss').eql(this.provider.issuer); + expect(header).to.have.property('aud').eql('clientSymmetric'); }); }); @@ -693,12 +712,14 @@ describe('encryption', () => { }); }); - it('responds encrypted with i.e. PBES2 password derived key id_token', function () { + it('responds encrypted', function () { expect(this.id_token).to.be.ok; expect(this.id_token.split('.')).to.have.lengthOf(5); - const header = JSON.parse(base64url.decode(this.id_token.split('.')[0])); + const header = decodeProtectedHeader(this.id_token); expect(header).to.have.property('alg', 'dir'); expect(header).to.have.property('enc', 'A128CBC-HS256'); + expect(header).to.have.property('iss').eql(this.provider.issuer); + expect(header).to.have.property('aud').eql('clientSymmetric-dir'); }); }); }); From a0e9e52c6a04f2a38389387d8035bb498ab462e9 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 3 Nov 2021 13:37:14 +0100 Subject: [PATCH 036/154] docs: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa3bcedd8..b4f26a4ab 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03 [resource-indicators]: https://tools.ietf.org/html/rfc8707 [jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html -[jwt-at]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-11 +[jwt-at]: https://www.rfc-editor.org/rfc/rfc9068.html [paseto-at]: https://paseto.io [support-sponsor]: https://github.com/sponsors/panva [par]: https://www.rfc-editor.org/rfc/rfc9126.html From b9dcf824f9b06c3d856749b3a13eb88b490f1b52 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 4 Nov 2021 10:33:53 +0100 Subject: [PATCH 037/154] chore(release): 7.10.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2202323af..782374db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.10.0](https://github.com/panva/node-oidc-provider/compare/v7.9.0...v7.10.0) (2021-11-04) + + +### Features + +* duplicate iss and aud as JWE Header Parameters ([b26ea44](https://github.com/panva/node-oidc-provider/commit/b26ea4465b3e45b8e63e69bd08c5de525494dea8)) + ## [7.9.0](https://github.com/panva/node-oidc-provider/compare/v7.8.1...v7.9.0) (2021-10-26) diff --git a/package.json b/package.json index 64c995ed3..67a7c4a76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.9.0", + "version": "7.10.0", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 72d6ca639340c9a9868bf07b65d80f029c52d1bb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 9 Nov 2021 11:47:02 +0100 Subject: [PATCH 038/154] ci: update conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31d6f350c..e18280082 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.35 + VERSION: release-v4.1.37 steps: - name: Checkout uses: actions/checkout@master From 49eed4c20b28ef95e7a1a6315783dd3956b8c84a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 15 Nov 2021 17:34:55 +0100 Subject: [PATCH 039/154] fix: clearly mark that multiple pop mechanisms are not allowed --- lib/models/mixins/is_sender_constrained.js | 7 +++++++ .../certificate_bound_access_tokens.test.js | 2 ++ test/dpop/dpop.test.js | 2 ++ 3 files changed, 11 insertions(+) diff --git a/lib/models/mixins/is_sender_constrained.js b/lib/models/mixins/is_sender_constrained.js index 12275ce8e..2424d4b31 100644 --- a/lib/models/mixins/is_sender_constrained.js +++ b/lib/models/mixins/is_sender_constrained.js @@ -1,6 +1,7 @@ const x5t = 'x5t#S256'; const jkt = 'jkt'; +const { InvalidRequest } = require('../../helpers/errors'); const { [x5t]: thumbprint } = require('../../helpers/calculate_thumbprint'); module.exports = (superclass) => class extends superclass { @@ -15,9 +16,15 @@ module.exports = (superclass) => class extends superclass { setThumbprint(prop, input) { switch (prop) { case 'x5t': + if (this[jkt]) { + throw new InvalidRequest('multiple proof-of-posession mechanisms are not allowed'); + } this[x5t] = thumbprint(input); break; case 'jkt': + if (this[x5t]) { + throw new InvalidRequest('multiple proof-of-posession mechanisms are not allowed'); + } this[jkt] = input; break; default: diff --git a/test/certificate_bound_access_tokens/certificate_bound_access_tokens.test.js b/test/certificate_bound_access_tokens/certificate_bound_access_tokens.test.js index 00a7620f3..533f914a5 100644 --- a/test/certificate_bound_access_tokens/certificate_bound_access_tokens.test.js +++ b/test/certificate_bound_access_tokens/certificate_bound_access_tokens.test.js @@ -34,6 +34,8 @@ describe('features.mTLS.certificateBoundAccessTokens', () => { }); at.setThumbprint('x5t', crt); + expect(() => at.setThumbprint('jkt', 'foo')).to.throw().with.property('error_description', 'multiple proof-of-posession mechanisms are not allowed'); + const bearer = await at.save(); await this.agent.get('/me') diff --git a/test/dpop/dpop.test.js b/test/dpop/dpop.test.js index 735d749d3..04ddd1e3d 100644 --- a/test/dpop/dpop.test.js +++ b/test/dpop/dpop.test.js @@ -64,6 +64,8 @@ describe('features.dPoP', () => { }); at.setThumbprint('jkt', this.jwk.thumbprint); + expect(() => at.setThumbprint('x5t', 'foo')).to.throw().with.property('error_description', 'multiple proof-of-posession mechanisms are not allowed'); + const dpop = await at.save(); await this.agent.get('/me') From 735954f1de351291d920495209ffe62ee886bffd Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 15 Nov 2021 17:36:17 +0100 Subject: [PATCH 040/154] chore: not everything is a Bug Fix per se, something is just a Fix --- .versionrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.versionrc.json b/.versionrc.json index 5ed66449e..b5007d2e7 100644 --- a/.versionrc.json +++ b/.versionrc.json @@ -9,7 +9,7 @@ }, { "type": "fix", - "section": "Bug Fixes" + "section": "Fixes" }, { "type": "chore", From 773b167b8abc8877ec25d43f4a8b0ef5dfd74ef6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 16 Nov 2021 11:00:39 +0100 Subject: [PATCH 041/154] chore(release): 7.10.1 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 782374db7..272fe26cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.10.1](https://github.com/panva/node-oidc-provider/compare/v7.10.0...v7.10.1) (2021-11-16) + + +### Fixes + +* clearly mark that multiple pop mechanisms are not allowed ([49eed4c](https://github.com/panva/node-oidc-provider/commit/49eed4c20b28ef95e7a1a6315783dd3956b8c84a)) + ## [7.10.0](https://github.com/panva/node-oidc-provider/compare/v7.9.0...v7.10.0) (2021-11-04) diff --git a/package.json b/package.json index 67a7c4a76..befbb0ec3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.10.0", + "version": "7.10.1", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 8d7116bd0e8d369e6133ee1b9004e706167335f5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 21 Nov 2021 20:53:33 +0100 Subject: [PATCH 042/154] ci: update conformance suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e18280082..4f261263f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: build-conformance-suite: runs-on: ubuntu-latest env: - VERSION: release-v4.1.37 + VERSION: release-v4.1.38 steps: - name: Checkout uses: actions/checkout@master From 02c821d7f16c6421d30ffc449366d4d79d951830 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 28 Nov 2021 17:36:32 +0100 Subject: [PATCH 043/154] fix: use paseto configuration from `getResourceServerInfo` (#1150) --- lib/helpers/resource_server.js | 1 + test/formats/paseto.test.js | 77 +++++++++++++++++----------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/helpers/resource_server.js b/lib/helpers/resource_server.js index 1ba024460..00550390e 100644 --- a/lib/helpers/resource_server.js +++ b/lib/helpers/resource_server.js @@ -8,6 +8,7 @@ module.exports = class ResourceServer { this.accessTokenTTL = data.accessTokenTTL; this.accessTokenFormat = data.accessTokenFormat; this.jwt = data.jwt; + this.paseto = data.paseto; } get scopes() { diff --git a/test/formats/paseto.test.js b/test/formats/paseto.test.js index 78c720fd9..170e0809d 100644 --- a/test/formats/paseto.test.js +++ b/test/formats/paseto.test.js @@ -17,6 +17,7 @@ if (above16) { paseto = require('paseto2'); } +const ResourceServer = require('../../lib/helpers/resource_server'); const epochTime = require('../../lib/helpers/epoch_time'); const bootstrap = require('../test_helper'); @@ -54,14 +55,14 @@ describe('paseto format', () => { const iiat = epochTime(); const rotations = 1; const extra = { foo: 'bar' }; - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 1, purpose: 'public', }, - }; + }); /* eslint-disable object-property-newline */ const fullPayload = { @@ -76,14 +77,14 @@ describe('paseto format', () => { describe('Resource Server Configuration', () => { it('v1.public', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 1, purpose: 'public', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -91,14 +92,14 @@ describe('paseto format', () => { }); it('v2.public', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 2, purpose: 'public', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -107,14 +108,14 @@ describe('paseto format', () => { if (above16) { it('v3.public', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 3, purpose: 'public', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -124,14 +125,14 @@ describe('paseto format', () => { if (above16) { it('v4.public', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 4, purpose: 'public', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -140,7 +141,7 @@ describe('paseto format', () => { } it('v1.local', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -148,7 +149,7 @@ describe('paseto format', () => { purpose: 'local', key: crypto.randomBytes(32), }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -156,7 +157,7 @@ describe('paseto format', () => { }); it('v1.local (keyObject)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -164,7 +165,7 @@ describe('paseto format', () => { purpose: 'local', key: crypto.createSecretKey(crypto.randomBytes(32)), }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -173,7 +174,7 @@ describe('paseto format', () => { if (above16) { it('v3.local', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -181,7 +182,7 @@ describe('paseto format', () => { purpose: 'local', key: crypto.randomBytes(32), }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -189,7 +190,7 @@ describe('paseto format', () => { }); it('v3.local (keyObject)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -197,7 +198,7 @@ describe('paseto format', () => { purpose: 'local', key: crypto.createSecretKey(crypto.randomBytes(32)), }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -206,7 +207,7 @@ describe('paseto format', () => { } it('public kid selection failing', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -214,7 +215,7 @@ describe('paseto format', () => { version: 1, purpose: 'public', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -226,7 +227,7 @@ describe('paseto format', () => { }); it('kid must be a string', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -235,7 +236,7 @@ describe('paseto format', () => { purpose: 'local', key: crypto.randomBytes(32), }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -247,14 +248,14 @@ describe('paseto format', () => { }); it('unsupported PASETO version and purpose', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 2, purpose: 'local', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -266,14 +267,14 @@ describe('paseto format', () => { }); it('local paseto requires a key', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 1, purpose: 'local', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -285,7 +286,7 @@ describe('paseto format', () => { }); it('local paseto requires a key 32 bytes', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -293,7 +294,7 @@ describe('paseto format', () => { purpose: 'local', key: crypto.randomBytes(16), }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -305,7 +306,7 @@ describe('paseto format', () => { }); it('local paseto requires a secret key (private provided)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -313,7 +314,7 @@ describe('paseto format', () => { purpose: 'local', key: await (await generateKeyPair('ed25519')).privateKey, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -325,7 +326,7 @@ describe('paseto format', () => { }); it('local paseto requires a secret key (public provided)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { @@ -333,7 +334,7 @@ describe('paseto format', () => { purpose: 'local', key: await (await generateKeyPair('ed25519')).publicKey, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -345,10 +346,10 @@ describe('paseto format', () => { }); it('missing paseto configuration', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -361,14 +362,14 @@ describe('paseto format', () => { if (!above16) { it('only >= 16.0.0 node supports v3 and v4', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: { version: 3, purpose: 'public', }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -381,11 +382,11 @@ describe('paseto format', () => { } it('invalid paseto configuration type', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'paseto', audience: 'foo', paseto: null, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); From 5716f6ee1d772a2e63d98dd24220738347140f70 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 28 Nov 2021 21:15:24 +0100 Subject: [PATCH 044/154] test: use ResourceServer instances in jwt access token tests --- test/formats/jwt.test.js | 89 ++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/test/formats/jwt.test.js b/test/formats/jwt.test.js index 1cc46b146..f9431b206 100644 --- a/test/formats/jwt.test.js +++ b/test/formats/jwt.test.js @@ -8,6 +8,7 @@ const sinon = require('sinon').createSandbox(); const { expect } = require('chai'); const base64url = require('base64url'); +const ResourceServer = require('../../lib/helpers/resource_server'); const epochTime = require('../../lib/helpers/epoch_time'); const bootstrap = require('../test_helper'); @@ -48,10 +49,10 @@ describe('jwt format', () => { const iiat = epochTime(); const rotations = 1; const extra = { foo: 'bar' }; - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', - }; + }); /* eslint-disable object-property-newline */ const fullPayload = { @@ -66,13 +67,13 @@ describe('jwt format', () => { describe('Resource Server Configuration', () => { it('can be used to specify the signing algorithm', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { sign: { alg: 'PS256' }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -83,10 +84,10 @@ describe('jwt format', () => { }); it('uses the default idtokensigningalg by default (no jwt)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -98,11 +99,11 @@ describe('jwt format', () => { }); it('uses the default idtokensigningalg by default (jwt)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: {}, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -114,13 +115,13 @@ describe('jwt format', () => { }); it('can be used to specify the signing algorithm to be HMAC (buffer)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { sign: { alg: 'HS256', key: crypto.randomBytes(32) }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -132,13 +133,13 @@ describe('jwt format', () => { }); it('kid must be a string (sign)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { sign: { alg: 'HS256', key: crypto.randomBytes(32), kid: 200 }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -150,7 +151,7 @@ describe('jwt format', () => { }); it('kid must be a string (encrypt)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -161,7 +162,7 @@ describe('jwt format', () => { kid: 200, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -173,13 +174,13 @@ describe('jwt format', () => { }); it('can be used to specify the signing algorithm to be HMAC (buffer w/ kid)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { sign: { alg: 'HS256', key: crypto.randomBytes(32), kid: 'feb-2020' }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -190,13 +191,13 @@ describe('jwt format', () => { }); it('can be used to specify the signing algorithm to be HMAC (KeyObject)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { sign: { alg: 'HS256', key: crypto.createSecretKey(crypto.randomBytes(32)) }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -208,7 +209,7 @@ describe('jwt format', () => { }); it('can be an encrypted JWT', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -219,7 +220,7 @@ describe('jwt format', () => { key: crypto.randomBytes(16), }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -235,7 +236,7 @@ describe('jwt format', () => { }); it('can be an encrypted JWT w/ kid', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -247,7 +248,7 @@ describe('jwt format', () => { kid: 'feb-2020', }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -258,7 +259,7 @@ describe('jwt format', () => { }); it('can be a nested JWT (explicit)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -271,7 +272,7 @@ describe('jwt format', () => { key: (await generateKeyPair('ec', { namedCurve: 'P-256' })).publicKey, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -287,7 +288,7 @@ describe('jwt format', () => { }); it('can be a nested JWT w/ kid', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -301,7 +302,7 @@ describe('jwt format', () => { kid: 'feb-2020', }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -312,7 +313,7 @@ describe('jwt format', () => { }); it('can be a nested JWT (implicit signing alg)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -323,7 +324,7 @@ describe('jwt format', () => { key: (await generateKeyPair('ec', { namedCurve: 'P-256' })).publicKey, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -338,7 +339,7 @@ describe('jwt format', () => { }); it('ensures "none" JWS algorithm cannot be used', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -346,7 +347,7 @@ describe('jwt format', () => { alg: 'none', }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -358,7 +359,7 @@ describe('jwt format', () => { }); it('ensures HMAC JWS algorithms get a key', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -366,7 +367,7 @@ describe('jwt format', () => { alg: 'HS256', }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -378,7 +379,7 @@ describe('jwt format', () => { }); it('ensures HMAC JWS algorithms get a secret key (1/2)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -387,7 +388,7 @@ describe('jwt format', () => { key: (await generateKeyPair('ec', { namedCurve: 'P-256' })).publicKey, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -399,7 +400,7 @@ describe('jwt format', () => { }); it('ensures HMAC JWS algorithms get a secret key (1/2)', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -408,7 +409,7 @@ describe('jwt format', () => { key: (await generateKeyPair('ec', { namedCurve: 'P-256' })).privateKey, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -420,7 +421,7 @@ describe('jwt format', () => { }); it('ensures Asymmetric JWS algorithms have a key in the provider keystore', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -428,7 +429,7 @@ describe('jwt format', () => { alg: 'ES512', }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -440,7 +441,7 @@ describe('jwt format', () => { }); it('ensures JWE key is public or secret', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -450,7 +451,7 @@ describe('jwt format', () => { key: (await generateKeyPair('ec', { namedCurve: 'P-256' })).privateKey, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -462,7 +463,7 @@ describe('jwt format', () => { }); it('ensures Nested JWT when JWE encryption is a public one', async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -472,7 +473,7 @@ describe('jwt format', () => { key: (await generateKeyPair('ec', { namedCurve: 'P-256' })).publicKey, }, }, - }; + }); const client = await this.provider.Client.find(clientId); const token = new this.provider.AccessToken({ client, ...fullPayload, resourceServer }); @@ -487,7 +488,7 @@ describe('jwt format', () => { for (const prop of ['alg', 'enc', 'key']) { // eslint-disable-next-line no-loop-func it(`ensures JWE Configuration has ${prop}`, async function () { - const resourceServer = { + const resourceServer = new ResourceServer(resource, { accessTokenFormat: 'jwt', audience: 'foo', jwt: { @@ -497,7 +498,7 @@ describe('jwt format', () => { key: crypto.randomBytes(16), }, }, - }; + }); delete resourceServer.jwt.encrypt[prop]; From a2a2c1fa96c15a663928ffce7624346d6b701ca5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 28 Nov 2021 21:15:42 +0100 Subject: [PATCH 045/154] chore(release): 7.10.2 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 272fe26cf..20f9eab89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.10.2](https://github.com/panva/node-oidc-provider/compare/v7.10.1...v7.10.2) (2021-11-28) + + +### Fixes + +* use paseto configuration from `getResourceServerInfo` ([#1150](https://github.com/panva/node-oidc-provider/issues/1150)) ([02c821d](https://github.com/panva/node-oidc-provider/commit/02c821d7f16c6421d30ffc449366d4d79d951830)) + ## [7.10.1](https://github.com/panva/node-oidc-provider/compare/v7.10.0...v7.10.1) (2021-11-16) diff --git a/package.json b/package.json index befbb0ec3..85f62e0cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.10.1", + "version": "7.10.2", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 55e85272c80e107f8fadec7bd0d327ca426a738c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 30 Nov 2021 14:42:20 +0100 Subject: [PATCH 046/154] docs: note unsupported paseto version+purpose combinations --- docs/README.md | 1 + lib/helpers/defaults.js | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/README.md b/docs/README.md index 0bfa1b608..9640bca35 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1813,6 +1813,7 @@ and a JWT Access Token Format. } } // PASETO Access Token Format (when accessTokenFormat is 'paseto') + // Note: v2.public and v4.public are NOT supported paseto?: { version: 1 | 2 | 3 | 4, purpose: 'local' | 'public', diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 98ff96ff3..448ae4ac2 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1712,6 +1712,7 @@ function getDefaults() { * } * * // PASETO Access Token Format (when accessTokenFormat is 'paseto') + * // Note: v2.public and v4.public are NOT supported * paseto?: { * version: 1 | 2 | 3 | 4, * purpose: 'local' | 'public', From 008072c2ea4614968838bbdf598e88ede399aade Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 1 Dec 2021 10:15:05 +0100 Subject: [PATCH 047/154] docs: note unsupported paseto version+purpose combinations --- docs/README.md | 2 +- lib/helpers/defaults.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 9640bca35..3fcbce6c7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1813,7 +1813,7 @@ and a JWT Access Token Format. } } // PASETO Access Token Format (when accessTokenFormat is 'paseto') - // Note: v2.public and v4.public are NOT supported + // Note: v2.local and v4.local are NOT supported paseto?: { version: 1 | 2 | 3 | 4, purpose: 'local' | 'public', diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 448ae4ac2..76f45b214 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1712,7 +1712,7 @@ function getDefaults() { * } * * // PASETO Access Token Format (when accessTokenFormat is 'paseto') - * // Note: v2.public and v4.public are NOT supported + * // Note: v2.local and v4.local are NOT supported * paseto?: { * version: 1 | 2 | 3 | 4, * purpose: 'local' | 'public', From 2628d7e4b81d22a3972e8f82c94b9ec4dd9835d4 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 3 Dec 2021 14:30:46 +0100 Subject: [PATCH 048/154] fix: expose invalid_dpop_proof error code and set it to 401 on userinfo --- lib/actions/userinfo.js | 5 ++- lib/helpers/validate_dpop.js | 11 ++++-- test/dpop/dpop.test.js | 76 +++++++++++++++++++++--------------- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/lib/actions/userinfo.js b/lib/actions/userinfo.js index 37eb94806..d459196fa 100644 --- a/lib/actions/userinfo.js +++ b/lib/actions/userinfo.js @@ -1,4 +1,4 @@ -const { InvalidDpopProof, InvalidToken, InsufficientScope } = require('../helpers/errors'); +const { InvalidToken, InsufficientScope, InvalidDpopProof } = require('../helpers/errors'); const difference = require('../helpers/_/difference'); const setWWWAuthenticate = require('../helpers/set_www_authenticate'); const bodyParser = require('../shared/conditional_body'); @@ -43,7 +43,8 @@ module.exports = [ } if (err instanceof InvalidDpopProof) { - err.error = err.message = 'invalid_token'; // eslint-disable-line no-multi-assign + // eslint-disable-next-line no-multi-assign + err.status = err.statusCode = 401; } setWWWAuthenticate(ctx, scheme, { diff --git a/lib/helpers/validate_dpop.js b/lib/helpers/validate_dpop.js index 99c883106..959a17d5a 100644 --- a/lib/helpers/validate_dpop.js +++ b/lib/helpers/validate_dpop.js @@ -44,21 +44,21 @@ module.exports = async (ctx, accessToken) => { ); if (typeof payload.jti !== 'string' || !payload.jti) { - throw new Error('must have a jti string property'); + throw new InvalidDpopProof('DPoP Proof must have a jti string property'); } if (payload.htm !== ctx.method) { - throw new Error('htm mismatch'); + throw new InvalidDpopProof('DPoP Proof htm mismatch'); } if (payload.htu !== ctx.oidc.urlFor(ctx.oidc.route)) { - throw new Error('htu mismatch'); + throw new InvalidDpopProof('DPoP Proof htu mismatch'); } if (accessToken) { const ath = base64url.encode(createHash('sha256').update(accessToken).digest()); if (payload.ath !== ath) { - throw new Error('ath mismatch'); + throw new InvalidDpopProof('DPoP Proof ath mismatch'); } } @@ -66,6 +66,9 @@ module.exports = async (ctx, accessToken) => { return { thumbprint, jti: payload.jti, iat: payload.iat }; } catch (err) { + if (err instanceof InvalidDpopProof) { + throw err; + } throw new InvalidDpopProof('invalid DPoP key binding', err.message); } }; diff --git a/test/dpop/dpop.test.js b/test/dpop/dpop.test.js index 04ddd1e3d..d39892a1b 100644 --- a/test/dpop/dpop.test.js +++ b/test/dpop/dpop.test.js @@ -118,10 +118,10 @@ describe('features.dPoP', () => { await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { jwk: key, typ: value } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); } @@ -129,10 +129,10 @@ describe('features.dPoP', () => { await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', `${base64url.encode(JSON.stringify({ jwk: key, typ: 'dpop+jwt', alg: value }))}.e30.`) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); } @@ -140,56 +140,56 @@ describe('features.dPoP', () => { await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: value } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); } await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key.toJWK(true) } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: await JWK.generate('oct') } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({ htm: 'POST', htu: `${this.provider.issuer}${this.suitePath('/me')}` }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof must have a jti string property' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({ jti: 'foo', htm: 'POST' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof htm mismatch' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({ jti: 'foo', htm: 'GET', htu: 'foo' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof htu mismatch' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop @@ -197,10 +197,10 @@ describe('features.dPoP', () => { jti: 'foo', htm: 'GET', htu: `${this.provider.issuer}${this.suitePath('/me')}`, iat: epochTime() - 61, }, key, { kid: false, iat: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop @@ -208,10 +208,10 @@ describe('features.dPoP', () => { jti: 'foo', htm: 'GET', htu: `${this.provider.issuer}${this.suitePath('/me')}`, }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: await JWK.generate('EC') } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); }); @@ -258,8 +258,8 @@ describe('features.dPoP', () => { await this.agent.get('/me') .set('Authorization', `DPoP ${dpop}`) .set('DPoP', this.proof(`${this.provider.issuer}${this.suitePath('/me')}`, 'GET', 'anotherAccessTokenValue')) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) - .expect(400); + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof ath mismatch' }) + .expect(401); expect(spy).to.have.property('calledOnce', true); expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt verification'); @@ -626,4 +626,16 @@ describe('features.dPoP', () => { expect(ClientCredentials).to.have.property('jkt', expectedS256); }); }); + + describe('status codes at the token endpoint', () => { + it('should be 400 for invalid_dpop_proof', async function () { + return this.agent.post('/token') + .auth('client', 'secret') + .send({ grant_type: 'client_credentials' }) + .set('DPoP', 'invalid') + .type('form') + .expect(400) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }); + }); + }); }); From d871316d06d50ae11602da88ec21030de6a41022 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 4 Dec 2021 09:31:42 +0100 Subject: [PATCH 049/154] chore(release): 7.10.3 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f9eab89..61d605aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.10.3](https://github.com/panva/node-oidc-provider/compare/v7.10.2...v7.10.3) (2021-12-04) + + +### Fixes + +* expose invalid_dpop_proof error code and set it to 401 on userinfo ([2628d7e](https://github.com/panva/node-oidc-provider/commit/2628d7e4b81d22a3972e8f82c94b9ec4dd9835d4)) + ## [7.10.2](https://github.com/panva/node-oidc-provider/compare/v7.10.1...v7.10.2) (2021-11-28) diff --git a/package.json b/package.json index 85f62e0cd..60a67c579 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.10.2", + "version": "7.10.3", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 2edbe335981086cbbec1400d15df8ff4b3bcfd8a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 4 Dec 2021 20:12:57 +0100 Subject: [PATCH 050/154] test: avoid max event listener warning --- test/core/hybrid/code+id_token+token.authorization.test.js | 3 +++ test/core/hybrid/code+token.authorization.test.js | 3 +++ test/core/implicit/id_token+token.authorization.test.js | 3 +++ test/formats/jwt.test.js | 4 ++++ test/formats/paseto.test.js | 5 +++++ test/refresh/refresh.grant.test.js | 3 +++ test/resource_indicators/resource_indicators.test.js | 3 +++ 7 files changed, 24 insertions(+) diff --git a/test/core/hybrid/code+id_token+token.authorization.test.js b/test/core/hybrid/code+id_token+token.authorization.test.js index 759908778..753ba0d30 100644 --- a/test/core/hybrid/code+id_token+token.authorization.test.js +++ b/test/core/hybrid/code+id_token+token.authorization.test.js @@ -9,6 +9,9 @@ const scope = 'openid'; describe('HYBRID code+id_token+token', () => { before(bootstrap(__dirname)); + afterEach(function () { + this.provider.removeAllListeners(); + }); ['get', 'post'].forEach((verb) => { describe(`${verb} ${route} with session`, () => { diff --git a/test/core/hybrid/code+token.authorization.test.js b/test/core/hybrid/code+token.authorization.test.js index 768fae9bf..4b0cd8125 100644 --- a/test/core/hybrid/code+token.authorization.test.js +++ b/test/core/hybrid/code+token.authorization.test.js @@ -9,6 +9,9 @@ const scope = 'openid'; describe('HYBRID code+token', () => { before(bootstrap(__dirname)); + afterEach(function () { + this.provider.removeAllListeners(); + }); ['get', 'post'].forEach((verb) => { describe(`${verb} ${route} with session`, () => { diff --git a/test/core/implicit/id_token+token.authorization.test.js b/test/core/implicit/id_token+token.authorization.test.js index b26010096..90e524aa7 100644 --- a/test/core/implicit/id_token+token.authorization.test.js +++ b/test/core/implicit/id_token+token.authorization.test.js @@ -9,6 +9,9 @@ const scope = 'openid'; describe('IMPLICIT id_token+token', () => { before(bootstrap(__dirname)); + afterEach(function () { + this.provider.removeAllListeners(); + }); ['get', 'post'].forEach((verb) => { describe(`${verb} ${route} with session`, () => { diff --git a/test/formats/jwt.test.js b/test/formats/jwt.test.js index f9431b206..4fa8d7c04 100644 --- a/test/formats/jwt.test.js +++ b/test/formats/jwt.test.js @@ -19,6 +19,10 @@ function decode(b64urljson) { describe('jwt format', () => { before(bootstrap(__dirname)); + afterEach(function () { + this.provider.removeAllListeners(); + }); + const accountId = 'account'; const claims = {}; const clientId = 'client'; diff --git a/test/formats/paseto.test.js b/test/formats/paseto.test.js index 170e0809d..4cb196855 100644 --- a/test/formats/paseto.test.js +++ b/test/formats/paseto.test.js @@ -25,6 +25,11 @@ const generateKeyPair = util.promisify(crypto.generateKeyPair); describe('paseto format', () => { before(bootstrap(__dirname)); + + afterEach(function () { + this.provider.removeAllListeners(); + }); + const accountId = 'account'; const claims = {}; const clientId = 'client'; diff --git a/test/refresh/refresh.grant.test.js b/test/refresh/refresh.grant.test.js index 66e917dcb..3b7164f6b 100644 --- a/test/refresh/refresh.grant.test.js +++ b/test/refresh/refresh.grant.test.js @@ -18,6 +18,9 @@ describe('grant_type=refresh_token', () => { before(bootstrap(__dirname)); afterEach(() => timekeeper.reset()); + afterEach(function () { + this.provider.removeAllListeners(); + }); beforeEach(function () { return this.login({ scope: 'openid email offline_access' }); }); afterEach(function () { return this.logout(); }); diff --git a/test/resource_indicators/resource_indicators.test.js b/test/resource_indicators/resource_indicators.test.js index 21fadaee1..868fa337b 100644 --- a/test/resource_indicators/resource_indicators.test.js +++ b/test/resource_indicators/resource_indicators.test.js @@ -23,6 +23,9 @@ describe('features.resourceIndicators defaults', () => { describe('features.resourceIndicators', () => { before(bootstrap(__dirname)); + afterEach(function () { + this.provider.removeAllListeners(); + }); before(function () { return this.login({ resources: { From 05ac3a8cc51f18d33e17982b81f1996e6a327e8c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 5 Dec 2021 14:17:04 +0100 Subject: [PATCH 051/154] fix: add iss to error responses when issAuthResp is enabled --- lib/shared/authorization_error_handler.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/shared/authorization_error_handler.js b/lib/shared/authorization_error_handler.js index 2f02c5b21..f646f0829 100644 --- a/lib/shared/authorization_error_handler.js +++ b/lib/shared/authorization_error_handler.js @@ -98,6 +98,9 @@ module.exports = (provider) => { const renderError = instance(provider).configuration('renderError'); await renderError(ctx, out, err); } else { + if (instance(provider).configuration('features.issAuthResp.enabled')) { + out.iss = provider.issuer; + } let mode = safe(params.response_mode); if (!instance(provider).responseModes.has(mode)) { mode = resolveResponseMode(safe(params.response_type)); From a8ee83b5ef000d1e4443f63acb81dcfffa9b1676 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 5 Dec 2021 14:50:25 +0100 Subject: [PATCH 052/154] chore: bump issAuthResp draft to -04 --- README.md | 4 +- docs/README.md | 2 +- lib/actions/authorization/respond.js | 2 +- lib/helpers/defaults.js | 2 +- lib/helpers/features.js | 6 +- test/iss/iss.config.js | 29 +++++ test/iss/iss.test.js | 179 +++++++++++++++++++++++++++ test/test_helper.js | 7 ++ 8 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 test/iss/iss.config.js create mode 100644 test/iss/iss.test.js diff --git a/README.md b/README.md index b4f26a4ab..4d39ad690 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The following draft specifications are implemented by oidc-provider: - [JWT Response for OAuth Token Introspection - draft 10][jwt-introspection] - [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - Implementer's Draft 01][jarm] - [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba] -- [OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 01][iss-auth-resp] +- [OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 04][iss-auth-resp] - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop] - [OpenID Connect Back-Channel Logout 1.0 - draft 06][backchannel-logout] - [OpenID Connect RP-Initiated Logout 1.0 - draft 01][rpinitiated-logout] @@ -147,7 +147,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [support-sponsor]: https://github.com/sponsors/panva [par]: https://www.rfc-editor.org/rfc/rfc9126.html [rpinitiated-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html -[iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01 +[iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04 [fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html [ciba]: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html [fapi-ciba]: https://openid.net/specs/openid-financial-api-ciba-ID1.html diff --git a/docs/README.md b/docs/README.md index 3fcbce6c7..1996005b1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1147,7 +1147,7 @@ async function introspectionAllowedPolicy(ctx, client, token) { ### features.issAuthResp -[draft-ietf-oauth-iss-auth-resp-01](https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01) - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response +[draft-ietf-oauth-iss-auth-resp-04](https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04) - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response Enables `iss` authorization response parameter for responses without existing countermeasures against mix-up attacks. diff --git a/lib/actions/authorization/respond.js b/lib/actions/authorization/respond.js index db7f811a8..14351c5b8 100644 --- a/lib/actions/authorization/respond.js +++ b/lib/actions/authorization/respond.js @@ -18,7 +18,7 @@ module.exports = async function respond(ctx, next) { out.state = params.state; } - if (instance(ctx.oidc.provider).configuration('features.issAuthResp.enabled')) { + if (!out.id_token && instance(ctx.oidc.provider).configuration('features.issAuthResp.enabled')) { out.iss = ctx.oidc.provider.issuer; } diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 76f45b214..80eb3336b 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -1839,7 +1839,7 @@ function getDefaults() { /* * features.issAuthResp * - * title: [draft-ietf-oauth-iss-auth-resp-01](https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01) - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response + * title: [draft-ietf-oauth-iss-auth-resp-04](https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04) - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response * * description: Enables `iss` authorization response parameter for responses without * existing countermeasures against mix-up attacks. diff --git a/lib/helpers/features.js b/lib/helpers/features.js index 0aaba3cb2..3db6a585a 100644 --- a/lib/helpers/features.js +++ b/lib/helpers/features.js @@ -51,10 +51,10 @@ const DRAFTS = new Map(Object.entries({ version: [0, 'id-00', 'individual-draft-00'], }, issAuthResp: { - name: 'OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 01', + name: 'OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 04', type: 'IETF OAuth Working Group draft', - url: 'https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-01', - version: ['draft-00', 'draft-01'], + url: 'https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04', + version: ['draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04'], }, })); diff --git a/test/iss/iss.config.js b/test/iss/iss.config.js new file mode 100644 index 000000000..f202dfd5e --- /dev/null +++ b/test/iss/iss.config.js @@ -0,0 +1,29 @@ +const cloneDeep = require('lodash/cloneDeep'); +const merge = require('lodash/merge'); + +const config = cloneDeep(require('../default.config')); + +merge(config.features, { + issAuthResp: { enabled: true }, + jwtResponseModes: { enabled: true }, +}); + +module.exports = { + config, + clients: [{ + client_id: 'client', + token_endpoint_auth_method: 'none', + redirect_uris: ['https://client.example.com/cb'], + grant_types: ['authorization_code', 'implicit'], + scope: 'openid', + response_types: [ + 'code id_token token', + 'code id_token', + 'code token', + 'code', + 'id_token token', + 'id_token', + 'none', + ], + }], +}; diff --git a/test/iss/iss.test.js b/test/iss/iss.test.js new file mode 100644 index 000000000..0539075dc --- /dev/null +++ b/test/iss/iss.test.js @@ -0,0 +1,179 @@ +const { expect } = require('chai'); + +const bootstrap = require('../test_helper'); + +describe('features.issAuthResp', () => { + before(bootstrap(__dirname)); + + describe('enriched discovery', () => { + it('shows the url now', function () { + return this.agent.get('/.well-known/openid-configuration') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('authorization_response_iss_parameter_supported', true); + }); + }); + }); + + describe('OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response', () => { + before(function () { return this.login(); }); + + it('response_type=code', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validatePresence(['iss'], false)) + .expect(auth.validateClientLocation) + .expect(auth.validateIss); + }); + + it('response_type=code token', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code token', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['iss'], false)) + .expect(auth.validateClientLocation) + .expect(auth.validateIss); + }); + + it('response_type=code id_token', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code id_token', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['code', 'state', 'id_token'])) + .expect(auth.validateClientLocation); + }); + + it('response_type=code id_token token', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code id_token token', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['code', 'state', 'id_token', 'access_token', 'token_type', 'expires_in', 'scope'])) + .expect(auth.validateClientLocation); + }); + + it('response_type=id_token token', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'id_token token', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['state', 'id_token', 'access_token', 'token_type', 'expires_in', 'scope'])) + .expect(auth.validateClientLocation); + }); + + it('response_type=id_token', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'id_token', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['state', 'id_token'])) + .expect(auth.validateClientLocation); + }); + + it('response_type=none', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'none', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validatePresence(['state', 'iss'])) + .expect(auth.validateClientLocation) + .expect(auth.validateIss); + }); + + it('response_mode=jwt', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code', + response_mode: 'jwt', + scope: 'openid', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validatePresence(['response'])) + .expect(auth.validateClientLocation); + }); + + it('error with regular response modes', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code', + scope: 'openid profile', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validatePresence(['error', 'iss'], false)) + .expect(auth.validateClientLocation) + .expect(auth.validateIss); + }); + + it('error with response_type none', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'none', + scope: 'openid profile', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validatePresence(['error', 'iss'], false)) + .expect(auth.validateClientLocation) + .expect(auth.validateIss); + }); + + it('error with response_mode=jwt', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code', + response_mode: 'jwt', + scope: 'openid profile', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validatePresence(['response'])) + .expect(auth.validateClientLocation); + }); + + it('error with response_mode=jwt fragment', function () { + const auth = new this.AuthorizationRequest({ + response_type: 'code id_token', + response_mode: 'jwt', + scope: 'openid profile', + }); + + return this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(303) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['response'])) + .expect(auth.validateClientLocation); + }); + }); +}); diff --git a/test/test_helper.js b/test/test_helper.js index 9bc1739fb..f055e5b38 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -222,6 +222,13 @@ module.exports = function testHelper(dir, { }, }); + Object.defineProperty(this, 'validateIss', { + value: (response) => { + const { query: { iss } } = parse(response.headers.location, true); + expect(iss).to.equal(issuerIdentifier); + }, + }); + Object.defineProperty(this, 'validateInteractionRedirect', { value: (response) => { const { hostname, search, query } = parse(response.headers.location); From 21f997fd590ba3d5b936013239ea6237395fb956 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 5 Dec 2021 14:52:08 +0100 Subject: [PATCH 053/154] chore(release): 7.10.4 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d605aeb..558bfe4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.10.4](https://github.com/panva/node-oidc-provider/compare/v7.10.3...v7.10.4) (2021-12-05) + + +### Fixes + +* add iss to error responses when issAuthResp is enabled ([05ac3a8](https://github.com/panva/node-oidc-provider/commit/05ac3a8cc51f18d33e17982b81f1996e6a327e8c)) + ## [7.10.3](https://github.com/panva/node-oidc-provider/compare/v7.10.2...v7.10.3) (2021-12-04) diff --git a/package.json b/package.json index 60a67c579..cc7d1e0b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-provider", - "version": "7.10.3", + "version": "7.10.4", "description": "OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect", "keywords": [ "appauth", From 2ac6a346498cccfbe0ca07474bdb270d5d712c1a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 6 Dec 2021 11:26:21 +0100 Subject: [PATCH 054/154] example: enable issAuthResp --- certification/fapi/index.js | 1 + certification/oidc/configuration.js | 1 + example/support/configuration.js | 1 + 3 files changed, 3 insertions(+) diff --git a/certification/fapi/index.js b/certification/fapi/index.js index 4845f0cbd..5544b4f2d 100644 --- a/certification/fapi/index.js +++ b/certification/fapi/index.js @@ -124,6 +124,7 @@ const fapi = new Provider(ISSUER, { enabled: true, profile: process.env.PROFILE ? process.env.PROFILE : '1.0 Final', }, + issAuthResp: { enabled: true }, mTLS: { enabled: true, certificateBoundAccessTokens: true, diff --git a/certification/oidc/configuration.js b/certification/oidc/configuration.js index beb098054..43a7faf94 100644 --- a/certification/oidc/configuration.js +++ b/certification/oidc/configuration.js @@ -66,6 +66,7 @@ module.exports = { features: { backchannelLogout: { enabled: true }, devInteractions: { enabled: false }, + issAuthResp: { enabled: true }, mTLS: { enabled: true, certificateBoundAccessTokens: true, diff --git a/example/support/configuration.js b/example/support/configuration.js index 2b031ee05..e5913316c 100644 --- a/example/support/configuration.js +++ b/example/support/configuration.js @@ -27,6 +27,7 @@ module.exports = { deviceFlow: { enabled: true }, // defaults to false revocation: { enabled: true }, // defaults to false + issAuthResp: { enabled: true }, // defaults to false }, jwks: { keys: [ From 1ac434ae23c0734b838a79c48eb82184e1ba788b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 13 Dec 2021 10:59:42 +0100 Subject: [PATCH 055/154] refactor: substr(ing) > slice --- docs/update-configuration.js | 6 +++--- example/views/repost.ejs | 4 ++-- lib/helpers/keystore.js | 4 ++-- lib/helpers/oidc_context.js | 2 +- lib/models/formats/paseto.js | 2 +- lib/provider.js | 2 +- test/ci.js | 2 +- test/formats/paseto.test.js | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/update-configuration.js b/docs/update-configuration.js index d44df1f70..079e04fc0 100644 --- a/docs/update-configuration.js +++ b/docs/update-configuration.js @@ -123,15 +123,15 @@ const props = [ if (nextIsOption) { nextIsOption = false; - option = blocks[strLine.substring(2)] = new Block(); // eslint-disable-line no-multi-assign + option = blocks[strLine.slice(2)] = new Block(); // eslint-disable-line no-multi-assign return; } const next = props.find((prop) => { if ( prop.startsWith('@') - ? strLine.substring(2, 2 + prop.length) === prop - : strLine.substring(2, 2 + prop.length + 1) === `${prop}:` + ? strLine.slice(2, 2 + prop.length) === prop + : strLine.slice(2, 2 + prop.length + 1) === `${prop}:` ) { let override; if (prop === 'example' && option.example) { diff --git a/example/views/repost.ejs b/example/views/repost.ejs index ff592c2c3..96767dde4 100644 --- a/example/views/repost.ejs +++ b/example/views/repost.ejs @@ -6,7 +6,7 @@