diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4e17abd1..593028c0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -13,9 +13,3 @@ contact_links: about: Are you asking your nth question? Relying on openid-client 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: - Do not disclose vulnerabilities via issues or discussions. Reach out to the project team - via e.g. email, we'll work together on patching the vulnerability and follow some form of - Responsible disclosure once fixed. Thank you. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7b3bebd8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index df2a3be2..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: "Code scanning - action" - -on: - pull_request: - paths-ignore: - - '**.md' - schedule: - - cron: '0 8 * * 4' - -jobs: - CodeQL-Build: - - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..6da1af56 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '20 11 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 00000000..cf5334fc --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,139 @@ +name: Conformance Checks + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '20 11 * * 1' + workflow_dispatch: + +jobs: + build: + uses: panva/.github/.github/workflows/build-conformance-suite.yml@main + + run: + runs-on: ubuntu-latest + needs: + - build + 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 + # TODO: work around the request_uri lodging service EOL + # - 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_client_type: plain_oauth + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: mtls + fapi_response_mode: jarm + fapi_client_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_client_type: plain_oauth + - plan: fapi1-advanced-final-client-test-plan + client_auth_type: private_key_jwt + fapi_response_mode: jarm + fapi_client_type: plain_oauth + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set Conformance Suite Version + run: | + export VERSION=($(curl --silent "https://gitlab.com/api/v4/projects/4175605/releases" | jq -r '.[0].tag_name')) + echo "VERSION=$VERSION" >> $GITHUB_ENV + - name: Load Cached Conformance Suite Build + uses: actions/cache@v3 + id: cache + with: + path: ./conformance-suite + key: ${{ needs.build.outputs.cache-key }} + fail-on-cache-miss: true + - 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 + - run: git clone --depth 1 --single-branch --branch main https://github.com/panva/openid-client-certification-suite.git runner + - uses: actions/setup-node@v4 + with: + node-version: lts/iron # 20 + cache: 'npm' + - run: npm clean-install + working-directory: ./runner + - run: npm install ${{ github.repository }}#${{ github.sha }} + working-directory: ./runner + - run: npm run test + working-directory: ./runner + - name: Upload test artifacts + uses: actions/upload-artifact@v3 + 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@v3 + with: + if-no-files-found: warn + name: ${{ matrix.setup.plan }} runner logs + path: runner/logs/*.log + if: ${{ failure() }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 44399fc3..24cc867c 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -2,16 +2,17 @@ name: 'Lock threads' on: schedule: - - cron: '0 9 * * *' + - cron: '20 11 * * 1' jobs: lock: + continue-on-error: true runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@be8aa5be94131386884a6da4189effda9b14aa21 # v4.0.1 with: github-token: ${{ github.token }} - issue-lock-inactive-days: '90' + issue-inactive-days: '90' issue-lock-reason: '' - pr-lock-inactive-days: '90' + pr-inactive-days: '90' pr-lock-reason: '' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..92596e17 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: ['v[0-9]+.[0-9]+.[0-9]+'] + +jobs: + npm: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: lts/iron # 20 + registry-url: https://registry.npmjs.org + always-auth: true + - run: npm publish --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + cleanup: + needs: + - npm + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: git push origin $GITHUB_SHA:v5.x + - run: git push origin HEAD:main + + github: + needs: + - npm + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: lts/iron # 20 + cache: 'npm' + - run: node .release-notes.cjs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/retry.yml b/.github/workflows/retry.yml new file mode 100644 index 00000000..c13c54f9 --- /dev/null +++ b/.github/workflows/retry.yml @@ -0,0 +1,17 @@ +name: Retry + +on: + workflow_run: + workflows: + - Conformance Checks + types: + - completed + +jobs: + retry: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1 }} + steps: + - run: gh api -XPOST ${{ github.event.workflow_run.rerun_url }}-failed-jobs + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a89458ed..1bf37718 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,224 +1,76 @@ -name: Continuous Integration +name: Test on: push: - paths-ignore: - - '**.md' + branches: [main] pull_request: - paths-ignore: - - '**.md' + branches: [main] schedule: - - cron: 0 10 * * 1-5 + - cron: '20 11 * * 1' + workflow_dispatch: jobs: + audit: + uses: panva/.github/.github/workflows/npm-audit.yml@main + test: - name: Node Tests + Coverage runs-on: ubuntu-latest - continue-on-error: ${{ matrix.experimental || false }} + continue-on-error: ${{ !startsWith(matrix.node-version, 'lts') }} strategy: fail-fast: false matrix: node-version: - - 12.19.0 - - 12 - - 14.15.0 - - 14 - - 16.13.0 - - 16 - include: - - experimental: true - node-version: '>=17' + - lts/erbium # 12 + - lts/fermium # 14 + - lts/gallium # 16 + - lts/hydrogen # 18 + - lts/iron # 20 + - current steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@v4 - name: Setup node - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - name: Store node version variable + uses: actions/setup-node@v4 id: node - run: | - echo "::set-output name=version::$(node -v)" - - name: Cache node_modules - uses: actions/cache@v2 - id: node_modules with: - path: node_modules - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package.json') }}-${{ steps.node.outputs.version }} + node-version: ${{ matrix.node-version }} + cache: 'npm' + check-latest: true + - run: npm install --global npm@8 + if: ${{ startsWith(steps.node.outputs.node-version, 'v12') || startsWith(steps.node.outputs.node-version, 'v14') }} - name: Install dependencies - run: npx panva/npm-install-retry - if: ${{ steps.node_modules.outputs.cache-hit != 'true' }} - - run: npm run coverage + run: npm clean-install + - run: npm run test - electron: + bun: runs-on: ubuntu-latest - strategy: - matrix: - electron-version: - - 12.0.0 - - latest + continue-on-error: true steps: - - uses: actions/checkout@master - - uses: actions/setup-node@v2 - with: - node-version: 12 - - name: Store node version variable + - name: Checkout + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 id: node - run: | - echo "::set-output name=version::$(node -v)" - - name: Cache node_modules - uses: actions/cache@v2 - id: node_modules with: - path: node_modules - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package.json') }}-${{ steps.node.outputs.version }} + node-version: lts/iron # 20 + cache: 'npm' + check-latest: true - name: Install dependencies - run: npx panva/npm-install-retry - if: ${{ steps.node_modules.outputs.cache-hit != 'true' }} - - run: npx xvfb-maybe npx electron@${{ matrix.electron-version }} ./test/electron test/**/*.test.js - - build-conformance-suite: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@master - - name: Set Conformance Suite Version - run: | - export VERSION=($(curl --silent "https://gitlab.com/api/v4/projects/4175605/releases" | jq -r '.[0].tag_name')) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - name: Load Cached Conformance Suite Build - uses: actions/cache@v2 - id: cache + run: npm clean-install + - uses: oven-sh/setup-bun@v1 with: - path: ./conformance-suite - key: suite-${{ hashFiles('**/test.yml') }}-${{ env.VERSION }} - - name: Conformance Suite Checkout - if: ${{ steps.cache.outputs.cache-hit != 'true' }} - run: git clone --depth 1 --single-branch --branch ${{ env.VERSION }} https://gitlab.com/openid/conformance-suite.git - - name: Conformance Suite Build - working-directory: ./conformance-suite - if: ${{ steps.cache.outputs.cache-hit != 'true' }} - 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 clean/-B -DskipTests=true/g' builder-compose.yml - docker-compose -f builder-compose.yml run builder + bun-version: latest + - run: bun ./node_modules/.bin/mocha test/**/*.test.js - conformance-suite: + electron: 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 - - run: git clone --depth 1 --single-branch --branch main 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 - - run: npm run test - working-directory: ./runner - - 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 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + id: node with: - if-no-files-found: warn - name: ${{ matrix.setup.plan }} runner logs - path: runner/logs/*.log - if: ${{ failure() }} + node-version: lts/iron # 20 + cache: 'npm' + - name: Install dependencies + run: npm clean-install + - run: npx xvfb-maybe npx electron@latest ./test/electron test/**/*.test.js diff --git a/.gitignore b/.gitignore index ff8e1a32..b97e8b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,6 @@ build/Release node_modules yarn.lock npm-shrinkwrap.json -package-lock.json # Optional npm cache directory .npm diff --git a/.mocharc.yml b/.mocharc.yml deleted file mode 100644 index 3930ec3e..00000000 --- a/.mocharc.yml +++ /dev/null @@ -1,3 +0,0 @@ -retries: 1 -forbid-only: true -forbid-pending: true diff --git a/.release-notes.cjs b/.release-notes.cjs new file mode 100644 index 00000000..63d7bd68 --- /dev/null +++ b/.release-notes.cjs @@ -0,0 +1,20 @@ +const fs = require('node:fs') +const { execSync } = require('node:child_process') + +execSync('git show HEAD -- CHANGELOG.md > CHANGELOG.diff') + +const tag = execSync('git tag --points-at HEAD').toString().trim() + +fs.writeFileSync( + 'notes.md', + fs + .readFileSync('CHANGELOG.diff') + .toString() + .split('\n') + .filter((line) => line.startsWith('+') && !line.startsWith('+++')) + .map((line) => line.slice(1)) + .slice(3) + .join('\n'), +) + +execSync(`gh release create ${tag} -F notes.md --title ${tag} --discussion-category Releases`) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a195ef7..d9405139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,154 @@ 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. +## [5.6.1](https://github.com/panva/node-openid-client/compare/v5.6.0...v5.6.1) (2023-10-11) + + +### Fixes + +* consistent space encoding in authorizationUrl ([#627](https://github.com/panva/node-openid-client/issues/627)) ([ad68223](https://github.com/panva/node-openid-client/commit/ad6822333d713733655865e234290417ea59382b)), closes [#626](https://github.com/panva/node-openid-client/issues/626) + +## [5.6.0](https://github.com/panva/node-openid-client/compare/v5.5.0...v5.6.0) (2023-10-03) + + +### Features + +* experimental Bun support ([a9d3a87](https://github.com/panva/node-openid-client/commit/a9d3a87d2727bb37a535aeac9da9851ffdef8613)), closes [#622](https://github.com/panva/node-openid-client/issues/622) [#623](https://github.com/panva/node-openid-client/issues/623) + +## [5.5.0](https://github.com/panva/node-openid-client/compare/v5.4.3...v5.5.0) (2023-09-08) + + +### Features + +* **DPoP:** remove experimental warning, DPoP is now RFC9449 ([133a022](https://github.com/panva/node-openid-client/commit/133a022cce8e0d7a386b59163c18c100c80df2ab)) + +## [5.4.3](https://github.com/panva/node-openid-client/compare/v5.4.2...v5.4.3) (2023-07-06) + + +### Fixes + +* handle empty client_secret with basic and post client auth ([#610](https://github.com/panva/node-openid-client/issues/610)) ([402c711](https://github.com/panva/node-openid-client/commit/402c711fde93d5644c3b70861c462213bc87ab34)), closes [#609](https://github.com/panva/node-openid-client/issues/609) + +## [5.4.2](https://github.com/panva/node-openid-client/compare/v5.4.1...v5.4.2) (2023-04-25) + + +### Fixes + +* bump oidc-token-hash ([20607e9](https://github.com/panva/node-openid-client/commit/20607e9eb72ea1dee0cfd714d66cd00285686f5f)) + +## [5.4.1](https://github.com/panva/node-openid-client/compare/v5.4.0...v5.4.1) (2023-04-21) + +## [5.4.0](https://github.com/panva/node-openid-client/compare/v5.3.4...v5.4.0) (2023-02-05) + + +### Features + +* allow third party initiated login requests to trigger strategy ([568709a](https://github.com/panva/node-openid-client/commit/568709abc786cc8e2d9c8de1543b0c488c284098)), closes [#510](https://github.com/panva/node-openid-client/issues/510) [#564](https://github.com/panva/node-openid-client/issues/564) + +## [5.3.4](https://github.com/panva/node-openid-client/compare/v5.3.3...v5.3.4) (2023-02-02) + + +### Fixes + +* regression introduced in v5.3.3 ([4f6e847](https://github.com/panva/node-openid-client/commit/4f6e847f126ca531c73d37e1a756ab62f361f86a)) + +## [5.3.3](https://github.com/panva/node-openid-client/compare/v5.3.2...v5.3.3) (2023-02-02) + + +### Refactor + +* remove use of Node.js v8 builtin ([f1881bc](https://github.com/panva/node-openid-client/commit/f1881bc61d424df4576864d610d4840101b45631)), closes [#442](https://github.com/panva/node-openid-client/issues/442) [#475](https://github.com/panva/node-openid-client/issues/475) [#555](https://github.com/panva/node-openid-client/issues/555) + +## [5.3.2](https://github.com/panva/node-openid-client/compare/v5.3.1...v5.3.2) (2023-01-20) + + +### Fixes + +* **passport:** ignore static state and nonce passed to Strategy() ([#556](https://github.com/panva/node-openid-client/issues/556)) ([43daff3](https://github.com/panva/node-openid-client/commit/43daff3d780d10d29e8ac8cd56b94d99aaa37986)) + +## [5.3.1](https://github.com/panva/node-openid-client/compare/v5.3.0...v5.3.1) (2022-11-28) + + +### Fixes + +* **typescript:** requestResource returns a Promise ([#546](https://github.com/panva/node-openid-client/issues/546)) ([8bc9519](https://github.com/panva/node-openid-client/commit/8bc9519d56a9759fedbad2418420f0c5b75f2455)), closes [#488](https://github.com/panva/node-openid-client/issues/488) + +## [5.3.0](https://github.com/panva/node-openid-client/compare/v5.2.1...v5.3.0) (2022-11-09) + + +### Features + +* JARM is now a stable feature ([10e3a37](https://github.com/panva/node-openid-client/commit/10e3a37efe2635c4b21fba30f5646ef7cf2f4b95)) + +## [5.2.1](https://github.com/panva/node-openid-client/compare/v5.2.0...v5.2.1) (2022-10-20) + + +### Fixes + +* **typescript:** add client_id and logout_hint to EndSessionParameters ([b7b5438](https://github.com/panva/node-openid-client/commit/b7b54384421f9f0fe0d9c42cf731d0877d95c256)) + +## [5.2.0](https://github.com/panva/node-openid-client/compare/v5.1.10...v5.2.0) (2022-10-19) + + +### Features + +* add client_id to endSessionUrl query strings ([6fd9350](https://github.com/panva/node-openid-client/commit/6fd93509b73a67693fb073d31308a0bfcae0ce3f)) + + +### Fixes + +* allow endSessionUrl defaults to be overriden ([7cc2402](https://github.com/panva/node-openid-client/commit/7cc240277c30badc7aa7431c31d72feec1237e23)) + +## [5.1.10](https://github.com/panva/node-openid-client/compare/v5.1.9...v5.1.10) (2022-09-28) + + +### Refactor + +* **engines:** remove package.json engines restriction ([9aefba3](https://github.com/panva/node-openid-client/commit/9aefba30dcf0e312051e6844b35b06bc457488d5)) + +## [5.1.9](https://github.com/panva/node-openid-client/compare/v5.1.8...v5.1.9) (2022-08-23) + + +### Fixes + +* safeguard TokenSet prototype methods ([7468674](https://github.com/panva/node-openid-client/commit/74686740ffc7c518bd7564dc7c69eb19f775dab8)), closes [#511](https://github.com/panva/node-openid-client/issues/511) + +## [5.1.8](https://github.com/panva/node-openid-client/compare/v5.1.7...v5.1.8) (2022-07-04) + + +### Fixes + +* ignore non-conform "unrecognized" id_token in oauthCallback() ([3425110](https://github.com/panva/node-openid-client/commit/34251106d142553f8614665c1cbfe94f8ca1e222)), closes [#503](https://github.com/panva/node-openid-client/issues/503) + +## [5.1.7](https://github.com/panva/node-openid-client/compare/v5.1.6...v5.1.7) (2022-06-25) + + +### Fixes + +* improve support of electron BrowserWindow with nodeIntegration ([9e5ea0f](https://github.com/panva/node-openid-client/commit/9e5ea0facee3eec6b16b647c3e891cbb126fc32e)) + +## [5.1.6](https://github.com/panva/node-openid-client/compare/v5.1.5...v5.1.6) (2022-05-10) + + +### Fixes + +* **typescript:** add types export for nodenext module resolution ([92fd33d](https://github.com/panva/node-openid-client/commit/92fd33d4716260ef61fcaaa8de32119c869e70fb)) + +## [5.1.5](https://github.com/panva/node-openid-client/compare/v5.1.4...v5.1.5) (2022-04-14) + + +### Fixes + +* interoperable audience array value for JWT Client auth assertions (again) ([96b367d](https://github.com/panva/node-openid-client/commit/96b367d920f5bf8cd31d805e159625dd1899b65d)) +* **typescript:** add error constructors ([#483](https://github.com/panva/node-openid-client/issues/483)) ([9505cba](https://github.com/panva/node-openid-client/commit/9505cbab42c741a64b5a9b5d586c2c874765adb8)) + +## [5.1.4](https://github.com/panva/node-openid-client/compare/v5.1.3...v5.1.4) (2022-03-04) + + +### Fixes + +* **dpop:** htu without querystring ([f6fa149](https://github.com/panva/node-openid-client/commit/f6fa149d11c2ea5c05b77b4fec6ee668fa7658ac)) + ## [5.1.3](https://github.com/panva/node-openid-client/compare/v5.1.2...v5.1.3) (2022-02-03) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86040ccb..10b0095f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,4 +16,4 @@ ask that you keep the language to English and keep on track with the issue at ha be respectful of our fellow contributors and keep an exemplary level of professionalism at all times. -[coc]: https://github.com/panva/node-openid-client/blob/master/CODE_OF_CONDUCT.md +[coc]: https://github.com/panva/node-openid-client/blob/main/CODE_OF_CONDUCT.md diff --git a/README.md b/README.md index 940b7e19..f4d4bbe6 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ openid-client. - self_signed_tls_client_auth - [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][feature-jar] - [RFC9126 - OAuth 2.0 Pushed Authorization Requests (PAR)][feature-par] -- [OpenID Connect RP-Initiated Logout 1.0 - draft 01][feature-rp-logout] +- [RFC9449 - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)][feature-dpop] +- [OpenID Connect RP-Initiated Logout 1.0][feature-rp-logout] - [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 04][feature-dpop] -- [OAuth 2.0 Authorization Server Issuer Identification - draft-04][feature-iss] +- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)][feature-jarm] +- [OAuth 2.0 Authorization Server Issuer Identification][feature-iss] -Updates to draft specifications (DPoP, JARM, etc) are released as MINOR library versions, +Updates to draft specifications 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. @@ -87,14 +87,17 @@ specific middlewares. Those can however be built using the exposed API, one such ## Install -Node.js LTS releases Codename Erbium (starting with ^12.19.0) and newer LTS releases are supported. -This means ^12.19.0 (Erbium), ^14.15.0 (Fermium), and ^16.13.0 (Gallium). Future LTS releases will -be added to this list as they're released. +Node.js LTS releases Codename Erbium and newer LTS releases are supported. ```console npm install openid-client ``` +Note: Other javascript runtimes are not supported. +I recommend [panva/oauth4webapi][oauth4webapi], or a derivate thereof, if you're +looking for a similarly compliant and certified client software that's not dependent +on the Node.js runtime builtins. + ## Quick start Discover an Issuer configuration using its published .well-known endpoints @@ -254,8 +257,10 @@ private API and is subject to change between any versions. #### How do I use it outside of Node.js -It is **only built for Node.js** environments - including openid-client in -browser-environment targeted projects is not supported. +It is **only built for Node.js**. Other javascript runtimes are not supported. +I recommend [panva/oauth4webapi][oauth4webapi], or a derivate thereof, if you're +looking for a similarly compliant and certified client software that's not dependent +on the Node.js runtime builtins. #### How to make the client send client_id and client_secret in the body? @@ -274,26 +279,27 @@ See [Customizing (docs)][documentation-customizing]. [feature-introspection]: https://tools.ietf.org/html/rfc7662 [feature-mtls]: https://tools.ietf.org/html/rfc8705 [feature-device-flow]: https://tools.ietf.org/html/rfc8628 -[feature-rp-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html -[feature-jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html +[feature-rp-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html +[feature-jarm]: https://openid.net/specs/oauth-v2-jarm.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-04 +[feature-dpop]: https://www.rfc-editor.org/rfc/rfc9449.html [feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html [feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html -[feature-iss]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-04 +[feature-iss]: https://www.rfc-editor.org/rfc/rfc9207.html [openid-certified-link]: https://openid.net/certification/ [passport-url]: http://passportjs.org [npm-url]: https://www.npmjs.com/package/openid-client [sponsor-auth0]: https://a0.to/try-auth0 [support-sponsor]: https://github.com/sponsors/panva -[documentation]: https://github.com/panva/node-openid-client/blob/master/docs/README.md -[documentation-issuer]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#issuer -[documentation-client]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#client -[documentation-customizing]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#customizing -[documentation-tokenset]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#tokenset -[documentation-strategy]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#strategy -[documentation-errors]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#errors -[documentation-generators]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#generators -[documentation-methods]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#client-authentication-methods -[documentation-webfinger]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#issuerwebfingerinput +[documentation]: https://github.com/panva/node-openid-client/blob/main/docs/README.md +[documentation-issuer]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#issuer +[documentation-client]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#client +[documentation-customizing]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#customizing +[documentation-tokenset]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#tokenset +[documentation-strategy]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#strategy +[documentation-errors]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#errors +[documentation-generators]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#generators +[documentation-methods]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#client-authentication-methods +[documentation-webfinger]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#issuerwebfingerinput [express-openid-connect]: https://www.npmjs.com/package/express-openid-connect +[oauth4webapi]: https://github.com/panva/oauth4webapi#readme diff --git a/docs/README.md b/docs/README.md index 7b147d6c..9d6ada90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -242,9 +242,11 @@ parameters. - `parameters`: `` - `id_token_hint`: `` | `` + - `client_id`: `` **Default:** client's client_id - `post_logout_redirect_uri`: `` **Default:** If only a single `client.post_logout_redirect_uris` member is present that one will be used automatically. - `state`: `` + - `logout_hint`: `` - any other end session parameters may be provided - Returns: `` @@ -290,9 +292,6 @@ 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 @@ -300,10 +299,9 @@ Performs the callback for Authorization Server's authorization response. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Parsed token endpoint response as a TokenSet. Tip: If you're using pure @@ -325,10 +323,9 @@ Performs `refresh_token` grant type exchange. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Parsed token endpoint response as a TokenSet. --- @@ -349,10 +346,9 @@ will also be checked to match the on in the TokenSet's ID Token. or the `token_type` property from a passed in TokenSet. - `params`: `` additional parameters to send with the userinfo request (as query string when GET, as x-www-form-urlencoded body when POST). - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Userinfo Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Userinfo Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Parsed userinfo response. --- @@ -370,10 +366,9 @@ Fetches an arbitrary resource with the provided Access Token in an Authorization - `method`: `` The HTTP method to use for the request. **Default:** 'GET' - `tokenType`: `` The token type as the Authorization Header scheme. **Default:** 'Bearer' or the `token_type` property from a passed in TokenSet. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Userinfo Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Userinfo Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Response is a [Got Response](https://github.com/sindresorhus/got/tree/v11.8.0#response) with the `body` property being a `` @@ -391,10 +386,9 @@ Performs an arbitrary `grant_type` exchange at the `token_endpoint`. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` --- @@ -468,10 +462,9 @@ a handle for subsequent Device Access Token Request polling. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` --- @@ -518,7 +511,7 @@ the following are valid values for `token_endpoint_auth_method`. in the request body - `tls_client_auth` and `self_signed_tls_client_auth` - sends client_id in the request body combined with client certificate and key configured via setting `cert` and `key` on a per-request basis - using [`docs#customizing-http-requests`](https://github.com/panva/node-openid-client/tree/master/docs#customizing-http-requests) + using [`docs#customizing-http-requests`](https://github.com/panva/node-openid-client/tree/main/docs#customizing-http-requests) Note: `*_jwt` methods resolve their signature algorithm either via the client's configured alg (`token_endpoint_auth_signing_alg`) or any of the issuer's supported algs @@ -659,7 +652,7 @@ client[custom.http_options] = function (url, options) { #### Customizing clock skew tolerance -It is possible the RP or OP environment has a system clock skew, to set a clock tolerance (in seconds) +It is possible the RP or OP environment has a system clock skew, which can result in the error "JWT not active yet". To set a clock tolerance (in seconds) ```js import { custom } from 'openid-client'; @@ -841,7 +834,7 @@ Creates a new Strategy - `options`: `` - `client`: `` Client instance. The strategy will use it. - - `params`: `` Authorization Request parameters. The strategy will use these. + - `params`: `` Authorization Request parameters. The strategy will use these for every authorization request. - `passReqToCallback`: `` Boolean specifying whether the verify function should get the request object as first argument instead. **Default:** 'false' - `usePKCE`: `` | `` The PKCE method to use. When 'true' it will resolve based @@ -856,6 +849,16 @@ Creates a new Strategy - `done`: `` - Returns: `` +Note: You can also set authorization request parameters dynamically using the `options` argument in `passport.authenticate([options])`: + +```js +app.get('/protected-route', function(req, res, next) { + if (shouldReConsent(req)) { + passport.authenticate('oidc', { prompt: 'consent' })(req, res, next); + } +}); +``` + --- ## generators diff --git a/lib/client.js b/lib/client.js index 09eff623..299b69c7 100644 --- a/lib/client.js +++ b/lib/client.js @@ -4,6 +4,7 @@ const crypto = require('crypto'); const { strict: assert } = require('assert'); const querystring = require('querystring'); const url = require('url'); +const { URL, URLSearchParams } = require('url'); const jose = require('jose'); const tokenHash = require('oidc-token-hash'); @@ -62,6 +63,12 @@ function authorizationHeaderValue(token, tokenType = 'Bearer') { return `${tokenType} ${token}`; } +function getSearchParams(input) { + const parsed = url.parse(input); + if (!parsed.search) return {}; + return querystring.parse(parsed.search.substring(1)); +} + function verifyPresence(payload, jwt, prop) { if (payload[prop] === undefined) { throw new RPError({ @@ -251,13 +258,21 @@ class BaseClient { throw new TypeError('params must be a plain object'); } assertIssuerConfiguration(this.issuer, 'authorization_endpoint'); - const target = url.parse(this.issuer.authorization_endpoint, true); - target.search = null; - target.query = { - ...target.query, - ...authorizationParams.call(this, params), - }; - return url.format(target); + const target = new URL(this.issuer.authorization_endpoint); + + for (const [name, value] of Object.entries(authorizationParams.call(this, params))) { + if (Array.isArray(value)) { + target.searchParams.delete(name); + for (const member of value) { + target.searchParams.append(name, member); + } + } else { + target.searchParams.set(name, value); + } + } + + // TODO: is the replace needed? + return target.href.replace(/\+/g, '%20'); } authorizationPost(params = {}) { @@ -288,31 +303,35 @@ class BaseClient { const { post_logout_redirect_uri = length === 1 ? postLogout : undefined } = params; - let hint = params.id_token_hint; - if (hint instanceof TokenSet) { - if (!hint.id_token) { + let id_token_hint; + ({ id_token_hint, ...params } = params); + if (id_token_hint instanceof TokenSet) { + if (!id_token_hint.id_token) { throw new TypeError('id_token not present in TokenSet'); } - hint = hint.id_token; + id_token_hint = id_token_hint.id_token; } - const target = url.parse(this.issuer.end_session_endpoint, true); - target.search = null; - target.query = { - ...params, - ...target.query, - ...{ + const target = url.parse(this.issuer.end_session_endpoint); + const query = defaults( + getSearchParams(this.issuer.end_session_endpoint), + params, + { post_logout_redirect_uri, - id_token_hint: hint, + client_id: this.client_id, }, - }; + { id_token_hint }, + ); - Object.entries(target.query).forEach(([key, value]) => { + Object.entries(query).forEach(([key, value]) => { if (value === null || value === undefined) { - delete target.query[key]; + delete query[key]; } }); + target.search = null; + target.query = query; + return url.format(target); } @@ -329,7 +348,7 @@ class BaseClient { if (isIncomingMessage) { switch (input.method) { case 'GET': - return pickCb(url.parse(input.url, true).query); + return pickCb(getSearchParams(input.url)); case 'POST': if (input.body === undefined) { throw new TypeError( @@ -354,7 +373,7 @@ class BaseClient { throw new TypeError('invalid IncomingMessage method'); } } else { - return pickCb(url.parse(input, true).query); + return pickCb(getSearchParams(input)); } } @@ -489,18 +508,6 @@ class BaseClient { 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; } @@ -569,13 +576,14 @@ class BaseClient { throw new OPError(params); } - if ('id_token' in params) { + if (typeof params.id_token === 'string' && params.id_token.length) { throw new RPError({ message: 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()', params, }); } + delete params.id_token; const RESPONSE_TYPE_REQUIRED_PARAMS = { code: ['code'], @@ -620,25 +628,14 @@ class BaseClient { { clientAssertionPayload, DPoP }, ); - if ('id_token' in tokenset) { + if (typeof tokenset.id_token === 'string' && tokenset.id_token.length) { throw new RPError({ message: 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()', params, }); } - - 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, - }); - } - } + delete tokenset.id_token; return tokenset; } @@ -723,11 +720,15 @@ class BaseClient { if (expectedAlg.match(/^(?:RSA|ECDH)/)) { const keystore = await keystores.get(this); - for (const { keyObject: key } of keystore.all({ - ...jose.decodeProtectedHeader(jwe), + const protectedHeader = jose.decodeProtectedHeader(jwe); + + for (const key of keystore.all({ + ...protectedHeader, use: 'enc', })) { - plaintext = await jose.compactDecrypt(jwe, key).then(getPlaintext, () => {}); + plaintext = await jose + .compactDecrypt(jwe, await key.keyObject(protectedHeader.alg)) + .then(getPlaintext, () => {}); if (plaintext) break; } } else { @@ -779,7 +780,10 @@ class BaseClient { } } - if (typeof maxAge === 'number' && payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE]) { + if ( + typeof maxAge === 'number' && + payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE] + ) { throw new RPError({ printf: [ 'too much time has elapsed since the last End-User authentication, max_age %i, auth_time: %i, now %i', @@ -794,7 +798,11 @@ class BaseClient { }); } - if (nonce !== skipNonceCheck && (payload.nonce || nonce !== undefined) && payload.nonce !== nonce) { + if ( + nonce !== skipNonceCheck && + (payload.nonce || nonce !== undefined) && + payload.nonce !== nonce + ) { throw new RPError({ printf: ['nonce mismatch, expected %s, got: %s', nonce, payload.nonce], jwt: idToken, @@ -1035,7 +1043,13 @@ class BaseClient { assert(isPlainObject(payload.sub_jwk)); const key = await jose.importJWK(payload.sub_jwk, header.alg); assert.equal(key.type, 'public'); - keys = [{ keyObject: key }]; + keys = [ + { + keyObject() { + return key; + }, + }, + ]; } catch (err) { throw new RPError({ message: 'failed to use sub_jwk claim as an asymmetric JSON Web Key', @@ -1060,7 +1074,7 @@ class BaseClient { for (const key of keys) { const verified = await jose - .compactVerify(jwt, key instanceof Uint8Array ? key : key.keyObject) + .compactVerify(jwt, key instanceof Uint8Array ? key : await key.keyObject(header.alg)) .catch(() => {}); if (verified) { return { @@ -1214,12 +1228,12 @@ class BaseClient { targetUrl = this.issuer.mtls_endpoint_aliases.userinfo_endpoint; } - targetUrl = new url.URL(targetUrl || this.issuer.userinfo_endpoint); + targetUrl = new URL(targetUrl || this.issuer.userinfo_endpoint); if (via === 'body') { options.headers.Authorization = undefined; options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - options.body = new url.URLSearchParams(); + options.body = new URLSearchParams(); options.body.append( 'access_token', accessToken instanceof TokenSet ? accessToken.access_token : accessToken, @@ -1239,7 +1253,7 @@ class BaseClient { }); } else { // POST && via header - options.body = new url.URLSearchParams(); + options.body = new URLSearchParams(); options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; Object.entries(params).forEach(([key, value]) => { options.body.append(key, value); @@ -1534,7 +1548,7 @@ class BaseClient { ...header, kid: symmetric ? undefined : key.jwk.kid, }) - .sign(symmetric ? key : key.keyObject); + .sign(symmetric ? key : await key.keyObject(signingAlgorithm)); } if (!eKeyManagement) { @@ -1558,7 +1572,7 @@ class BaseClient { ...fields, kid: key instanceof Uint8Array ? undefined : key.jwk.kid, }) - .encrypt(key instanceof Uint8Array ? key : key.keyObject); + .encrypt(key instanceof Uint8Array ? key : await key.keyObject(fields.alg)); } async pushedAuthorizationRequest(params = {}, { clientAssertionPayload } = {}) { @@ -1625,110 +1639,191 @@ class BaseClient { fapi() { return this.constructor.name === 'FAPI1Client'; } -} -/** - * @name validateJARM - * @api private - */ -async function validateJARM(response) { - const expectedAlg = this.authorization_signed_response_alg; - const { payload } = await this.validateJWT(response, expectedAlg, ['iss', 'exp', 'aud']); - return pickCb(payload); -} + async validateJARM(response) { + const expectedAlg = this.authorization_signed_response_alg; + const { payload } = await this.validateJWT(response, expectedAlg, ['iss', 'exp', 'aud']); + return pickCb(payload); + } -Object.defineProperty(BaseClient.prototype, 'validateJARM', { - enumerable: true, - configurable: true, - value(...args) { - process.emitWarning( - "The JARM API implements an OIDF implementer's draft. Breaking draft implementations are included as minor versions of the openid-client library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.", - 'DraftWarning', - ); - Object.defineProperty(BaseClient.prototype, 'validateJARM', { - enumerable: true, - configurable: true, - value: validateJARM, - }); - return this.validateJARM(...args); - }, -}); + /** + * @name dpopProof + * @api private + */ + async dpopProof(payload, privateKeyInput, accessToken) { + if (!isPlainObject(payload)) { + throw new TypeError('payload must be a plain object'); + } + + let privateKey; + if (isKeyObject(privateKeyInput)) { + privateKey = privateKeyInput; + } else if (privateKeyInput[Symbol.toStringTag] === 'CryptoKey') { + privateKey = privateKeyInput; + } else if (jose.cryptoRuntime === 'node:crypto') { + privateKey = crypto.createPrivateKey(privateKeyInput); + } else { + throw new TypeError('unrecognized crypto runtime'); + } -const RSPS = /^(?:RS|PS)(?:256|384|512)$/; -function determineRsaAlgorithm(privateKey, privateKeyInput, valuesSupported) { - if ( - typeof privateKeyInput === 'object' && - typeof privateKeyInput.key === 'object' && - privateKeyInput.key.alg - ) { - return privateKeyInput.key.alg; - } + if (privateKey.type !== 'private') { + throw new TypeError('"DPoP" option must be a private key'); + } + let alg = determineDPoPAlgorithm.call(this, privateKey, privateKeyInput); - if (Array.isArray(valuesSupported)) { - let candidates = valuesSupported.filter(RegExp.prototype.test.bind(RSPS)); - if (privateKey.asymmetricKeyType === 'rsa-pss') { - candidates = candidates.filter((value) => value.startsWith('PS')); + if (!alg) { + throw new TypeError('could not determine DPoP JWS Algorithm'); } - return ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS384'].find((preferred) => - candidates.includes(preferred), - ); + + return new jose.SignJWT({ + ath: accessToken + ? base64url.encode(crypto.createHash('sha256').update(accessToken).digest()) + : undefined, + ...payload, + }) + .setProtectedHeader({ + alg, + typ: 'dpop+jwt', + jwk: await getJwk(privateKey, privateKeyInput), + }) + .setIssuedAt() + .setJti(random()) + .sign(privateKey); } +} - return 'PS256'; +function determineDPoPAlgorithmFromCryptoKey(cryptoKey) { + switch (cryptoKey.algorithm.name) { + case 'Ed25519': + case 'Ed448': + return 'EdDSA'; + case 'ECDSA': { + switch (cryptoKey.algorithm.namedCurve) { + case 'P-256': + return 'ES256'; + case 'P-384': + return 'ES384'; + case 'P-521': + return 'ES512'; + default: + break; + } + break; + } + case 'RSASSA-PKCS1-v1_5': + return `RS${cryptoKey.algorithm.hash.name.slice(4)}`; + case 'RSA-PSS': + return `PS${cryptoKey.algorithm.hash.name.slice(4)}`; + default: + throw new TypeError('unsupported DPoP private key'); + } } -const p256 = Buffer.from([42, 134, 72, 206, 61, 3, 1, 7]); -const p384 = Buffer.from([43, 129, 4, 0, 34]); -const p521 = Buffer.from([43, 129, 4, 0, 35]); -const secp256k1 = Buffer.from([43, 129, 4, 0, 10]); +let determineDPoPAlgorithm; +if (jose.cryptoRuntime === 'node:crypto') { + determineDPoPAlgorithm = function (privateKey, privateKeyInput) { + if (privateKeyInput[Symbol.toStringTag] === 'CryptoKey') { + return determineDPoPAlgorithmFromCryptoKey(privateKey); + } + + switch (privateKey.asymmetricKeyType) { + case 'ed25519': + case 'ed448': + return 'EdDSA'; + case 'ec': + return determineEcAlgorithm(privateKey, privateKeyInput); + case 'rsa': + case rsaPssParams && 'rsa-pss': + return determineRsaAlgorithm( + privateKey, + privateKeyInput, + this.issuer.dpop_signing_alg_values_supported, + ); + default: + throw new TypeError('unsupported DPoP private key'); + } + }; -function determineEcAlgorithm(privateKey, privateKeyInput) { - // If input was a JWK - switch ( - typeof privateKeyInput === 'object' && - typeof privateKeyInput.key === 'object' && - privateKeyInput.key.crv - ) { - case 'P-256': + const RSPS = /^(?:RS|PS)(?:256|384|512)$/; + function determineRsaAlgorithm(privateKey, privateKeyInput, valuesSupported) { + if ( + typeof privateKeyInput === 'object' && + privateKeyInput.format === 'jwk' && + privateKeyInput.key && + privateKeyInput.key.alg + ) { + return privateKeyInput.key.alg; + } + + if (Array.isArray(valuesSupported)) { + let candidates = valuesSupported.filter(RegExp.prototype.test.bind(RSPS)); + if (privateKey.asymmetricKeyType === 'rsa-pss') { + candidates = candidates.filter((value) => value.startsWith('PS')); + } + return ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS384'].find((preferred) => + candidates.includes(preferred), + ); + } + + return 'PS256'; + } + + const p256 = Buffer.from([42, 134, 72, 206, 61, 3, 1, 7]); + const p384 = Buffer.from([43, 129, 4, 0, 34]); + const p521 = Buffer.from([43, 129, 4, 0, 35]); + const secp256k1 = Buffer.from([43, 129, 4, 0, 10]); + + function determineEcAlgorithm(privateKey, privateKeyInput) { + // If input was a JWK + switch ( + typeof privateKeyInput === 'object' && + typeof privateKeyInput.key === 'object' && + privateKeyInput.key.crv + ) { + case 'P-256': + return 'ES256'; + case 'secp256k1': + return 'ES256K'; + case 'P-384': + return 'ES384'; + case 'P-512': + return 'ES512'; + default: + break; + } + + const buf = privateKey.export({ format: 'der', type: 'pkcs8' }); + const i = buf[1] < 128 ? 17 : 18; + const len = buf[i]; + const curveOid = buf.slice(i + 1, i + 1 + len); + if (curveOid.equals(p256)) { return 'ES256'; - case 'secp256k1': - return 'ES256K'; - case 'P-384': + } + + if (curveOid.equals(p384)) { return 'ES384'; - case 'P-512': + } + if (curveOid.equals(p521)) { return 'ES512'; - default: - break; - } - - const buf = privateKey.export({ format: 'der', type: 'pkcs8' }); - const i = buf[1] < 128 ? 17 : 18; - const len = buf[i]; - const curveOid = buf.slice(i + 1, i + 1 + len); - if (curveOid.equals(p256)) { - return 'ES256'; - } + } - if (curveOid.equals(p384)) { - return 'ES384'; - } - if (curveOid.equals(p521)) { - return 'ES512'; - } + if (curveOid.equals(secp256k1)) { + return 'ES256K'; + } - if (curveOid.equals(secp256k1)) { - return 'ES256K'; + throw new TypeError('unsupported DPoP private key curve'); } - - throw new TypeError('unsupported DPoP private key curve'); +} else { + determineDPoPAlgorithm = determineDPoPAlgorithmFromCryptoKey; } const jwkCache = new WeakMap(); -async function getJwk(privateKey, privateKeyInput) { +async function getJwk(keyObject, privateKeyInput) { if ( + jose.cryptoRuntime === 'node:crypto' && typeof privateKeyInput === 'object' && typeof privateKeyInput.key === 'object' && - privateKeyInput.key.crv + privateKeyInput.format === 'jwk' ) { return pick(privateKeyInput.key, 'kty', 'crv', 'x', 'y', 'e', 'n'); } @@ -1737,92 +1832,15 @@ async function getJwk(privateKey, privateKeyInput) { return jwkCache.get(privateKeyInput); } - const jwk = pick(await jose.exportJWK(privateKey), 'kty', 'crv', 'x', 'y', 'e', 'n'); + const jwk = pick(await jose.exportJWK(keyObject), 'kty', 'crv', 'x', 'y', 'e', 'n'); - if (isKeyObject(privateKeyInput)) { + if (isKeyObject(privateKeyInput) || jose.cryptoRuntime === 'WebCryptoAPI') { jwkCache.set(privateKeyInput, jwk); } return jwk; } -/** - * @name dpopProof - * @api private - */ -async function dpopProof(payload, privateKeyInput, accessToken) { - if (!isPlainObject(payload)) { - throw new TypeError('payload must be a plain object'); - } - - let privateKey; - if (isKeyObject(privateKeyInput)) { - privateKey = privateKeyInput; - } else { - privateKey = crypto.createPrivateKey(privateKeyInput); - } - - if (privateKey.type !== 'private') { - throw new TypeError('"DPoP" option must be a private key'); - } - let alg; - switch (privateKey.asymmetricKeyType) { - case 'ed25519': - case 'ed448': - alg = 'EdDSA'; - break; - case 'ec': - alg = determineEcAlgorithm(privateKey, privateKeyInput); - break; - case 'rsa': - case rsaPssParams && 'rsa-pss': - alg = determineRsaAlgorithm( - privateKey, - privateKeyInput, - this.issuer.dpop_signing_alg_values_supported, - ); - break; - default: - throw new TypeError('unsupported DPoP private key asymmetric key type'); - } - - if (!alg) { - throw new TypeError('could not determine DPoP JWS Algorithm'); - } - - return new jose.SignJWT({ - ath: accessToken - ? base64url.encode(crypto.createHash('sha256').update(accessToken).digest()) - : undefined, - ...payload, - }) - .setProtectedHeader({ - alg, - typ: 'dpop+jwt', - jwk: await getJwk(privateKey, privateKeyInput), - }) - .setIssuedAt() - .setJti(random()) - .sign(privateKey); -} - -Object.defineProperty(BaseClient.prototype, 'dpopProof', { - enumerable: true, - configurable: true, - value(...args) { - process.emitWarning( - 'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-04.html). Breaking draft implementations are included as minor versions of the openid-client library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.', - 'DraftWarning', - ); - Object.defineProperty(BaseClient.prototype, 'dpopProof', { - enumerable: true, - configurable: true, - value: dpopProof, - }); - return this.dpopProof(...args); - }, -}); - module.exports = (issuer, aadIssValidation = false) => class Client extends BaseClient { constructor(...args) { @@ -1833,4 +1851,5 @@ module.exports = (issuer, aadIssValidation = false) => return issuer; } }; + module.exports.BaseClient = BaseClient; diff --git a/lib/device_flow_handle.js b/lib/device_flow_handle.js index 7277ded1..ed61c7c0 100644 --- a/lib/device_flow_handle.js +++ b/lib/device_flow_handle.js @@ -2,9 +2,6 @@ const { inspect } = require('util'); const { RPError, OPError } = require('./errors'); const now = require('./helpers/unix_timestamp'); -const { authenticatedPost } = require('./helpers/client'); -const processResponse = require('./helpers/process_response'); -const TokenSet = require('./token_set'); class DeviceFlowHandle { #aborted; @@ -61,23 +58,16 @@ class DeviceFlowHandle { await new Promise((resolve) => setTimeout(resolve, this.#interval)); - const response = await authenticatedPost.call( - this.#client, - 'token', - { - form: { + let tokenset; + try { + tokenset = await this.#client.grant( + { ...this.#exchangeBody, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: this.device_code, }, - responseType: 'json', - }, - { clientAssertionPayload: this.#clientAssertionPayload, DPoP: this.#DPoP }, - ); - - let responseBody; - try { - responseBody = processResponse(response); + { clientAssertionPayload: this.#clientAssertionPayload, DPoP: this.#DPoP }, + ); } catch (err) { switch (err instanceof OPError && err.error) { case 'slow_down': @@ -89,8 +79,6 @@ class DeviceFlowHandle { } } - const tokenset = new TokenSet(responseBody); - if ('id_token' in tokenset) { await this.#client.decryptIdToken(tokenset); await this.#client.validateIdToken(tokenset, undefined, 'token', this.#maxAge); diff --git a/lib/helpers/client.js b/lib/helpers/client.js index 529f75d0..8c2f7fc7 100644 --- a/lib/helpers/client.js +++ b/lib/helpers/client.js @@ -70,7 +70,7 @@ async function clientAssertion(endpoint, payload) { return new jose.CompactSign(Buffer.from(JSON.stringify(payload))) .setProtectedHeader({ alg, kid: key.jwk && key.jwk.kid }) - .sign(key.keyObject); + .sign(await key.keyObject(alg)); } async function authFor(endpoint, { clientAssertionPayload } = {}) { @@ -81,7 +81,7 @@ async function authFor(endpoint, { clientAssertionPayload } = {}) { case 'none': return { form: { client_id: this.client_id } }; case 'client_secret_post': - if (!this.client_secret) { + if (typeof this.client_secret !== 'string') { throw new TypeError( 'client_secret_post client authentication method requires a client_secret', ); @@ -90,19 +90,8 @@ async function authFor(endpoint, { clientAssertionPayload } = {}) { case 'private_key_jwt': case 'client_secret_jwt': { const timestamp = now(); - - const mTLS = endpoint === 'token' && this.tls_client_certificate_bound_access_tokens; const audience = [ - ...new Set( - [ - this.issuer.issuer, - this.issuer.token_endpoint, - this.issuer[`${endpoint}_endpoint`], - mTLS && this.issuer.mtls_endpoint_aliases - ? this.issuer.mtls_endpoint_aliases.token_endpoint - : undefined, - ].filter(Boolean), - ), + ...new Set([this.issuer.issuer, this.issuer.token_endpoint].filter(Boolean)), ]; const assertion = await clientAssertion.call(this, endpoint, { @@ -131,7 +120,7 @@ async function authFor(endpoint, { clientAssertionPayload } = {}) { // > Appendix B, and the encoded value is used as the username; the client // > password is encoded using the same algorithm and used as the // > password. - if (!this.client_secret) { + if (typeof this.client_secret !== 'string') { throw new TypeError( 'client_secret_basic client authentication method requires a client_secret', ); diff --git a/lib/helpers/deep_clone.js b/lib/helpers/deep_clone.js index 4ad037bb..0ec848a2 100644 --- a/lib/helpers/deep_clone.js +++ b/lib/helpers/deep_clone.js @@ -1,3 +1 @@ -const { serialize, deserialize } = require('v8'); - -module.exports = globalThis.structuredClone || ((obj) => deserialize(serialize(obj))); +module.exports = globalThis.structuredClone || ((obj) => JSON.parse(JSON.stringify(obj))); diff --git a/lib/helpers/keystore.js b/lib/helpers/keystore.js index e9dad799..6118430f 100644 --- a/lib/helpers/keystore.js +++ b/lib/helpers/keystore.js @@ -1,11 +1,7 @@ -const v8 = require('v8'); - const jose = require('jose'); -const clone = globalThis.structuredClone || ((value) => v8.deserialize(v8.serialize(value))); - +const clone = require('./deep_clone'); const isPlainObject = require('./is_plain_object'); -const isKeyObject = require('./is_key_object'); const internal = Symbol(); @@ -54,7 +50,7 @@ function getKtyFromAlg(alg) { function getAlgorithms(use, alg, kty, crv) { // Ed25519, Ed448, and secp256k1 always have "alg" - // OKP always has use + // OKP always has "use" if (alg) { return new Set([alg]); } @@ -68,7 +64,20 @@ function getAlgorithms(use, alg, kty, crv) { } if (use === 'sig' || use === undefined) { - algs = algs.concat([`ES${crv.slice(-3)}`.replace('21', '12')]); + switch (crv) { + case 'P-256': + case 'P-384': + algs = algs.concat([`ES${crv.slice(-3)}`.replace('21', '12')]); + break; + case 'P-521': + algs = algs.concat(['ES512']); + break; + case 'secp256k1': + if (jose.cryptoRuntime === 'node:crypto') { + algs = algs.concat(['ES256K']); + } + break; + } } return new Set(algs); @@ -80,7 +89,10 @@ function getAlgorithms(use, alg, kty, crv) { let algs = []; if (use === 'enc' || use === undefined) { - algs = algs.concat(['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5']); + algs = algs.concat(['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512']); + if (jose.cryptoRuntime === 'node:crypto') { + algs = algs.concat(['RSA1_5']); + } } if (use === 'sig' || use === undefined) { @@ -228,36 +240,25 @@ module.exports = class KeyStore { } } - const keyObject = await jose.importJWK(jwk, alg || fauxAlg(jwk.kty)).catch(() => {}); - - if (!keyObject) continue; - - if (keyObject instanceof Uint8Array || keyObject.type === 'secret') { - if (onlyPrivate) { - throw new Error('jwks must only contain private keys'); - } - continue; - } - - if (!isKeyObject(keyObject)) { - throw new Error('what?!'); - } - - if (onlyPrivate && keyObject.type !== 'private') { + if (onlyPrivate && (jwk.kty === 'oct' || !jwk.d)) { throw new Error('jwks must only contain private keys'); } - if (onlyPublic && keyObject.type !== 'public') { - continue; - } - - if (kty === 'RSA' && keyObject.asymmetricKeySize < 2048) { + if (onlyPublic && (jwk.d || jwk.k)) { continue; } keys.push({ jwk: { ...jwk, alg, use }, - keyObject, + async keyObject(alg) { + if (this[alg]) { + return this[alg]; + } + + const keyObject = await jose.importJWK(this.jwk, alg); + this[alg] = keyObject; + return keyObject; + }, get algorithms() { Object.defineProperty(this, 'algorithms', { value: getAlgorithms(this.jwk.use, this.jwk.alg, this.jwk.kty, this.jwk.crv), diff --git a/lib/helpers/request.js b/lib/helpers/request.js index ad86809a..45e4f881 100644 --- a/lib/helpers/request.js +++ b/lib/helpers/request.js @@ -3,6 +3,7 @@ const querystring = require('querystring'); const http = require('http'); const https = require('https'); const { once } = require('events'); +const { URL } = require('url'); const LRU = require('lru-cache'); @@ -74,7 +75,7 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP opts.headers = opts.headers || {}; opts.headers.DPoP = await this.dpopProof( { - htu: url.href, + htu: `${url.origin}${url.pathname}`, htm: options.method, nonce: nonces.get(nonceKey), }, @@ -116,7 +117,7 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP } let response; - const req = (url.protocol === 'https:' ? https.request : http.request)(url, opts); + const req = (url.protocol === 'https:' ? https.request : http.request)(url.href, opts); return (async () => { if (json) { send(req, JSON.stringify(json), 'application/json'); diff --git a/lib/passport_strategy.js b/lib/passport_strategy.js index ce3cf352..24b92e34 100644 --- a/lib/passport_strategy.js +++ b/lib/passport_strategy.js @@ -42,6 +42,11 @@ function OpenIDConnectStrategy( this._usePKCE = usePKCE; this._key = sessionKey || `oidc:${url.parse(this._issuer.issuer).hostname}`; this._params = cloneDeep(params); + + // state and nonce are handled in authenticate() + delete this._params.state; + delete this._params.nonce; + this._extras = cloneDeep(extras); if (!this._params.response_type) this._params.response_type = resolveResponseType.call(client); @@ -81,8 +86,14 @@ OpenIDConnectStrategy.prototype.authenticate = async function authenticate(req, const sessionKey = this._key; const session = Session.wrap(req.session); - /* start authentication request */ - if (Object.keys(reqParams).length === 0) { + const { 0: parameter, length } = Object.keys(reqParams); + + /** + * Start authentication request if this has no authorization response parameters or + * this might a login initiated from a third party as per + * https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. + */ + if (length === 0 || (length === 1 && parameter === 'iss')) { // provide options object with extra authentication parameters const params = { state: random(), diff --git a/lib/token_set.js b/lib/token_set.js index cfdb3ce9..cdf2f0d8 100644 --- a/lib/token_set.js +++ b/lib/token_set.js @@ -4,6 +4,11 @@ const now = require('./helpers/unix_timestamp'); class TokenSet { constructor(values) { Object.assign(this, values); + const { constructor, ...properties } = Object.getOwnPropertyDescriptors( + this.constructor.prototype, + ); + + Object.defineProperties(this, properties); } set expires_in(value) { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..902aed01 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1392 @@ +{ + "name": "openid-client", + "version": "5.6.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openid-client", + "version": "5.6.1", + "license": "MIT", + "dependencies": { + "jose": "^4.15.4", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^16.18.59", + "@types/passport": "^1.0.14", + "base64url": "^3.0.1", + "chai": "^4.3.10", + "mocha": "^10.2.0", + "nock": "^13.3.6", + "prettier": "^2.8.8", + "readable-mock-req": "^0.2.2", + "sinon": "^9.2.4", + "timekeeper": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", + "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.37", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", + "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", + "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.39", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz", + "integrity": "sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", + "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", + "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.59.tgz", + "integrity": "sha512-PJ1w2cNeKUEdey4LiPra0ZuxZFOGvetswE8qHRriV/sUkL5Al4tTmPV9D2+Y/TPIxTHHgxTfRjZVKWhPw/ORhQ==", + "dev": true + }, + "node_modules/@types/passport": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.14.tgz", + "integrity": "sha512-D6p2ygR2S7Cq5PO7iUaEIQu/5WrM0tONu6Lxgk0C9r3lafQIlVpWCo3V/KI9To3OqHBxcfQaOeK+8AvwW5RYmw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.9", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", + "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", + "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", + "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", + "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-readable-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-readable-stream/-/is-readable-stream-1.0.1.tgz", + "integrity": "sha512-hj/cPKXI7ITRRFeaHPg9WwEzye4cj+xjcUnvelBuCcBsyXYzYXi//Yh+oSVn8bMiyt1b8cNOPwkYxEJXl+6v7w==", + "deprecated": "use is-stream instead", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nise": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nock": { + "version": "13.3.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.6.tgz", + "integrity": "sha512-lT6YuktKroUFM+27mubf2uqQZVy2Jf+pfGzuh9N6VwdHlFoZqvi4zyxFTVR1w/ChPqGY6yxGehHp6C3wqCASCw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-mock-req": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/readable-mock-req/-/readable-mock-req-0.2.2.tgz", + "integrity": "sha512-SQwHpJeeCsmtZ8mZmi4aoS3ujArujrQp7QMqt4t+O4f9sBztMv0V9Oufdc1vjzU7MMCc9TLOj7tYKoRE/QHMMg==", + "dev": true, + "dependencies": { + "is-readable-stream": "~1.0.0", + "methods": "~1.1.1", + "readable-stream": "~1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/sinon": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "deprecated": "16.1.1", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.1", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/timekeeper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index a7b66e61..0c12c561 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openid-client", - "version": "5.1.6", + "version": "5.6.1", "description": "OpenID Connect Relying Party (RP, Client) implementation for Node.js runtime, supports passportjs", "keywords": [ "auth", @@ -30,48 +30,37 @@ "license": "MIT", "author": "Filip Skokan ", "exports": { + "types": "./types/index.d.ts", "import": "./lib/index.mjs", "require": "./lib/index.js" }, - "main": "lib/index.js", - "types": "types/index.d.ts", + "main": "./lib/index.js", + "types": "./types/index.d.ts", "files": [ "lib", "types/index.d.ts" ], "scripts": { - "coverage": "nyc mocha test/**/*.test.js", - "prettier": "npx prettier --loglevel silent --write ./lib ./test ./certification ./types", + "format": "npx prettier --loglevel silent --write ./lib ./test ./certification ./types", "test": "mocha test/**/*.test.js" }, - "nyc": { - "reporter": [ - "lcov", - "text-summary" - ] - }, "dependencies": { - "jose": "^4.1.4", + "jose": "^4.15.4", "lru-cache": "^6.0.0", - "object-hash": "^2.0.1", - "oidc-token-hash": "^5.0.1" + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" }, "devDependencies": { - "@types/node": "^16.11.5", - "@types/passport": "^1.0.7", + "@types/node": "^16.18.59", + "@types/passport": "^1.0.14", "base64url": "^3.0.1", - "chai": "^4.2.0", - "jose2": "npm:jose@^2.0.5", - "mocha": "^8.2.0", - "nock": "^13.0.2", - "nyc": "^15.1.0", - "prettier": "^2.4.1", + "chai": "^4.3.10", + "mocha": "^10.2.0", + "nock": "^13.3.6", + "prettier": "^2.8.8", "readable-mock-req": "^0.2.2", - "sinon": "^9.2.0", - "timekeeper": "^2.2.0" - }, - "engines": { - "node": "^12.19.0 || ^14.15.0 || ^16.13.0" + "sinon": "^9.2.4", + "timekeeper": "^2.3.1" }, "standard-version": { "scripts": { @@ -101,7 +90,7 @@ { "type": "refactor", "section": "Refactor", - "hidden": true + "hidden": false }, { "type": "perf", diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index 459c7218..8466721e 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -8,7 +8,7 @@ const { expect } = require('chai'); const base64url = require('base64url'); const nock = require('nock'); const sinon = require('sinon'); -const jose2 = require('jose2'); +const jose = require('jose'); const timekeeper = require('timekeeper'); const TokenSet = require('../../lib/token_set'); @@ -17,12 +17,19 @@ const now = require('../../lib/helpers/unix_timestamp'); const { Issuer, custom } = require('../../lib'); const clientInternal = require('../../lib/helpers/client'); const issuerInternal = require('../../lib/helpers/issuer'); +const KeyStore = require('../keystore'); const fail = () => { throw new Error('expected promise to be rejected'); }; const encode = (object) => base64url.encode(JSON.stringify(object)); +function getSearchParams(input) { + const parsed = url.parse(input); + if (!parsed.search) return {}; + return querystring.parse(parsed.search.substring(1)); +} + describe('Client', () => { afterEach(timekeeper.reset); afterEach(nock.cleanAll); @@ -57,12 +64,11 @@ describe('Client', () => { it('returns a string with the url with some basic defaults', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ redirect_uri: 'https://rp.example.com/cb', }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', redirect_uri: 'https://rp.example.com/cb', @@ -73,12 +79,11 @@ describe('Client', () => { it('returns a string with the url and client meta specific defaults', function () { expect( - url.parse( + getSearchParams( this.clientWithMeta.authorizationUrl({ nonce: 'foo', }), - true, - ).query, + ), ).to.eql({ nonce: 'foo', client_id: 'identifier', @@ -89,7 +94,7 @@ describe('Client', () => { }); it('returns a string with the url and no defaults if client has more metas', function () { - expect(url.parse(this.clientWithMultipleMetas.authorizationUrl(), true).query).to.eql({ + expect(getSearchParams(this.clientWithMultipleMetas.authorizationUrl())).to.eql({ client_id: 'identifier', scope: 'openid', }); @@ -97,12 +102,11 @@ describe('Client', () => { it('keeps original query parameters', function () { expect( - url.parse( + getSearchParams( this.clientWithQuery.authorizationUrl({ redirect_uri: 'https://rp.example.com/cb', }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', redirect_uri: 'https://rp.example.com/cb', @@ -114,15 +118,14 @@ describe('Client', () => { it('allows to overwrite the defaults', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ scope: 'openid offline_access', redirect_uri: 'https://rp.example.com/cb', response_type: 'id_token', nonce: 'foobar', }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', scope: 'openid offline_access', @@ -134,13 +137,12 @@ describe('Client', () => { it('allows any other params to be provide too', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ state: 'state', custom: 'property', }), - true, - ).query, + ), ).to.contain({ state: 'state', custom: 'property', @@ -149,12 +151,11 @@ describe('Client', () => { it('allows resource to passed as an array', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ resource: ['urn:example:com', 'urn:example-2:com'], }), - true, - ).query, + ), ).to.deep.contain({ resource: ['urn:example:com', 'urn:example-2:com'], }); @@ -162,12 +163,11 @@ describe('Client', () => { it('auto-stringifies claims parameter', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ claims: { id_token: { email: null } }, }), - true, - ).query, + ), ).to.contain({ claims: '{"id_token":{"email":null}}', }); @@ -175,25 +175,23 @@ describe('Client', () => { it('removes null and undefined values', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ state: null, prompt: undefined, }), - true, - ).query, + ), ).not.to.have.keys('state', 'prompt'); }); it('stringifies other values', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ max_age: 300, foo: true, }), - true, - ).query, + ), ).to.contain({ max_age: '300', foo: 'true', @@ -205,6 +203,16 @@ describe('Client', () => { this.client.authorizationUrl(true); }).to.throw(TypeError, 'params must be a plain object'); }); + + it('returns a space-delimited scope parameter', function () { + expect( + this.client.authorizationUrl({ + state: 'state', + scope: 'openid profile email', + }), + ).to.eql('https://op.example.com/auth?client_id=identifier&scope=openid%20profile%20email&response_type=code&state=state'); + }); + }); describe('#endSessionUrl', function () { @@ -241,15 +249,18 @@ describe('Client', () => { }).to.throw('end_session_endpoint must be configured on the issuer'); }); - it('returns the end_session_endpoint only if nothing is passed', function () { - expect(this.client.endSessionUrl()).to.eql('https://op.example.com/session/end'); + it('returns the end_session_endpoint with client_id if nothing is passed', function () { + expect(this.client.endSessionUrl()).to.eql( + 'https://op.example.com/session/end?client_id=identifier', + ); expect(this.clientWithQuery.endSessionUrl()).to.eql( - 'https://op.example.com/session/end?foo=bar', + 'https://op.example.com/session/end?foo=bar&client_id=identifier', ); }); it('defaults the post_logout_redirect_uri if client has some', function () { - expect(url.parse(this.clientWithUris.endSessionUrl(), true).query).to.eql({ + expect(getSearchParams(this.clientWithUris.endSessionUrl())).to.eql({ + client_id: 'identifier', post_logout_redirect_uri: 'https://rp.example.com/logout/cb', }); }); @@ -261,13 +272,13 @@ describe('Client', () => { access_token: 'tokenValue', }); expect( - url.parse( + getSearchParams( this.client.endSessionUrl({ id_token_hint: hint, }), - true, - ).query, + ), ).to.eql({ + client_id: 'identifier', id_token_hint: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.', }); }); @@ -284,36 +295,50 @@ describe('Client', () => { ).to.throw(TypeError, 'id_token not present in TokenSet'); }); + it('allows to override default applied values', function () { + expect( + getSearchParams( + this.client.endSessionUrl({ + post_logout_redirect_uri: 'override', + client_id: 'override', + }), + ), + ).to.eql({ + post_logout_redirect_uri: 'override', + client_id: 'override', + }); + }); + it('allows for recommended and optional query params to be passed in', function () { expect( - url.parse( + getSearchParams( this.client.endSessionUrl({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', id_token_hint: 'idtoken', }), - true, - ).query, + ), ).to.eql({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', id_token_hint: 'idtoken', + client_id: 'identifier', }); expect( - url.parse( + getSearchParams( this.clientWithQuery.endSessionUrl({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', id_token_hint: 'idtoken', foo: 'this will be ignored', }), - true, - ).query, + ), ).to.eql({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', foo: 'bar', id_token_hint: 'idtoken', + client_id: 'identifier', }); }); }); @@ -548,17 +573,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setExpirationTime('5m') + .setProtectedHeader({ alg: 'HS256' }) + .sign(new TextEncoder().encode(client.client_secret)); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -601,24 +624,24 @@ describe('Client', () => { authorization_encrypted_response_enc: 'A128GCM', }); - const response = jose2.JWE.encrypt( - jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ), - await client.secretForAlg('A128GCM'), - { + const cleartext = new TextEncoder().encode( + await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setExpirationTime('5m') + .setProtectedHeader({ alg: 'HS256' }) + .sign(new TextEncoder().encode(client.client_secret)), + ); + + const response = await new jose.CompactEncrypt(cleartext) + .setProtectedHeader({ alg: 'dir', enc: 'A128GCM', - }, - ); + }) + .encrypt(await client.secretForAlg('A128GCM')); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -676,17 +699,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(client.client_secret)); return this.client .callback( @@ -924,17 +945,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(client.client_secret)); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -977,24 +996,24 @@ describe('Client', () => { authorization_encrypted_response_enc: 'A128GCM', }); - const response = jose2.JWE.encrypt( - jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ), - await client.secretForAlg('A128GCM'), - { + const cleartext = new TextEncoder().encode( + await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setExpirationTime('5m') + .setProtectedHeader({ alg: 'HS256' }) + .sign(new TextEncoder().encode(client.client_secret)), + ); + + const response = await new jose.CompactEncrypt(cleartext) + .setProtectedHeader({ alg: 'dir', enc: 'A128GCM', - }, - ); + }) + .encrypt(await client.secretForAlg('A128GCM')); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -1052,17 +1071,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuer.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuer.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(client.client_secret)); return this.client .oauthCallback( @@ -1100,6 +1117,18 @@ describe('Client', () => { }); }); + it('ignores the id_token when falsy', function () { + return this.client + .oauthCallback('https://rp.example.com/cb', { + access_token: 'foo', + token_type: 'bearer', + id_token: '', + }) + .then((tokenset) => { + expect(tokenset).not.to.have.property('id_token'); + }); + }); + it('rejects when id_token was issued by the token endpoint', function () { nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -1120,6 +1149,23 @@ describe('Client', () => { ); }); }); + + it('ignores the the token endpoint id_token property when falsy', function () { + nock('https://op.example.com') + .matchHeader('Accept', 'application/json') + .matchHeader('Content-Length', isNumber) + .matchHeader('Transfer-Encoding', isUndefined) + .post('/token') + .reply(200, { id_token: '' }); + + return this.client + .oauthCallback('https://rp.example.com/cb', { + code: 'foo', + }) + .then((tokenset) => { + expect(tokenset).not.to.have.property('id_token'); + }); + }); }); describe('response type checks', function () { @@ -1329,63 +1375,57 @@ describe('Client', () => { }); }); - it('passes ID Token validations when ID Token is returned', function () { + it('passes ID Token validations when ID Token is returned', async function () { nock('https://op.example.com') .matchHeader('Accept', 'application/json') .post('/token') // to make sure filteringRequestBody works .reply(200, { access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'foo', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '5m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'foo', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(this.client.client_secret)), }); return this.client.refresh( new TokenSet({ access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'foo', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '6m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'foo', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('6m') + .sign(new TextEncoder().encode(this.client.client_secret)), }), ); }); - it('rejects when returned ID Token sub does not match the one passed in', function () { + it('rejects when returned ID Token sub does not match the one passed in', async function () { nock('https://op.example.com') .matchHeader('Accept', 'application/json') .post('/token') // to make sure filteringRequestBody works .reply(200, { access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'bar', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '5m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'bar', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('5m') + .sign(new TextEncoder().encode(this.client.client_secret)), }); return this.client @@ -1393,17 +1433,15 @@ describe('Client', () => { new TokenSet({ access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'foo', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '5m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'foo', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .setIssuedAt() + .sign(new TextEncoder().encode(this.client.client_secret)), }), ) .then(fail, (error) => { @@ -1816,7 +1854,7 @@ describe('Client', () => { .reply(200, '{"notavalid"}'); return client.userinfo('foo').then(fail, function (error) { - expect(error.message).to.eql('Unexpected token } in JSON at position 12'); + expect(error.message).to.match(/in JSON at position 12/); expect(error).to.have.property('response'); }); }); @@ -2025,7 +2063,7 @@ describe('Client', () => { }); return client.introspect('tokenValue').then(fail, function (error) { - expect(error.message).to.eql('Unexpected token } in JSON at position 12'); + expect(error.message).to.match(/in JSON at position 12/); expect(error).to.have.property('response'); }); }); @@ -2203,6 +2241,18 @@ describe('Client', () => { ); }); }); + + it('allows client_secret to be empty string', async function () { + const issuer = new Issuer(); + const client = new issuer.Client({ + client_id: 'an:identifier', + client_secret: '', + token_endpoint_auth_method: 'client_secret_post', + }); + expect(await clientInternal.authFor.call(client, 'token')).to.eql({ + form: { client_id: 'an:identifier', client_secret: '' }, + }); + }); }); describe('when client_secret_basic', function () { @@ -2238,6 +2288,14 @@ describe('Client', () => { ); }); }); + + it('allows client_secret to be empty string', async function () { + const issuer = new Issuer(); + const client = new issuer.Client({ client_id: 'an:identifier', client_secret: '' }); + expect(await clientInternal.authFor.call(client, 'token')).to.eql({ + headers: { Authorization: 'Basic YW4lM0FpZGVudGlmaWVyOg==' }, + }); + }); }); describe('when client_secret_jwt', function () { @@ -2334,7 +2392,7 @@ describe('Client', () => { token_endpoint_auth_signing_alg_values_supported: ['ES256', 'ES384'], }); - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); return keystore.generate('EC', 'P-256').then(() => { const client = new issuer.Client( @@ -2435,7 +2493,7 @@ describe('Client', () => { token_endpoint: 'https://op.example.com/token', }); - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); return keystore.generate('EC', 'P-256').then(() => { const client = new issuer.Client( @@ -2467,7 +2525,7 @@ describe('Client', () => { }); before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return this.keystore.generate('RSA'); }); @@ -2502,12 +2560,20 @@ describe('Client', () => { token_endpoint_auth_method: 'tls_client_auth', }); - this.IdToken = async (key, alg, payload) => { - return jose2.JWS.sign(payload, key, { - alg, - typ: 'oauth-authz-req+jwt', - kid: alg.startsWith('HS') ? undefined : key.kid, - }); + this.IdToken = async (jwkOrSecret, alg, payload) => { + let key; + if (jwkOrSecret instanceof Uint8Array) { + key = jwkOrSecret; + } else { + key = await jose.importJWK(jwkOrSecret, alg); + } + return new jose.SignJWT(payload) + .setProtectedHeader({ + alg, + typ: 'oauth-authz-req+jwt', + kid: alg.startsWith('HS') ? undefined : key.kid, + }) + .sign(key); }; }); @@ -3467,7 +3533,7 @@ describe('Client', () => { describe('signed and encrypted responses', function () { before(function () { - this.keystore = jose2.JWKS.asKeyStore({ + this.keystore = new KeyStore({ keys: [ { kty: 'EC', @@ -3789,7 +3855,7 @@ describe('Client', () => { describe('#requestObject', function () { before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return this.keystore.generate('RSA'); }); @@ -3824,7 +3890,7 @@ describe('Client', () => { }); it('verifies keystore has the appropriate key', async function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); await keystore.generate('EC'); const client = new this.issuer.Client( { client_id: 'identifier', request_object_signing_alg: 'EdDSA' }, @@ -4095,7 +4161,7 @@ describe('Client', () => { describe('#requestObject (encryption when multiple keys match)', function () { before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return Promise.all([this.keystore.generate('RSA'), this.keystore.generate('RSA')]); }); diff --git a/test/client/discover_client.test.js b/test/client/discover_client.test.js index 362c704e..095e3f1a 100644 --- a/test/client/discover_client.test.js +++ b/test/client/discover_client.test.js @@ -78,7 +78,7 @@ describe('Client#fromUri()', () => { return issuer.Client.fromUri('https://op.example.com/client/identifier').then( fail, function (error) { - expect(error.message).to.eql('Unexpected token } in JSON at position 12'); + expect(error.message).to.match(/in JSON at position 12/); expect(error).to.have.property('response'); }, ); diff --git a/test/client/dpop.test.js b/test/client/dpop.test.js index 04aa4ef8..4b1311bc 100644 --- a/test/client/dpop.test.js +++ b/test/client/dpop.test.js @@ -2,7 +2,7 @@ const { isUndefined } = require('util'); const { expect } = require('chai'); const nock = require('nock'); -const jose2 = require('jose2'); +const jose = require('jose'); const { Issuer, @@ -20,8 +20,6 @@ const issuer = new Issuer({ dpop_signing_alg_values_supported: ['PS512', 'PS384'], }); -const privateKey = jose2.JWK.generateSync('EC').keyObject; - const fail = () => { throw new Error('expected promise to be rejected'); }; @@ -49,42 +47,58 @@ describe('DPoP', () => { }); }); - it('DPoP Private Key can be passed also as valid createPrivateKey input', async function () { - if (parseInt(process.versions.node, 10) >= 16) { - const jwk = (await jose2.JWK.generate('EC')).toJWK(true); - await this.client.dpopProof({}, { format: 'jwk', key: jwk }); - } + if (jose.cryptoRuntime === 'node:crypto') { + it('DPoP Private Key can be passed also as valid createPrivateKey input', async function () { + if (parseInt(process.versions.node, 10) >= 16) { + const jwk = await jose.exportJWK( + ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, + ); + await this.client.dpopProof({}, { format: 'jwk', key: jwk }); + } - { - const pem = (await jose2.JWK.generate('EC')).toPEM(true); - await this.client.dpopProof({}, pem); - await this.client.dpopProof({}, { key: pem, format: 'pem' }); - } + { + const pem = await jose.exportPKCS8( + ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, + ); + await this.client.dpopProof({}, pem); + await this.client.dpopProof({}, { key: pem, format: 'pem' }); + } - { - const der = (await jose2.JWK.generate('EC')).keyObject.export({ - format: 'der', - type: 'pkcs8', - }); - await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs8' }); - } + { + const der = ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey.export({ + format: 'der', + type: 'pkcs8', + }); + await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs8' }); + } - { - const der = (await jose2.JWK.generate('EC')).keyObject.export({ - format: 'der', - type: 'sec1', - }); - await this.client.dpopProof({}, { key: der, format: 'der', type: 'sec1' }); - } + { + const der = ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey.export({ + format: 'der', + type: 'sec1', + }); + await this.client.dpopProof({}, { key: der, format: 'der', type: 'sec1' }); + } - { - const der = (await jose2.JWK.generate('RSA')).keyObject.export({ - format: 'der', - type: 'pkcs1', - }); - await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs1' }); - } - }); + { + const der = ( + await jose.generateKeyPair('RS256', { extractable: true }) + ).privateKey.export({ + format: 'der', + type: 'pkcs1', + }); + await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs1' }); + } + }); + } it('DPoP Proof JWT w/o ath', async function () { const proof = await this.client.dpopProof( @@ -94,151 +108,140 @@ describe('DPoP', () => { baz: true, }, ( - await jose2.JWK.generate('RSA') - ).keyObject, + await jose.generateKeyPair('RS256', { extractable: true }) + ).privateKey, ); - const decoded = jose2.JWT.decode(proof, { complete: true }); - expect(decoded).to.have.nested.property('header.typ', 'dpop+jwt'); - expect(decoded).to.have.nested.property('payload.iat'); - expect(decoded).to.have.nested.property('payload.jti'); - expect(decoded).to.have.nested.property('payload.htu', 'foo'); - expect(decoded).to.have.nested.property('payload.htm', 'bar'); - expect(decoded).to.have.nested.property('payload.baz', true); - expect(decoded).to.have.nested.property('header.jwk').that.has.keys('kty', 'e', 'n'); + const header = jose.decodeProtectedHeader(proof); + const payload = jose.decodeJwt(proof); + expect(header).to.have.property('jwk').that.has.keys('kty', 'e', 'n'); + expect(header).to.have.property('typ', 'dpop+jwt'); + expect(payload).to.have.property('iat'); + expect(payload).to.have.property('jti'); + expect(payload).to.have.property('htu', 'foo'); + expect(payload).to.have.property('htm', 'bar'); + expect(payload).to.have.property('baz', true); expect( - jose2.JWT.decode( - await this.client.dpopProof({}, (await jose2.JWK.generate('EC')).keyObject), + jose.decodeProtectedHeader( + await this.client.dpopProof( + {}, + ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, + ), { complete: true }, ), ) - .to.have.nested.property('header.jwk') + .to.have.property('jwk') .that.has.keys('kty', 'crv', 'x', 'y'); expect( - jose2.JWT.decode( - await this.client.dpopProof({}, (await jose2.JWK.generate('OKP')).keyObject), - { complete: true }, + jose.decodeProtectedHeader( + await this.client.dpopProof( + {}, + ( + await jose.generateKeyPair('EdDSA', { extractable: true }) + ).privateKey, + ), ), ) - .to.have.nested.property('header.jwk') + .to.have.property('jwk') .that.has.keys('kty', 'crv', 'x'); }); it('DPoP Proof JWT w/ ath', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); const proof = await this.client.dpopProof( { htu: 'foo', htm: 'bar', }, - ( - await jose2.JWK.generate('EC') - ).keyObject, + privateKey, 'foo', ); - const decoded = jose2.JWT.decode(proof, { complete: true }); - expect(decoded).to.have.nested.property( - 'payload.ath', - 'LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564', - ); + const payload = jose.decodeJwt(proof); + expect(payload).to.have.property('ath', 'LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564'); }); - it('else this.issuer.dpop_signing_alg_values_supported is used', async function () { - const proof = await this.client.dpopProof( - {}, - ( - await jose2.JWK.generate('RSA', 2048) - ).keyObject, - ); - // 256 is not supported by the issuer, next one in line is PS384 - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'PS384', - ); - }); + if (jose.cryptoRuntime === 'node:crypto') { + it('else this.issuer.dpop_signing_alg_values_supported is used', async function () { + const proof = await this.client.dpopProof( + {}, + ( + await jose.generateKeyPair('RS256', { extractable: true }) + ).privateKey, + ); + // 256 is not supported by the issuer, next one in line is PS384 + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'PS384'); + }); + } it('unless the key dictates an algorithm', async function () { { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('OKP', 'Ed25519') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'EdDSA', + await jose.generateKeyPair('EdDSA', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'EdDSA'); } - if (!('electron' in process.versions)) { + if (!('electron' in process.versions) && jose.cryptoRuntime === 'node:crypto') { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('OKP', 'Ed448') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'EdDSA', + await jose.generateKeyPair('EdDSA', { crv: 'Ed448' }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'EdDSA'); } { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'P-256') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES256', + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES256'); } - if (!('electron' in process.versions)) { + if (!('electron' in process.versions) && jose.cryptoRuntime === 'node:crypto') { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'secp256k1') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES256K', + await jose.generateKeyPair('ES256K', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES256K'); } { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'P-384') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES384', + await jose.generateKeyPair('ES384', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES384'); } { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'P-521') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES512', + await jose.generateKeyPair('ES512', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES512'); } }); }); it('is enabled for userinfo', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); + nock('https://op.example.com').get('/me').reply(200, { sub: 'foo' }); await this.client.userinfo('foo', { DPoP: privateKey }); @@ -246,15 +249,17 @@ describe('DPoP', () => { expect(this.httpOpts).to.have.nested.property('headers.DPoP'); const proof = this.httpOpts.headers.DPoP; - const proofJWT = jose2.JWT.decode(proof, { complete: true }); - expect(proofJWT).to.have.nested.property('payload.ath'); + const proofJWT = jose.decodeJwt(proof); + expect(proofJWT).to.have.property('ath'); }); it('handles DPoP nonce in userinfo', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); + nock('https://op.example.com') .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.be.undefined; return true; }) @@ -264,21 +269,21 @@ describe('DPoP', () => { }) .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) @@ -295,10 +300,12 @@ describe('DPoP', () => { }); it('handles DPoP nonce in grant', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); + nock('https://op.example.com') .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.be.undefined; return true; }) @@ -311,21 +318,21 @@ describe('DPoP', () => { ) .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { access_token: 'foo' }) .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { access_token: 'foo' }) .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) @@ -342,10 +349,11 @@ describe('DPoP', () => { }); it('handles DPoP nonce in requestResource', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://rs.example.com') .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.be.undefined; return true; }) @@ -355,21 +363,21 @@ describe('DPoP', () => { }) .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) @@ -393,6 +401,7 @@ describe('DPoP', () => { }); it('is enabled for requestResource', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://rs.example.com') .matchHeader('Transfer-Encoding', isUndefined) .matchHeader('Content-Length', isUndefined) @@ -407,11 +416,12 @@ describe('DPoP', () => { expect(this.httpOpts).to.have.nested.property('headers.DPoP'); const proof = this.httpOpts.headers.DPoP; - const proofJWT = jose2.JWT.decode(proof, { complete: true }); - expect(proofJWT).to.have.nested.property('payload.ath'); + const proofJWT = jose.decodeJwt(proof); + expect(proofJWT).to.have.property('ath'); }); it('is enabled for grant', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); await this.client.grant({ grant_type: 'foo' }, { DPoP: privateKey }); @@ -420,6 +430,7 @@ describe('DPoP', () => { }); it('is enabled for refresh', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); await this.client.refresh('foo', { DPoP: privateKey }); @@ -428,6 +439,7 @@ describe('DPoP', () => { }); it('is enabled for oauthCallback', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); await this.client.oauthCallback('foo', { code: 'foo' }, {}, { DPoP: privateKey }); @@ -436,6 +448,7 @@ describe('DPoP', () => { }); it('is enabled for callback', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); try { @@ -446,6 +459,7 @@ describe('DPoP', () => { }); it('is enabled for deviceAuthorization', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/device').reply(200, { expires_in: 60, device_code: 'foo', diff --git a/test/client/implicit_kid.test.js b/test/client/implicit_kid.test.js index 728f3b9e..445b9e16 100644 --- a/test/client/implicit_kid.test.js +++ b/test/client/implicit_kid.test.js @@ -1,12 +1,12 @@ const { expect } = require('chai'); -const jose2 = require('jose2'); const nock = require('nock'); const Issuer = require('../../lib/issuer'); const clientInternal = require('../../lib/helpers/client'); +const KeyStore = require('../keystore'); async function noKidJWKS() { - const store = new jose2.JWKS.KeyStore(); + const store = new KeyStore(); await store.generate('EC'); const jwks = store.toJWKS(true); delete jwks.keys[0].kid; diff --git a/test/client/mtls.test.js b/test/client/mtls.test.js index 92075bda..59c55a8b 100644 --- a/test/client/mtls.test.js +++ b/test/client/mtls.test.js @@ -3,7 +3,7 @@ const path = require('path'); const { expect } = require('chai'); const nock = require('nock'); -const jose2 = require('jose2'); +const jose = require('jose'); const { Issuer, custom } = require('../../lib'); const clientHelpers = require('../../lib/helpers/client'); @@ -102,33 +102,32 @@ describe('mutual-TLS', () => { token_endpoint_auth_signing_alg: 'HS256', tls_client_certificate_bound_access_tokens: true, }); + this.jwtAuthClientNoSenderConstraining = new issuer.Client({ + client_id: 'client', + client_secret: 'secret', + token_endpoint_auth_method: 'client_secret_jwt', + token_endpoint_auth_signing_alg: 'HS256', + tls_client_certificate_bound_access_tokens: false, + }); this.client[custom.http_options] = () => ({ key, cert }); }); - it('uses the mtls endpoint alias for token endpoint when using jwt auth and tls certs', async function () { + it('uses the issuer identifier and token endpoint as private_key_jwt audiences', async function () { let { form: { client_assertion: jwt }, } = await clientHelpers.authFor.call(this.jwtAuthClient, 'token'); - let { aud } = jose2.JWT.decode(jwt); - expect(aud).to.include('https://mtls.op.example.com/token'); - expect(aud).to.include('https://op.example.com/token'); - expect(aud).to.include('https://op.example.com'); + let { aud } = jose.decodeJwt(jwt); + expect(aud).to.deep.equal(['https://op.example.com', 'https://op.example.com/token']); ({ form: { client_assertion: jwt }, } = await clientHelpers.authFor.call(this.jwtAuthClient, 'introspection')); - ({ aud } = jose2.JWT.decode(jwt)); - expect(aud).not.to.include('https://mtls.op.example.com/token/introspect'); - expect(aud).to.include('https://op.example.com/token/introspect'); - expect(aud).to.include('https://op.example.com/token'); - expect(aud).to.include('https://op.example.com'); + ({ aud } = jose.decodeJwt(jwt)); + expect(aud).to.deep.equal(['https://op.example.com', 'https://op.example.com/token']); ({ form: { client_assertion: jwt }, } = await clientHelpers.authFor.call(this.jwtAuthClient, 'revocation')); - ({ aud } = jose2.JWT.decode(jwt)); - expect(aud).not.to.include('https://mtls.op.example.com/token/revoke'); - expect(aud).to.include('https://op.example.com/token/revoke'); - expect(aud).to.include('https://op.example.com/token'); - expect(aud).to.include('https://op.example.com'); + ({ aud } = jose.decodeJwt(jwt)); + expect(aud).to.deep.equal(['https://op.example.com', 'https://op.example.com/token']); }); it('requires mTLS for userinfo when tls_client_certificate_bound_access_tokens is true', async function () { diff --git a/test/client/register_client.test.js b/test/client/register_client.test.js index 72bb874a..0d5f2386 100644 --- a/test/client/register_client.test.js +++ b/test/client/register_client.test.js @@ -1,11 +1,11 @@ const { isNumber, isUndefined } = require('util'); const { expect } = require('chai'); -const jose2 = require('jose2'); const sinon = require('sinon'); const nock = require('nock'); const { Issuer, custom } = require('../../lib'); +const KeyStore = require('../keystore'); const fail = () => { throw new Error('expected promise to be rejected'); @@ -82,14 +82,14 @@ describe('Client#register', () => { nock('https://op.example.com').post('/client/registration').reply(201, '{"notavalid"}'); return issuer.Client.register({}).then(fail, function (error) { - expect(error.message).to.eql('Unexpected token } in JSON at position 12'); + expect(error.message).to.match(/in JSON at position 12/); expect(error).to.have.property('response'); }); }); describe('with keystore (as option)', function () { it('enriches the registration with jwks if not provided (or jwks_uri)', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); nock('https://op.example.com') .filteringRequestBody(function (body) { @@ -109,7 +109,7 @@ describe('Client#register', () => { }); it('ignores the keystore during registration if jwks is provided', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); nock('https://op.example.com') .filteringRequestBody(function (body) { @@ -134,7 +134,7 @@ describe('Client#register', () => { }); it('ignores the keystore during registration if jwks_uri is provided', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); nock('https://op.example.com') .filteringRequestBody(function (body) { @@ -166,7 +166,7 @@ describe('Client#register', () => { }); it('does not accept oct keys', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); return keystore.generate('oct', 32).then(() => { return issuer.Client.register({}, { jwks: keystore.toJWKS(true) }).then( diff --git a/test/client/self_issued.test.js b/test/client/self_issued.test.js index cda850ce..f43ed4da 100644 --- a/test/client/self_issued.test.js +++ b/test/client/self_issued.test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const nock = require('nock'); const timekeeper = require('timekeeper'); -const jose2 = require('jose2'); +const jose = require('jose'); const { Issuer } = require('../../lib'); @@ -35,29 +35,33 @@ describe('Validating Self-Issued OP responses', () => { Object.assign(this, { issuer, client }); }); - const idToken = (claims = {}) => { - const jwk = jose2.JWK.generateSync('EC'); - return jose2.JWT.sign( - { - sub_jwk: jwk.toJWK(), - sub: jwk.thumbprint, - ...claims, - }, - jwk, - { expiresIn: '2h', issuer: 'https://self-issued.me', audience: 'https://rp.example.com/cb' }, - ); - }; + async function idToken(claims = {}) { + const kp = await jose.generateKeyPair('ES256', { extractable: true }); + const jwk = await jose.exportJWK(kp.publicKey); + const sub = await jose.calculateJwkThumbprint(jwk); + return await new jose.SignJWT({ + sub_jwk: jwk, + sub, + ...claims, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'ES256' }) + .setIssuer('https://self-issued.me') + .setAudience('https://rp.example.com/cb') + .setExpirationTime('2h') + .sign(kp.privateKey); + } describe('consuming an ID Token response', () => { - it('consumes a self-issued response', function () { + it('consumes a self-issued response', async function () { const { client } = this; - return client.callback(undefined, { id_token: idToken() }); + return client.callback(undefined, { id_token: await idToken() }); }); - it('expects sub_jwk to be in the ID Token claims', function () { + it('expects sub_jwk to be in the ID Token claims', async function () { const { client } = this; return client - .callback(undefined, { id_token: idToken({ sub_jwk: undefined }) }) + .callback(undefined, { id_token: await idToken({ sub_jwk: undefined }) }) .then(fail, (err) => { expect(err.name).to.equal('RPError'); expect(err.message).to.equal('missing required JWT property sub_jwk'); @@ -65,10 +69,10 @@ describe('Validating Self-Issued OP responses', () => { }); }); - it('expects sub_jwk to be a public JWK', function () { + it('expects sub_jwk to be a public JWK', async function () { const { client } = this; return client - .callback(undefined, { id_token: idToken({ sub_jwk: 'foobar' }) }) + .callback(undefined, { id_token: await idToken({ sub_jwk: 'foobar' }) }) .then(fail, (err) => { expect(err.name).to.equal('RPError'); expect(err.message).to.equal('failed to use sub_jwk claim as an asymmetric JSON Web Key'); @@ -76,13 +80,15 @@ describe('Validating Self-Issued OP responses', () => { }); }); - it('expects sub to be the thumbprint of the sub_jwk', function () { + it('expects sub to be the thumbprint of the sub_jwk', async function () { const { client } = this; - return client.callback(undefined, { id_token: idToken({ sub: 'foo' }) }).then(fail, (err) => { - expect(err.name).to.equal('RPError'); - expect(err.message).to.equal('failed to match the subject with sub_jwk'); - expect(err).to.have.property('jwt'); - }); + return client + .callback(undefined, { id_token: await idToken({ sub: 'foo' }) }) + .then(fail, (err) => { + expect(err.name).to.equal('RPError'); + expect(err.message).to.equal('failed to match the subject with sub_jwk'); + expect(err).to.have.property('jwt'); + }); }); }); }); diff --git a/test/issuer/discover_issuer.test.js b/test/issuer/discover_issuer.test.js index 7a9c9f4c..e0adf068 100644 --- a/test/issuer/discover_issuer.test.js +++ b/test/issuer/discover_issuer.test.js @@ -278,7 +278,7 @@ describe('Issuer#discover()', () => { .reply(200, '{"notavalid"}'); return Issuer.discover('https://op.example.com').then(fail, function (error) { - expect(error.message).to.eql('Unexpected token } in JSON at position 12'); + expect(error.message).to.match(/in JSON at position 12/); expect(error).to.have.property('response'); }); }); diff --git a/test/issuer/issuer_instance.test.js b/test/issuer/issuer_instance.test.js index 2f1fff14..f58bfcb4 100644 --- a/test/issuer/issuer_instance.test.js +++ b/test/issuer/issuer_instance.test.js @@ -2,10 +2,10 @@ const { expect } = require('chai'); const LRU = require('lru-cache'); const nock = require('nock'); const sinon = require('sinon'); -const jose2 = require('jose2'); const { Issuer, custom } = require('../../lib'); const issuerInternal = require('../../lib/helpers/issuer'); +const KeyStore = require('../keystore'); const fail = () => { throw new Error('expected promise to be rejected'); @@ -22,7 +22,7 @@ describe('Issuer', () => { }); before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return this.keystore.generate('RSA'); }); @@ -115,7 +115,11 @@ describe('Issuer', () => { return this.keystore.generate('RSA', undefined, { kid }).then(() => { nock('https://op.example.com').get('/certs').reply(200, this.keystore.toJWKS()); - return issuerInternal.queryKeyStore.call(this.issuer, { alg: 'RS256', kid, use: 'sig' }); + return issuerInternal.queryKeyStore + .call(this.issuer, { alg: 'RS256', kid, use: 'sig' }) + .then((result) => { + expect(result).to.have.lengthOf(2); + }); }); }); diff --git a/test/keystore.js b/test/keystore.js new file mode 100644 index 00000000..40007ead --- /dev/null +++ b/test/keystore.js @@ -0,0 +1,97 @@ +const jose = require('jose'); +const crypto = require('crypto'); +const base64url = require('../lib/helpers/base64url'); + +module.exports = class KeyStore { + constructor({ keys } = {}) { + this.keys = keys || []; + } + + async generate(kty, crvOrSize, { alg, kid, use } = {}) { + let kp; + if (kty !== 'oct' && alg) { + kp = await jose.generateKeyPair(alg); + } else { + switch (kty) { + case 'EC': { + switch (crvOrSize) { + case undefined: + case 'P-256': + kp = await jose.generateKeyPair('ES256', { extractable: true }); + break; + case 'P-384': + kp = await jose.generateKeyPair('ES384', { extractable: true }); + break; + case 'P-521': + kp = await jose.generateKeyPair('ES512', { extractable: true }); + break; + case 'secp256k1': + kp = await jose.generateKeyPair('ES256K', { extractable: true }); + break; + } + break; + } + case 'oct': { + const secret = crypto.randomBytes((crvOrSize || 256) >> 3); + const jwk = { + kty: 'oct', + use: use, + alg, + k: base64url.encode(secret), + }; + jwk.kid = kid || (await jose.calculateJwkThumbprint(jwk)); + this.keys.push(jwk); + return; + } + case 'RSA': { + kp = await jose.generateKeyPair('RS256', { modulusLength: crvOrSize, extractable: true }); + break; + } + case 'OKP': { + switch (crvOrSize) { + case undefined: + case 'Ed25519': + case 'Ed448': + kp = await jose.generateKeyPair('EdDSA', { crv: crvOrSize, extractable: true }); + break; + case 'X25519': + case 'X448': + kp = await jose.generateKeyPair('ECDH-ES', { crv: crvOrSize, extractable: true }); + break; + } + break; + } + } + } + const jwk = { + ...(await jose.exportJWK(kp.privateKey)), + kid, + }; + jwk.kid || (jwk.kid = await jose.calculateJwkThumbprint(jwk)); + if (use) jwk.use = use; + this.keys.push(jwk); + } + + get(query) { + if (!query) { + return this.keys[0]; + } + const { kty } = query || {}; + return this.keys.find((jwk) => { + return jwk.kty === kty; + }); + } + + toJWKS(includePrivate) { + if (includePrivate) { + return { keys: this.keys }; + } + + return { + keys: this.keys.map((privateKey) => { + const { k, d, dp, dq, p, q, qi, ...jwk } = privateKey; + return jwk; + }), + }; + } +}; diff --git a/test/passport/passport_strategy.test.js b/test/passport/passport_strategy.test.js index 2f5b6540..d542dadb 100644 --- a/test/passport/passport_strategy.test.js +++ b/test/passport/passport_strategy.test.js @@ -128,6 +128,28 @@ describe('OpenIDConnectStrategy', () => { ); }); + it('starts authentication requests for TPIL GETs', function () { + const params = { iss: 'https://op.example.com' }; + const strategy = new Strategy({ client: this.client, params }, () => {}); + + const req = new MockRequest('GET', '/login/oidc'); + req.session = {}; + + strategy.redirect = sinon.spy(); + strategy.authenticate(req); + + expect(strategy.redirect.calledOnce).to.be.true; + const target = strategy.redirect.firstCall.args[0]; + expect(target).to.include('redirect_uri='); + expect(target).to.include('scope='); + expect(req.session).to.have.property('oidc:op.example.com'); + expect(req.session['oidc:op.example.com']).to.have.keys( + 'state', + 'response_type', + 'code_verifier', + ); + }); + it('starts authentication requests for POSTs', async function () { const strategy = new Strategy({ client: this.client }, () => {}); @@ -197,13 +219,14 @@ describe('OpenIDConnectStrategy', () => { expect(target).to.include(`resource=${encodeURIComponent('urn:example:foo')}`); }); - it('automatically includes nonce for where it applies', async function () { + it('automatically includes nonce for where it applies (and ignores one from params)', async function () { const strategy = new Strategy( { client: this.client, params: { response_type: 'code id_token token', response_mode: 'form_post', + nonce: 'foo', }, }, () => {}, @@ -220,6 +243,7 @@ describe('OpenIDConnectStrategy', () => { expect(target).to.include('redirect_uri='); expect(target).to.include('scope='); expect(target).to.include('nonce='); + expect(target).not.to.include('nonce=foo'); expect(target).to.include('response_mode=form_post'); expect(req.session).to.have.property('oidc:op.example.com'); expect(req.session['oidc:op.example.com']).to.have.keys( @@ -230,6 +254,37 @@ describe('OpenIDConnectStrategy', () => { ); }); + it('ignores static state coming from params', function () { + const strategy = new Strategy( + { + client: this.client, + params: { + state: 'foo', + }, + }, + () => {}, + ); + + const req = new MockRequest('GET', '/login/oidc'); + req.session = {}; + + strategy.redirect = sinon.spy(); + strategy.authenticate(req); + + expect(strategy.redirect.calledOnce).to.be.true; + const target = strategy.redirect.firstCall.args[0]; + expect(target).to.include('redirect_uri='); + expect(target).to.include('scope='); + expect(target).to.include('state='); + expect(target).not.to.include('state=foo'); + expect(req.session).to.have.property('oidc:op.example.com'); + expect(req.session['oidc:op.example.com']).to.have.keys( + 'state', + 'response_type', + 'code_verifier', + ); + }); + describe('use pkce', () => { it('will throw when explictly provided value is not supported', function () { expect(() => { diff --git a/test/tokenset/tokenset.test.js b/test/tokenset/tokenset.test.js index 78530089..c2fe01b8 100644 --- a/test/tokenset/tokenset.test.js +++ b/test/tokenset/tokenset.test.js @@ -54,4 +54,30 @@ describe('TokenSet', function () { expect(JSON.parse(JSON.stringify(ts))).to.eql(ts); }); + + it('cannot have its prototype methods overloaded', function () { + let ts = new TokenSet({ + claims: null, + id_token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ', + }); + + expect(ts.claims).to.be.a('function'); + expect(ts.claims()).to.eql({ admin: true, name: 'John Doe', sub: '1234567890' }); + + ts = new TokenSet({ expires_in: 'foo' }); + ts.expires_in = 200; + expect(ts.expires_in).to.be.a('number'); + expect(ts.expired()).to.eql(false); + + const e = new Error(); + class CustomTokenSet extends TokenSet { + expired() { + throw e; + } + } + + ts = new CustomTokenSet({}); + expect(() => ts.expired()).to.throw(e); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 7322774c..835e5c91 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -8,6 +8,7 @@ import * as http2 from 'http2'; import { URL } from 'url'; import * as jose from 'jose'; import * as crypto from 'crypto'; +import { format } from 'util'; export type HttpOptions = Partial< Pick< @@ -141,6 +142,8 @@ export interface EndSessionParameters { id_token_hint?: TokenSet | string; post_logout_redirect_uri?: string; state?: string; + client_id?: string; + logout_hint?: string; [key: string]: unknown; } @@ -166,7 +169,7 @@ export interface OAuthCallbackChecks { state?: string; code_verifier?: string; jarm?: boolean; - scope?: string; + scope?: string; // TODO: remove in v6.x } export interface OpenIDCallbackChecks extends OAuthCallbackChecks { @@ -354,7 +357,7 @@ declare class BaseClient { tokenType?: string; DPoP?: DPoPInput; }, - ): { body?: Buffer } & http.IncomingMessage; + ): Promise<{ body?: Buffer } & http.IncomingMessage>; grant(body: GrantBody, extras?: GrantExtras): Promise; introspect( token: string, @@ -578,6 +581,18 @@ export namespace errors { scope?: string; session_state?: string; response?: { body?: UnknownObject | Buffer } & http.IncomingMessage; + + constructor( + params: { + error: string; + error_description?: string; + error_uri?: string; + state?: string; + scope?: string; + session_state?: string; + }, + response?: { body?: UnknownObject | Buffer } & http.IncomingMessage, + ); } class RPError extends Error { @@ -592,6 +607,14 @@ export namespace errors { exp?: number; iat?: number; auth_time?: number; + + constructor(...args: Parameters); + constructor(options: { + message?: string; + printf?: Parameters; + response?: { body?: UnknownObject | Buffer } & http.IncomingMessage; + [key: string]: unknown; + }); } } diff --git a/types/openid-client-tests.ts b/types/openid-client-tests.ts index 7ca13a5c..dc958155 100644 --- a/types/openid-client-tests.ts +++ b/types/openid-client-tests.ts @@ -167,8 +167,8 @@ async (req: IncomingMessage) => { callbackResponse, { headers: { Accept: 'application/json' } }, ); - console.log(resource.body.byteLength); - console.log(resource.body.toString('utf-8')); + console.log(resource.body!.byteLength); + console.log(resource.body!.toString('utf-8')); //