diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc660540..94b0d5bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - name: Checkout uses: actions/checkout@master - name: Setup node - uses: actions/setup-node@v2-beta + uses: actions/setup-node@v2 with: node-version: 12 - name: Store node version variable @@ -57,7 +57,7 @@ jobs: - name: Checkout uses: actions/checkout@master - name: Setup node - uses: actions/setup-node@v2-beta + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Store node version variable @@ -87,7 +87,7 @@ jobs: - windows-latest steps: - uses: actions/checkout@master - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v2 with: node-version: 12 - name: Store node version variable @@ -105,59 +105,146 @@ jobs: if: ${{ steps.node_modules.outputs.cache-hit != 'true' }} - run: npx xvfb-maybe npx electron@${{ matrix.electron-version }} ./test/electron test/**/*.test.js - oidc-conformance: + build-conformance-suite: runs-on: ubuntu-latest + env: + VERSION: release-v4.1.35 steps: - - run: | - docker pull panvafs/oidc-certification-rp-ci - docker run -d -p 8080:8080 panvafs/oidc-certification-rp-ci - while ! curl -sk https://127.0.0.1:8080 >/dev/null; do sleep 2; done - - run: git clone --depth 1 --single-branch --branch five https://github.com/panva/openid-client-conformance-tests.git . - - uses: actions/setup-node@v1 + - name: Checkout + uses: actions/checkout@master + - name: Load Cached Conformance Suite Build + uses: actions/cache@v2 + id: cache with: - node-version: 12 - - run: npx panva/npm-install-retry - - run: npm install ${{ github.repository }}#${{ github.sha }} - - run: npm run test + path: ./conformance-suite + key: suite-${{ hashFiles('**/test.yml') }} + - name: Conformance Suite Checkout + if: ${{ steps.cache.outputs.cache-hit != 'true' }} + run: git clone --depth 1 --single-branch --branch $VERSION https://gitlab.com/openid/conformance-suite.git + - name: Conformance Suite Build + working-directory: ./conformance-suite + if: ${{ steps.cache.outputs.cache-hit != 'true' }} env: - ISSUER: https://localhost:8080 - NODE_TLS_REJECT_UNAUTHORIZED: 0 - CI: true - - fapi-conformance: - runs-on: ubuntu-latest - steps: - - run: git clone --depth 1 --single-branch --branch release-v4.1.32 https://gitlab.com/openid/conformance-suite.git - - env: MAVEN_CACHE: ./m2 run: | sed -i -e 's/localhost/localhost.emobix.co.uk/g' src/main/resources/application.properties - sed -i -e 's/-B/-B -DskipTests=true/g' builder-compose.yml + sed -i -e 's/-B clean/-B -DskipTests=true/g' builder-compose.yml docker-compose -f builder-compose.yml run builder + + conformance-suite: + runs-on: ubuntu-latest + needs: + - test + - electron + - build-conformance-suite + env: + NODE_TLS_REJECT_UNAUTHORIZED: 0 + DEBUG: runner,moduleId* + SUITE_BASE_URL: https://localhost.emobix.co.uk:8443 + PLAN_NAME: ${{ matrix.setup.plan }} + VARIANT: ${{ toJSON(matrix.setup) }} + strategy: + fail-fast: false + matrix: + setup: + # OIDC BASIC + - plan: oidcc-client-basic-certification-test-plan + + # OIDC IMPLICIT + - plan: oidcc-client-implicit-certification-test-plan + + # OIDC HYBRID + - plan: oidcc-client-hybrid-certification-test-plan + + # OIDC CONFIG + - plan: oidcc-client-config-certification-test-plan + + # OIDC DYNAMIC + - plan: oidcc-client-dynamic-certification-test-plan + + # FAPI 1.0 ID-2 + - plan: fapi-rw-id2-client-test-plan + client_auth_type: mtls + - plan: fapi-rw-id2-client-test-plan + client_auth_type: private_key_jwt + + # FAPI 1.0 Advanced Final + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + fapi_auth_request_method: pushed + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + fapi_auth_request_method: pushed + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + fapi_response_mode: jarm + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + fapi_response_mode: jarm + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + fapi_auth_request_method: pushed + fapi_response_mode: jarm + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + fapi_auth_request_method: pushed + fapi_response_mode: jarm + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + fapi_auth_request_method: pushed + fapi_response_mode: jarm + fapi_jarm_type: plain_oauth + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + fapi_response_mode: jarm + fapi_jarm_type: plain_oauth + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + fapi_auth_request_method: pushed + fapi_response_mode: jarm + fapi_jarm_type: plain_oauth + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + fapi_response_mode: jarm + fapi_jarm_type: plain_oauth + + steps: + - name: Load Cached Conformance Suite Build + uses: actions/cache@v2 + id: cache + with: + path: ./conformance-suite + key: suite-${{ hashFiles('**/test.yml') }} + - name: Run Conformance Suite + working-directory: ./conformance-suite + run: | docker-compose -f docker-compose-dev.yml up -d while ! curl -skfail https://localhost.emobix.co.uk:8443/api/runner/available >/dev/null; do sleep 2; done - working-directory: ./conformance-suite - - run: git clone --depth 1 --single-branch --branch five https://github.com/panva/openid-client-fapi-certification.git runner - - uses: actions/setup-node@v1 + - run: git clone --depth 1 --single-branch --branch five https://github.com/panva/openid-client-certification-suite.git runner + - uses: actions/setup-node@v2 with: node-version: 12 - run: npx panva/npm-install-retry working-directory: ./runner - run: npm install ${{ github.repository }}#${{ github.sha }} working-directory: ./runner - - name: run mtls variant - run: npm run test - working-directory: ./runner - env: - NODE_TLS_REJECT_UNAUTHORIZED: 0 - DEBUG: runner,fapi-rw-id2-* - VARIANT: '{"client_auth_type":"mtls","fapi_profile":"plain_fapi"}' - SUITE_BASE_URL: https://localhost.emobix.co.uk:8443 - - name: run private_key_jwt variant - run: npm run test + - run: npm run test working-directory: ./runner - env: - NODE_TLS_REJECT_UNAUTHORIZED: 0 - DEBUG: runner,fapi-rw-id2-* - VARIANT: '{"client_auth_type":"private_key_jwt","fapi_profile":"plain_fapi"}' - SUITE_BASE_URL: https://localhost.emobix.co.uk:8443 + - name: Upload test artifacts + uses: actions/upload-artifact@v2 + with: + path: runner/export-*.zip + name: ${{ matrix.setup.plan }} failed html results + if-no-files-found: ignore + if: ${{ failure() }} + - name: Upload test logs + uses: actions/upload-artifact@v2 + with: + if-no-files-found: warn + name: ${{ matrix.setup.plan }} runner logs + path: runner/logs/*.log + if: ${{ failure() }} diff --git a/README.md b/README.md index 8f3eed44..c1f89ca5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ openid-client. - [RFC9126 - OAuth 2.0 Pushed Authorization Requests (PAR)][feature-par] - [OpenID Connect Session Management 1.0 - draft 28][feature-rp-logout] - RP-Initiated Logout -- [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - ID2][feature-fapi] +- [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][feature-fapi] - [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - ID1][feature-jarm] - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][feature-dpop] @@ -286,7 +286,7 @@ See [Customizing (docs)](https://github.com/panva/node-openid-client/blob/master [feature-device-flow]: https://tools.ietf.org/html/rfc8628 [feature-rp-logout]: https://openid.net/specs/openid-connect-session-1_0.html#RPLogout [feature-jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html -[feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-ID2.html +[feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html [feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03 [feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html [feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html diff --git a/docs/README.md b/docs/README.md index 472d7ba4..aa166c44 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,7 +29,7 @@ If you or your business use openid-client, please consider becoming a [sponsor][ - [Class: <Issuer>](#class-issuer) - [new Issuer(metadata)](#new-issuermetadata) - [issuer.Client](#issuerclient) - - [issuer.FAPIClient](#issuerfapiclient) + - [issuer.FAPI1Client](#issuerfapi1client) - [issuer.metadata](#issuermetadata) - [Issuer.discover(issuer)](#issuerdiscoverissuer) - [Issuer.webfinger(input)](#issuerwebfingerinput) @@ -87,12 +87,12 @@ Returns the `` class tied to this issuer. --- -#### `issuer.FAPIClient` +#### `issuer.FAPI1Client` -Returns the `` class tied to this issuer. `` inherits from `` and -adds necessary FAPI behaviours: +Returns the `` class tied to this issuer. `` inherits from `` and +adds necessary [Financial-grade API Security Profile 1.0 - Part 2: Advanced][] behaviours: -- Returns: `` +- Returns: `` The behaviours are: - `s_hash` presence and value checks in authorization endpoint response ID Tokens @@ -292,6 +292,10 @@ Performs the callback for Authorization Server's authorization response. - `max_age`: `` When provided the authorization response's ID Token auth_time parameter will be checked to be conform to the max_age value. Use of this check is required if you sent a max_age parameter into an authorization request. **Default:** uses client's `default_max_age`. + - `scope`: `` (FAPI1Client only) When provided the Token Endpoint Authorization Code + exchange response `scope` will be checked to be either an exact match, or containing a subset of + the scope sent in the authorization request. + - `extras`: `` - `exchangeBody`: `` extra request body properties to be sent to the AS during code exchange. @@ -1029,6 +1033,7 @@ request instance. [webfinger-discovery]: https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery [got-library]: https://github.com/sindresorhus/got/tree/v11.8.0 [client-authentication]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication +[Financial-grade API Security Profile 1.0 - Part 2: Advanced]: https://openid.net/specs/openid-financial-api-part-2-1_0.html [^dpop-exception]: Ed25519, Ed448, and all Elliptic Curve keys have a fixed algorithm. RSA and RSA-PSS keys look for an algorithm supported by the issuer metadata, if none is found PS256 is used as fallback. diff --git a/lib/client.js b/lib/client.js index 3adb2a7e..0bcfc0d0 100644 --- a/lib/client.js +++ b/lib/client.js @@ -23,9 +23,7 @@ const { OPError, RPError } = require('./errors'); const now = require('./helpers/unix_timestamp'); const { random } = require('./helpers/generators'); const request = require('./helpers/request'); -const { - CALLBACK_PROPERTIES, CLIENT_DEFAULTS, JWT_CONTENT, CLOCK_TOLERANCE, -} = require('./helpers/consts'); +const { CLOCK_TOLERANCE } = require('./helpers/consts'); const instance = require('./helpers/weak_cache'); const KeyStore = require('./helpers/keystore'); const { authenticatedPost, resolveResponseType, resolveRedirectUri } = require('./helpers/client'); @@ -39,7 +37,20 @@ const [major, minor] = process.version const rsaPssParams = major >= 17 || (major === 16 && minor >= 9); function pickCb(input) { - return pick(input, ...CALLBACK_PROPERTIES); + return pick( + input, + 'access_token', // OAuth 2.0 + 'code', // OAuth 2.0 + 'error', // OAuth 2.0 + 'error_description', // OAuth 2.0 + 'error_uri', // OAuth 2.0 + 'expires_in', // OAuth 2.0 + 'id_token', // OIDC Core 1.0 + 'state', // OAuth 2.0 + 'token_type', // OAuth 2.0 + 'session_state', // OIDC Session Management + 'response', // FAPI JARM + ); } function authorizationHeaderValue(token, tokenType = 'Bearer') { @@ -143,7 +154,11 @@ function getDefaultsForEndpoint(endpoint, issuer, properties) { } } -class BaseClient {} +class BaseClient { + fapi() { + return this.constructor.name === 'FAPI1Client'; + } +} module.exports = (issuer, aadIssValidation = false) => class Client extends BaseClient { /** @@ -157,7 +172,39 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base throw new TypeError('client_id is required'); } - const properties = { ...CLIENT_DEFAULTS, ...metadata }; + const properties = { + grant_types: ['authorization_code'], + id_token_signed_response_alg: 'RS256', + authorization_signed_response_alg: 'RS256', + response_types: ['code'], + token_endpoint_auth_method: 'client_secret_basic', + ...(this.fapi() ? { + grant_types: ['authorization_code', 'implicit'], + id_token_signed_response_alg: 'PS256', + authorization_signed_response_alg: 'PS256', + response_types: ['code id_token'], + tls_client_certificate_bound_access_tokens: true, + token_endpoint_auth_method: undefined, + } : undefined), + ...metadata, + }; + + if (this.fapi()) { + switch (properties.token_endpoint_auth_method) { + case 'self_signed_tls_client_auth': + case 'tls_client_auth': + break; + case 'private_key_jwt': + if (!jwks) { + throw new TypeError('jwks is required'); + } + break; + case undefined: + throw new TypeError('token_endpoint_auth_method is required'); + default: + throw new TypeError('invalid or unsupported token_endpoint_auth_method'); + } + } handleCommonMistakes(this, metadata, properties); @@ -428,6 +475,18 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base tokenset.session_state = params.session_state; } + if (tokenset.scope && checks.scope && this.fapi()) { + const expected = new Set(checks.scope.split(' ')); + const actual = tokenset.scope.split(' '); + if (!actual.every(Set.prototype.has, expected)) { + throw new RPError({ + message: 'unexpected scope returned', + checks, + scope: tokenset.scope, + }); + } + } + return tokenset; } @@ -513,13 +572,27 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base } if (params.code) { - return this.grant({ + const tokenset = await this.grant({ ...exchangeBody, grant_type: 'authorization_code', code: params.code, redirect_uri: redirectUri, code_verifier: checks.code_verifier, }, { clientAssertionPayload, DPoP }); + + if (tokenset.scope && checks.scope && this.fapi()) { + const expected = new Set(checks.scope.split(' ')); + const actual = tokenset.scope.split(' '); + if (!actual.every(Set.prototype.has, expected)) { + throw new RPError({ + message: 'unexpected scope returned', + checks, + scope: tokenset.scope, + }); + } + } + + return tokenset; } return new TokenSet(params); @@ -691,8 +764,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base }); } - const fapi = this.constructor.name === 'FAPIClient'; - if (returnedBy === 'authorization') { if (!payload.at_hash && tokenSet.access_token) { throw new RPError({ @@ -708,7 +779,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base }); } - if (fapi) { + if (this.fapi()) { if (!payload.s_hash && (tokenSet.state || state)) { throw new RPError({ message: 'missing required property s_hash', @@ -730,7 +801,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base } } - if (fapi && payload.iat < timestamp - 3600) { + if (this.fapi() && payload.iat < timestamp - 3600) { throw new RPError({ printf: ['JWT issued too far in the past, now %i, iat %i', timestamp, payload.iat], now: timestamp, @@ -1101,7 +1172,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base let parsed = processResponse(response, { bearer: true }); if (jwt) { - if (!JWT_CONTENT.test(response.headers['content-type'])) { + if (!/^application\/jwt/.test(response.headers['content-type'])) { throw new RPError({ message: 'expected application/jwt response from the userinfo_endpoint', response, @@ -1370,7 +1441,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base let signed; let key; - const fapi = this.constructor.name === 'FAPIClient'; const unix = now(); const header = { alg: signingAlgorithm, typ: 'oauth-authz-req+jwt' }; const payload = JSON.stringify(defaults({}, requestObject, { @@ -1380,7 +1450,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base jti: random(), iat: unix, exp: unix + 300, - ...(fapi ? { nbf: unix } : undefined), + ...(this.fapi() ? { nbf: unix } : undefined), })); if (signingAlgorithm === 'none') { diff --git a/lib/helpers/client.js b/lib/helpers/client.js index 1a658dc0..6ceeb4e5 100644 --- a/lib/helpers/client.js +++ b/lib/helpers/client.js @@ -100,7 +100,7 @@ async function authFor(endpoint, { clientAssertionPayload } = {}) { }, }; } - default: { // client_secret_basic + case 'client_secret_basic': { // This is correct behaviour, see https://tools.ietf.org/html/rfc6749#section-2.3.1 and the // related appendix. (also https://github.com/panva/node-openid-client/pull/91) // > The client identifier is encoded using the @@ -115,6 +115,9 @@ async function authFor(endpoint, { clientAssertionPayload } = {}) { const value = Buffer.from(encoded).toString('base64'); return { headers: { Authorization: `Basic ${value}` } }; } + default: { + throw new TypeError(`missing, or unsupported, ${endpoint}_endpoint_auth_method`); + } } } diff --git a/lib/helpers/consts.js b/lib/helpers/consts.js index f652be81..78a80706 100644 --- a/lib/helpers/consts.js +++ b/lib/helpers/consts.js @@ -1,60 +1,8 @@ -const OIDC_DISCOVERY = '/.well-known/openid-configuration'; -const WEBFINGER = '/.well-known/webfinger'; -const REL = 'http://openid.net/specs/connect/1.0/issuer'; -const AAD_MULTITENANT_DISCOVERY = [ - `https://login.microsoftonline.com/common${OIDC_DISCOVERY}`, - `https://login.microsoftonline.com/common/v2.0${OIDC_DISCOVERY}`, - `https://login.microsoftonline.com/organizations/v2.0${OIDC_DISCOVERY}`, - `https://login.microsoftonline.com/consumers/v2.0${OIDC_DISCOVERY}`, -]; - -const CLIENT_DEFAULTS = { - grant_types: ['authorization_code'], - id_token_signed_response_alg: 'RS256', - authorization_signed_response_alg: 'RS256', - response_types: ['code'], - token_endpoint_auth_method: 'client_secret_basic', -}; - -const ISSUER_DEFAULTS = { - claim_types_supported: ['normal'], - claims_parameter_supported: false, - grant_types_supported: ['authorization_code', 'implicit'], - request_parameter_supported: false, - request_uri_parameter_supported: true, - require_request_uri_registration: false, - response_modes_supported: ['query', 'fragment'], - token_endpoint_auth_methods_supported: ['client_secret_basic'], -}; - -const CALLBACK_PROPERTIES = [ - 'access_token', // 6749 - 'code', // 6749 - 'error', // 6749 - 'error_description', // 6749 - 'error_uri', // 6749 - 'expires_in', // 6749 - 'id_token', // Core 1.0 - 'state', // 6749 - 'token_type', // 6749 - 'session_state', // Session Management - 'response', // JARM -]; - -const JWT_CONTENT = /^application\/jwt/; - -const HTTP_OPTIONS = Symbol('openid-client.custom.http-options'); -const CLOCK_TOLERANCE = Symbol('openid-client.custom.clock-tolerance'); +/* eslint-disable symbol-description */ +const HTTP_OPTIONS = Symbol(); +const CLOCK_TOLERANCE = Symbol(); module.exports = { - AAD_MULTITENANT_DISCOVERY, - CALLBACK_PROPERTIES, - CLIENT_DEFAULTS, CLOCK_TOLERANCE, HTTP_OPTIONS, - ISSUER_DEFAULTS, - JWT_CONTENT, - OIDC_DISCOVERY, - REL, - WEBFINGER, }; diff --git a/lib/issuer.js b/lib/issuer.js index f4f81cc1..79a64a79 100644 --- a/lib/issuer.js +++ b/lib/issuer.js @@ -14,12 +14,25 @@ const webfingerNormalize = require('./helpers/webfinger_normalize'); const instance = require('./helpers/weak_cache'); const request = require('./helpers/request'); const { assertIssuerConfiguration } = require('./helpers/assert'); -const { - ISSUER_DEFAULTS, OIDC_DISCOVERY, WEBFINGER, REL, AAD_MULTITENANT_DISCOVERY, -} = require('./helpers/consts'); const KeyStore = require('./helpers/keystore'); +const AAD_MULTITENANT_DISCOVERY = [ + 'https://login.microsoftonline.com/common/.well-known/openid-configuration', + 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration', + 'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration', + 'https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration', +]; const AAD_MULTITENANT = Symbol('AAD_MULTITENANT'); +const ISSUER_DEFAULTS = { + claim_types_supported: ['normal'], + claims_parameter_supported: false, + grant_types_supported: ['authorization_code', 'implicit'], + request_parameter_supported: false, + request_uri_parameter_supported: true, + require_request_uri_registration: false, + response_modes_supported: ['query', 'fragment'], + token_endpoint_auth_methods_supported: ['client_secret_basic'], +}; class Issuer { /** @@ -65,7 +78,7 @@ class Issuer { Object.defineProperties(this, { Client: { value: Client }, - FAPIClient: { value: class FAPIClient extends Client {} }, + FAPI1Client: { value: class FAPI1Client extends Client {} }, }); } @@ -162,17 +175,17 @@ class Issuer { static async webfinger(input) { const resource = webfingerNormalize(input); const { host } = url.parse(resource); - const webfingerUrl = `https://${host}${WEBFINGER}`; + const webfingerUrl = `https://${host}/.well-known/webfinger`; const response = await request.call(this, { method: 'GET', url: webfingerUrl, responseType: 'json', - searchParams: { resource, rel: REL }, + searchParams: { resource, rel: 'http://openid.net/specs/connect/1.0/issuer' }, }); const body = processResponse(response); - const location = Array.isArray(body.links) && body.links.find((link) => typeof link === 'object' && link.rel === REL && link.href); + const location = Array.isArray(body.links) && body.links.find((link) => typeof link === 'object' && link.rel === 'http://openid.net/specs/connect/1.0/issuer' && link.href); if (!location) { throw new RPError({ @@ -196,7 +209,7 @@ class Issuer { const issuer = await this.discover(expectedIssuer); if (issuer.issuer !== expectedIssuer) { - registry.delete(issuer.issuer); + registry.del(issuer.issuer); throw new RPError('discovered issuer mismatch, expected %s, got: %s', expectedIssuer, issuer.issuer); } return issuer; @@ -227,9 +240,9 @@ class Issuer { let pathname; if (parsed.pathname.endsWith('/')) { - pathname = `${parsed.pathname}${OIDC_DISCOVERY.substring(1)}`; + pathname = `${parsed.pathname}.well-known/openid-configuration`; } else { - pathname = `${parsed.pathname}${OIDC_DISCOVERY}`; + pathname = `${parsed.pathname}/.well-known/openid-configuration`; } const wellKnownUri = url.format({ ...parsed, pathname }); diff --git a/lib/issuer_registry.js b/lib/issuer_registry.js index 4fd05ea3..291f489b 100644 --- a/lib/issuer_registry.js +++ b/lib/issuer_registry.js @@ -1,3 +1,3 @@ -const REGISTRY = new Map(); +const LRU = require('lru-cache'); -module.exports = REGISTRY; +module.exports = new LRU({ max: 100 }); diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index 7cc8e775..f190b5b1 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -1941,9 +1941,9 @@ describe('Client', () => { client_secret: 'its gotta be a long secret and i mean at least 32 characters', }, undefined, { additionalAuthorizedParties: ['authorized third party', 'another third party'] }); - this.fapiClient = new this.issuer.FAPIClient({ + this.fapiClient = new this.issuer.FAPI1Client({ client_id: 'identifier', - client_secret: 'secure', + token_endpoint_auth_method: 'tls_client_auth', }); this.IdToken = async (key, alg, payload) => { @@ -2601,7 +2601,7 @@ describe('Client', () => { const code = 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y'; // eslint-disable-line camelcase, max-len const c_hash = '77QmUPtjPfzWtF2AnpK9RQ'; // eslint-disable-line camelcase - return this.IdToken(this.keystore.get(), 'RS256', { + return this.IdToken(this.keystore.get(), 'PS256', { c_hash, iss: this.issuer.issuer, sub: 'userId', @@ -2623,7 +2623,7 @@ describe('Client', () => { const c_hash = '77QmUPtjPfzWtF2AnpK9RQ'; // eslint-disable-line camelcase const s_hash = 'LCa0a2j_xo_5m0U8HTBBNA'; // eslint-disable-line camelcase - return this.IdToken(this.keystore.get(), 'RS256', { + return this.IdToken(this.keystore.get(), 'PS256', { c_hash, s_hash, iss: this.issuer.issuer, @@ -3338,7 +3338,7 @@ describe('Client', () => { describe('FAPIClient', function () { it('includes nbf by default', function () { - const client = new this.issuer.FAPIClient({ client_id: 'identifier', request_object_signing_alg: 'PS256' }, this.keystore.toJWKS(true)); + const client = new this.issuer.FAPI1Client({ client_id: 'identifier', request_object_signing_alg: 'PS256', token_endpoint_auth_method: 'private_key_jwt' }, this.keystore.toJWKS(true)); return client.requestObject({}) .then((signed) => { const { iat, exp, nbf } = JSON.parse(base64url.decode(signed.split('.')[1])); diff --git a/test/issuer/discover_webfinger.test.js b/test/issuer/discover_webfinger.test.js index 67656788..318fbb6d 100644 --- a/test/issuer/discover_webfinger.test.js +++ b/test/issuer/discover_webfinger.test.js @@ -2,7 +2,8 @@ const { expect } = require('chai'); const nock = require('nock'); const sinon = require('sinon'); -const { Issuer, Registry, custom } = require('../../lib'); +const { Issuer, custom } = require('../../lib'); +const Registry = require('../../lib/issuer_registry'); const fail = () => { throw new Error('expected promise to be rejected'); }; @@ -119,7 +120,7 @@ describe('Issuer#webfinger()', () => { }); it('validates the discovered issuer is the same as from webfinger', function () { - Registry.clear(); + Registry.reset(); const webfinger = nock('https://op.example.com') .get('/.well-known/webfinger') .query(function (query) { diff --git a/types/index.d.ts b/types/index.d.ts index c03e4193..9e751a78 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -650,9 +650,9 @@ export class Issuer { Client: TypeOfGenericClient; /** - * Returns the class tied to this issuer. + * Returns the class tied to this issuer. */ - FAPIClient: TypeOfGenericClient; + FAPI1Client: TypeOfGenericClient; /** * Returns metadata from the issuer's discovery document.