diff --git a/.formatter.exs b/.formatter.exs index 65f2b21..0f10a5d 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,6 +2,7 @@ inputs: [ "lib/**/*.ex", "test/**/*.exs", + "certification/*.exs", ".formatter.exs", ".credo.exs", "mix.exs" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..5ca6257 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,3 @@ +{ + "enabledManagers": ["asdf"] +} diff --git a/.github/workflows/branch_main.yml b/.github/workflows/branch_main.yml index 058e43c..0344844 100644 --- a/.github/workflows/branch_main.yml +++ b/.github/workflows/branch_main.yml @@ -17,6 +17,8 @@ jobs: security-events: write uses: ./.github/workflows/part_test.yml + secrets: + OID_CERTIFICATION_API_TOKEN: ${{ secrets.OID_CERTIFICATION_API_TOKEN }} docs: name: "Docs" @@ -29,3 +31,11 @@ jobs: uses: ./.github/workflows/part_docs.yml with: attest: false + + dependency_submission: + name: "Mix Dependency Submission" + + permissions: + contents: write + + uses: ./.github/workflows/part_dependency_submission.yml diff --git a/.github/workflows/part_dependency_submission.yml b/.github/workflows/part_dependency_submission.yml new file mode 100644 index 0000000..a0f5c2f --- /dev/null +++ b/.github/workflows/part_dependency_submission.yml @@ -0,0 +1,26 @@ +on: + workflow_call: {} + +name: "Mix Dependency Submission" + +permissions: + contents: read + +jobs: + submit: + name: "Submit" + + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: erlef/mix-dependency-submission@dd81a2f0238bd242a4674703ba7b99c0b284b2f1 # v1.1.3 diff --git a/.github/workflows/part_docs.yml b/.github/workflows/part_docs.yml index 8f403e7..3a0a39e 100644 --- a/.github/workflows/part_docs.yml +++ b/.github/workflows/part_docs.yml @@ -24,12 +24,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-elixir@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -54,13 +54,13 @@ jobs: tar -czvf docs.tar.gz doc - name: "Attest docs provenance" - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 id: attest-docs-provenance - if: "${{ github.event.inputs.attest }}" + if: "${{ inputs.attest }}" with: subject-path: 'docs.tar.gz' - name: "Copy docs provenance" - if: "${{ github.event.inputs.attest }}" + if: "${{ inputs.attest }}" run: cp "$ATTESTATION" docs.tar.gz.sigstore env: ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}" diff --git a/.github/workflows/part_publish.yml b/.github/workflows/part_publish.yml index c7d025d..2bed39f 100644 --- a/.github/workflows/part_publish.yml +++ b/.github/workflows/part_publish.yml @@ -23,12 +23,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions diff --git a/.github/workflows/part_release.yml b/.github/workflows/part_release.yml index a1d71d0..8354046 100644 --- a/.github/workflows/part_release.yml +++ b/.github/workflows/part_release.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -52,7 +52,7 @@ jobs: ${{ inputs.releaseName }} - name: "Download Docs Artifact" - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: docs path: . diff --git a/.github/workflows/part_test.yml b/.github/workflows/part_test.yml index 461203d..d42b19d 100644 --- a/.github/workflows/part_test.yml +++ b/.github/workflows/part_test.yml @@ -1,5 +1,9 @@ on: - workflow_call: {} + workflow_call: + secrets: + OID_CERTIFICATION_API_TOKEN: + description: "OpenID Certification API Token" + required: true name: "Test" @@ -22,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -49,12 +53,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -74,12 +78,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -100,7 +104,7 @@ jobs: - run: mix format --check-formatted eunit: - name: rebar3 eunit (${{ matrix.otp }}) + name: rebar3 eunit (${{ matrix.name }}) runs-on: ubuntu-latest @@ -110,23 +114,24 @@ jobs: fail-fast: false matrix: include: + # Lowest Supported - otp: "26.0" - unstable: false - - otp: "26.2.5" - unstable: false - - otp: "27.1.2" - unstable: false + name: "lowest" + # Latest Supported + - otp: "${{ needs.detectToolVersions.outputs.otpVersion }}" + name: "latest" + # Test Master - otp: "master" - unstable: true + name: "master" steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: otp-version: ${{ matrix.otp }} @@ -139,15 +144,15 @@ jobs: restore-keys: | eunit-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - run: rebar3 eunit --cover --cover_export_name "eunit-${{ steps.setupBEAM.outputs.otp-version }}" - continue-on-error: ${{ matrix.unstable }} + continue-on-error: ${{ matrix.name == 'master' }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: "${{ matrix.otp == needs.detectToolVersions.outputs.otpVersion }}" + if: "${{ matrix.name == 'latest' }}" with: name: eunit-coverage-${{ matrix.otp }} path: "_build/test/cover/eunit-${{ steps.setupBEAM.outputs.otp-version }}.coverdata" conformance: - name: rebar3 ct (${{ matrix.otp }}) + name: rebar3 ct (${{ matrix.name }}) runs-on: ubuntu-latest @@ -157,23 +162,24 @@ jobs: fail-fast: false matrix: include: + # Lowest Supported - otp: "26.0" - unstable: false - - otp: "26.2.5" - unstable: false - - otp: "27.1.2" - unstable: false + name: "lowest" + # Latest Supported + - otp: "${{ needs.detectToolVersions.outputs.otpVersion }}" + name: "latest" + # Test Master - otp: "master" - unstable: true + name: "master" steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: otp-version: ${{ matrix.otp }} @@ -186,15 +192,15 @@ jobs: restore-keys: | ct-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - run: rebar3 ct --cover --cover_export_name "ct-${{ steps.setupBEAM.outputs.otp-version }}" - continue-on-error: ${{ matrix.unstable }} + continue-on-error: ${{ matrix.name == 'master' }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: "${{ matrix.otp == needs.detectToolVersions.outputs.otpVersion }}" + if: "${{ matrix.name == 'latest' }}" with: name: ct-coverage-${{ matrix.otp }} path: _build/test/cover/ct-${{ steps.setupBEAM.outputs.otp-version }}.coverdata mix_test: - name: mix test (${{ matrix.elixir }}) + name: mix test (${{ matrix.name }}) runs-on: ubuntu-latest @@ -204,30 +210,27 @@ jobs: fail-fast: false matrix: include: + # Lowest Supported - elixir: "1.14.5" otp: "26.2.5" - unstable: false - - elixir: "1.15.8" - otp: "26.2.5" - unstable: false - - elixir: "1.16.3" - otp: "26.2.5" - unstable: false - - elixir: "1.17.3" - otp: "27.1.2" - unstable: false + name: "lowest" + # Latest Supported + - elixir: "${{ needs.detectToolVersions.outputs.elixirVersion }}" + otp: "${{ needs.detectToolVersions.outputs.otpVersion }}" + name: "latest" + # Test Main - elixir: "main" otp: "${{ needs.detectToolVersions.outputs.otpVersion }}" - unstable: true + name: "main" steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: otp-version: "${{ matrix.otp }}" @@ -248,9 +251,9 @@ jobs: mix_test-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}- - run: mix deps.get - run: mix test --cover --export-coverage "mix_test-${{ steps.setupBEAM.outputs.elixir-version }}" - continue-on-error: ${{ matrix.unstable }} + continue-on-error: ${{ matrix.name == 'main' }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: "${{ matrix.otp == needs.detectToolVersions.outputs.otpVersion }}" + if: "${{ matrix.name == 'latest' }}" with: name: mix_test-coverage-${{ matrix.elixir }} path: cover/mix_test-${{ steps.setupBEAM.outputs.elixir-version }}.coverdata @@ -264,12 +267,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -280,7 +283,7 @@ jobs: key: mix_test_coverage-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-${{ hashFiles('mix.exs') }} restore-keys: | mix_test_coverage-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}- - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: artifacts - name: Unpack Artifacts @@ -310,12 +313,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -326,7 +329,7 @@ jobs: key: cover-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.config') }} restore-keys: | cover-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: artifacts - name: Unpack Artifacts @@ -346,12 +349,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -375,12 +378,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -401,7 +404,7 @@ jobs: - run: mix deps.compile - run: mix credo --format sarif > results.sarif - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: results.sarif category: credo @@ -413,12 +416,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -445,12 +448,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -470,12 +473,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 id: setupBEAM with: version-file: .tool-versions @@ -487,3 +490,43 @@ jobs: restore-keys: | hank-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - run: rebar3 hank + + certification: + name: "OpenID Certification" + + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: erlef/setup-beam@8e7fdef09ffa3ea54328b5000c170a3b9b20ca96 # v1.20.3 + id: setupBEAM + with: + version-file: .tool-versions + version-type: strict + + - run: mkdir -p "$TMP/certification_out" + env: + TMP: ${{ runner.temp }} + + # Prime Mix.install to prevent Mix Compile Locks + - run: 'echo "" | certification/implementation_plug.exs' + + - uses: erlef/oid_certo@e47ec90093332ecc3c11d0bdec85d5306c778c29 # v1.0.0-beta.4 + with: + command: >- + certification/config.json + --implementation certification/implementation_plug.exs + --output-directory "$TMP/certification_out" + api-token: ${{ secrets.OID_CERTIFICATION_API_TOKEN }} + env: + TMP: ${{ runner.temp }} + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: certification-plug + path: "${{ runner.temp }}/certification_out" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index af679f3..46796fe 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,6 +18,8 @@ jobs: security-events: write uses: ./.github/workflows/part_test.yml + secrets: + OID_CERTIFICATION_API_TOKEN: ${{ secrets.OID_CERTIFICATION_API_TOKEN }} docs: name: "Docs" @@ -38,11 +40,11 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0 \ No newline at end of file + uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index a03d01c..7e711b5 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -46,7 +46,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif @@ -76,6 +76,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index b4dc0c2..b739424 100644 --- a/.gitignore +++ b/.gitignore @@ -29,9 +29,6 @@ oidcc-*.tar mix.lock rebar.lock -# Certification Subtree -/certification - # Other Rebar Files ebin/ log/ diff --git a/.tool-versions b/.tool-versions index 3261510..02967d6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -erlang 27.1.2 -rebar 3.24.0 -elixir 1.17.3 +erlang 27.3.4 +rebar 3.25.0 +elixir 1.18.4 diff --git a/certification/config.json b/certification/config.json new file mode 100644 index 0000000..730dcd1 --- /dev/null +++ b/certification/config.json @@ -0,0 +1,166 @@ +{ + "plans": [ + { + "name": "oidcc-client-basic-certification-test-plan", + "config": { + "description": "Basic Certification (Plain HTTP)" + }, + "variant": { + "request_type": "plain_http_request", + "client_registration": "dynamic_client" + } + }, + { + "name": "oidcc-client-basic-certification-test-plan", + "config": { + "description": "Basic Certification (Request Object)" + }, + "variant": { + "request_type": "request_object", + "client_registration": "dynamic_client" + } + }, + { + "name": "oidcc-client-formpost-basic-certification-test-plan", + "config": { + "description": "Form POST Certification" + }, + "variant": { + "request_type": "plain_http_request", + "client_registration": "dynamic_client" + } + }, + { + "name": "oidcc-client-test-plan", + "ignore": [ + "oidcc-client-test-discovery-webfinger-acct", + "oidcc-client-test-discovery-webfinger-url", + "oidcc-client-test-userinfo-bearer-body", + "oidcc-client-test-aggregated-claims" + ], + "config": { + "description": "Comprehensive (client_secret_basic)" + }, + "variant": { + "client_auth_type": "client_secret_basic", + "request_type": "plain_http_request", + "response_type": "code", + "client_registration": "dynamic_client", + "response_mode": "default" + } + }, + { + "name": "oidcc-client-test-plan", + "ignore": [ + "oidcc-client-test-discovery-webfinger-acct", + "oidcc-client-test-discovery-webfinger-url", + "oidcc-client-test-userinfo-bearer-body", + "oidcc-client-test-aggregated-claims" + ], + "config": { + "description": "Comprehensive (client_secret_post)" + }, + "variant": { + "client_auth_type": "client_secret_post", + "request_type": "plain_http_request", + "response_type": "code", + "client_registration": "dynamic_client", + "response_mode": "default" + } + }, + { + "name": "oidcc-client-test-plan", + "ignore": [ + "oidcc-client-test-discovery-webfinger-acct", + "oidcc-client-test-discovery-webfinger-url", + "oidcc-client-test-userinfo-bearer-body", + "oidcc-client-test-aggregated-claims" + ], + "config": { + "description": "Comprehensive (client_secret_jwt)" + }, + "variant": { + "client_auth_type": "client_secret_jwt", + "request_type": "plain_http_request", + "response_type": "code", + "client_registration": "dynamic_client", + "response_mode": "default" + } + }, + { + "name": "oidcc-client-test-plan", + "ignore": [ + "oidcc-client-test-discovery-webfinger-acct", + "oidcc-client-test-discovery-webfinger-url", + "oidcc-client-test-userinfo-bearer-body", + "oidcc-client-test-aggregated-claims" + ], + "config": { + "description": "Comprehensive (private_key_jwt)" + }, + "variant": { + "client_auth_type": "private_key_jwt", + "request_type": "plain_http_request", + "response_type": "code", + "client_registration": "dynamic_client", + "response_mode": "default" + } + }, + { + "name": "oidcc-client-refreshtoken-test-plan", + "config": { + "description": "Refresh Token" + }, + "ignore": [ + "oidcc-client-test-discovery-webfinger-acct", + "oidcc-client-test-discovery-webfinger-url", + "oidcc-client-test-userinfo-bearer-body", + "oidcc-client-test-aggregated-claims" + ], + "variant": { + "client_auth_type": "client_secret_basic", + "request_type": "plain_http_request", + "response_type": "code", + "client_registration": "dynamic_client", + "response_mode": "default" + } + }, + { + "name": "oidcc-client-config-certification-test-plan", + "config": { + "description": "Configuration Certification" + }, + "variant": { + "client_auth_type": "client_secret_basic", + "request_type": "plain_http_request", + "response_mode": "default", + "client_registration": "dynamic_client" + } + }, + { + "name": "oidcc-client-test-3rd-party-init-login-test-plan", + "config": { + "description": "3rd Party Init" + }, + "variant": { + "client_auth_type": "client_secret_basic", + "request_type": "plain_http_request", + "response_type": "code", + "client_registration": "dynamic_client", + "response_mode": "default" + } + }, + { + "name": "oidcc-client-rp-initiated-logout-rp-basic", + "config": { + "description": "RP Initiated Logout (RP Basic)" + }, + "variant": { + "client_auth_type": "client_secret_basic", + "request_type": "plain_http_request", + "response_mode": "default", + "client_registration": "dynamic_client" + } + } + ] +} \ No newline at end of file diff --git a/certification/implementation_plug.exs b/certification/implementation_plug.exs new file mode 100755 index 0000000..fb4e6eb --- /dev/null +++ b/certification/implementation_plug.exs @@ -0,0 +1,398 @@ +#!/usr/bin/env elixir + +{:ok, handler_config} = :logger.get_handler_config(:default) + +handler_config = + handler_config + |> put_in([:config, :type], :standard_error) + |> put_in([:formatter], Logger.Formatter.new(format: "$message\n")) + +:ok = :logger.remove_handler(:default) +:ok = :logger.add_handler(:default, :logger_std_h, handler_config) + +Mix.start() +Mix.shell(Mix.Shell.Quiet) + +Mix.install([ + {:bandit, "~> 1.0"}, + {:oidcc, path: Path.dirname(__DIR__), override: true}, + {:oidcc_plug, "~> 0.3.1"}, + {:jason, "~> 1.4"}, + {:phoenix_live_view, "~> 1.0"}, + {:phoenix, "~> 1.7"} +]) + +defmodule Oidcc.CommandAndControl do + @moduledoc false + + alias Oidcc.Conformance.ProviderConfiguration + alias Oidcc.ProviderConfiguration.Worker + + require Logger + + defstruct port: nil, public_url: nil, variant: nil, port_reservation: nil, client_jwks: nil + + def start do + JOSE.unsecured_signing(true) + + IO.stream() + |> Stream.map(&String.trim/1) + |> Stream.reject(&(&1 == "")) + |> Stream.map(fn "CMD " <> command -> JSON.decode!(command) end) + |> Enum.reduce(%__MODULE__{}, fn command, state -> + {:ok, response, state} = apply_command(command, state) + + case response do + nil -> IO.puts("ACK") + response -> IO.puts("ACK #{JSON.encode!(response)}") + end + + state + end) + end + + defp apply_command( + %{ + "action" => "init", + "exposed" => %{"issuer" => issuer}, + "public_url" => public_url, + "variant" => variant + }, + state + ) do + {:ok, _pid} = + Worker.start_link(%{ + issuer: issuer, + name: ProviderConfiguration + }) + + {port, port_reservation} = reserve_port() + + {:ok, %{url: public_url, port: port}, + %{ + state + | port: port, + public_url: public_url, + variant: variant, + port_reservation: port_reservation + }} + end + + defp apply_command(%{"action" => "register_client"}, state) do + provider_configuration = Worker.get_provider_configuration(ProviderConfiguration) + + rsa_jwk = %{ + JOSE.JWK.generate_key({:rsa, 2048}) + | fields: %{"use" => "sig", "kid" => "the-one-and-only"} + } + + {_meta, public_jwk} = JOSE.JWK.to_public_map(rsa_jwk) + {_meta, private_jwk} = JOSE.JWK.to_map(rsa_jwk) + public_jwks = JOSE.JWK.from_map(%{"keys" => [public_jwk]}) + private_jwks = JOSE.JWK.from_map(%{"keys" => [private_jwk]}) + + {:ok, %Oidcc.ClientRegistration.Response{client_id: client_id, client_secret: client_secret}} = + Oidcc.ClientRegistration.register(provider_configuration, %Oidcc.ClientRegistration{ + initiate_login_uri: "#{state.public_url}/authorize", + redirect_uris: ["#{state.public_url}/callback"], + userinfo_signed_response_alg: "RS256", + token_endpoint_auth_method: state.variant["client_auth_type"] || "client_secret_basic", + jwks: public_jwks + }) + + {:ok, %{client_id: client_id, client_secret: client_secret}, + %{state | client_jwks: private_jwks}} + end + + defp apply_command( + %{ + "action" => "start_server", + "client_id" => client_id, + "client_secret" => client_secret + }, + state + ) do + Application.put_env(:oidcc_conformance, Oidcc.Conformance.AuthController, + client_id: client_id, + client_secret: client_secret, + client_context_opts: %{client_jwks: state.client_jwks} + ) + + %URI{host: host, path: path} = URI.new!(state.public_url) + + :gen_tcp.close(state.port_reservation) + + {:ok, _} = + Supervisor.start_link( + [ + {Oidcc.Conformance.Endpoint, + adapter: Bandit.PhoenixAdapter, + url: [host: host, scheme: "https", port: 443, path: path], + http: [ + ip: {127, 0, 0, 1}, + port: state.port + ], + render_errors: [ + formats: [html: Oidcc.Conformance.ErrorHTML], + layout: false + ], + server: true, + secret_key_base: String.duplicate("a", 64), + debug_errors: true} + ], + strategy: :one_for_one + ) + + {:ok, nil, state} + end + + defp apply_command(other, _state) do + raise "Unknown command: #{inspect(other)}" + end + + defp reserve_port do + {:ok, listen} = :gen_tcp.listen(0, []) + {:ok, port} = :inet.port(listen) + + {port, listen} + end +end + +defmodule Oidcc.Conformance.ErrorHTML do + use Phoenix.Component + + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end + +defmodule Oidcc.Conformance.AuthController do + use Phoenix.Controller, formats: [:html] + + use Phoenix.VerifiedRoutes, + endpoint: Oidcc.Conformance.Endpoint, + router: Oidcc.Conformance.Router, + statics: [] + + alias Oidcc.Conformance.ProviderConfiguration + alias Oidcc.Plug.AuthorizationCallback + + require Logger + + plug(:put_layout, false) + + plug( + Oidcc.Plug.Authorize, + [ + provider: ProviderConfiguration, + client_id: &__MODULE__.client_id/0, + client_secret: &__MODULE__.client_secret/0, + redirect_uri: &__MODULE__.callback_uri/0, + scopes: ["openid", "profile"], + client_context_opts: &__MODULE__.client_context_opts/0 + ] + when action in [:authorize] + ) + + plug( + AuthorizationCallback, + [ + provider: ProviderConfiguration, + client_id: &__MODULE__.client_id/0, + client_secret: &__MODULE__.client_secret/0, + redirect_uri: &__MODULE__.callback_uri/0, + client_context_opts: &__MODULE__.client_context_opts/0 + ] + when action in [:callback] + ) + + def index(conn, _params) do + render(conn, "index.html") + end + + def logged_in(conn, _params) do + case Plug.Conn.get_session(conn, "oidcc") do + nil -> + redirect(conn, to: ~p"/") + + %{token: token, userinfo: userinfo} -> + render(conn, "logged_in.html", token: token, userinfo: userinfo) + end + end + + def authorize(conn, _params) do + conn + end + + def callback( + %Plug.Conn{private: %{AuthorizationCallback => {:ok, {token, userinfo}}}} = conn, + _params + ) do + conn + |> put_session("oidcc", %{ + userinfo: userinfo, + token: token + }) + |> redirect(to: ~p"/logged-in") + end + + def callback(%Plug.Conn{private: %{AuthorizationCallback => {:error, reason}}} = conn, _params) do + Logger.error("Authorization error: #{inspect(reason)}") + + conn + |> put_status(400) + |> render(:error, reason: reason) + end + + def callback_form(conn, %{"code" => code}) do + # Redirect neccesary since session does not include nonce + # on cross origin post + redirect(conn, to: ~p"/callback?code=#{code}") + end + + def refresh(conn, _params) do + case Plug.Conn.get_session(conn, "oidcc") do + nil -> + redirect(conn, to: ~p"/") + + %{token: token} -> + case refresh_token(token) do + {:ok, {token, userinfo}} -> + conn + |> put_session("oidcc", %{userinfo: userinfo, token: token}) + |> redirect(to: ~p"/logged-in") + + {:error, reason} -> + Logger.error("Refresh error: #{inspect(reason)}") + + conn + |> put_status(400) + |> render(:error, reason: reason) + end + end + end + + defp refresh_token(token) do + with {:ok, token} <- + Oidcc.refresh_token(token, ProviderConfiguration, client_id(), client_secret()), + {:ok, userinfo} <- + Oidcc.retrieve_userinfo( + token, + ProviderConfiguration, + client_id(), + client_secret(), + %{} + ) do + {:ok, {token, userinfo}} + end + end + + @doc false + def client_id do + Application.fetch_env!(:oidcc_conformance, __MODULE__)[:client_id] + end + + @doc false + def client_secret do + Application.fetch_env!(:oidcc_conformance, __MODULE__)[:client_secret] + end + + def client_context_opts do + Application.fetch_env!(:oidcc_conformance, __MODULE__)[:client_context_opts] + end + + @doc false + def callback_uri do + url(~p"/callback") + end +end + +defmodule Oidcc.Conformance.AuthHTML do + use Phoenix.Component + + use Phoenix.VerifiedRoutes, + endpoint: Oidcc.Conformance.Endpoint, + router: Oidcc.Conformance.Router, + statics: [] + + def index(assigns) do + ~H""" +

Hello to the Oidcc Conformance Test Suite!

+ +

Actions

+ + """ + end + + def logged_in(assigns) do + ~H""" +

Hello <%= @userinfo["sub"] %>!

+ +

Token

+
<%= inspect(@token, pretty: true) %>
+ +

Userinfo

+
<%= inspect(@userinfo, pretty: true) %>
+ +

Actions

+ + """ + end + + def error(assigns) do + ~H""" +

Authorization Error

+ +
<%= inspect(@reason, pretty: true) %>
+ """ + end +end + +defmodule Oidcc.Conformance.Router do + use Phoenix.Router + + pipeline :browser do + plug(:fetch_session) + plug(:accepts, ["html"]) + end + + scope "/", Oidcc.Conformance do + pipe_through(:browser) + + get("/", AuthController, :index) + get("/logged-in", AuthController, :logged_in) + get("/authorize", AuthController, :authorize) + get("/callback", AuthController, :callback) + post("/callback", AuthController, :callback_form) + get("/refresh", AuthController, :refresh) + end +end + +defmodule Oidcc.Conformance.Endpoint do + use Phoenix.Endpoint, otp_app: :oidcc_conformance + + @session_options [ + store: :cookie, + key: "_oidcc_conformance_key", + signing_salt: String.duplicate("a", 64), + same_site: "Lax" + ] + + plug(Plug.Session, @session_options) + + plug(Plug.Parsers, parsers: [:urlencoded]) + + plug(Plug.Logger, log: :info) + + plug(Oidcc.Conformance.Router) +end + +Oidcc.CommandAndControl.start() diff --git a/guides/private-key-jwt.md b/guides/private-key-jwt.md new file mode 100644 index 0000000..25e6b8e --- /dev/null +++ b/guides/private-key-jwt.md @@ -0,0 +1,53 @@ +# Using `private_key_jwt` + +To use `private_key_jwt`, you need to provide the private key as a `JOSE.JWK` +wherever `client_context_options` can be provided. + + +You also need to set a dummy client secret for now, so that the client is considered +authenticated. + + + +### Erlang + +```erlang +%% Load key into jwk format +ClientJwk0 = jose_jwk:from_pem(<<"key_pem">>), + +%% Set kid field, to make the computed jwts have a kid header +ClientJwk = ClientJwk0#jose_jwk{ + fields = #{<<"kid">> => <<"private_kid">>} +}, + +%% Refresh token when it expires +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker( + Pid, + <<"client_id">>, + <<"dummy_client_secret">>, + #{client_jwks => ClientJwk} + ). +``` + +### Elixir + +```elixir +# Load key into jwk format +# Set kid field, to make the computed jwts have a kid header +client_jwk = + key + |> JOSE.JWK.from_pem() + |> Map.put(:fields, %{"kid" => kid}) + +# Refresh token when it expires +{ok, client_context} = + Oidcc.ClientContext.from_configuration_worker( + pid, + "client_id", + "dummy_client_secret", + %{client_jwks: client_jwk} + ). +``` + + diff --git a/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex b/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex index adb9780..0248634 100644 --- a/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex +++ b/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex @@ -60,20 +60,16 @@ case Code.ensure_loaded(Igniter.Mix.Task) do end @impl Igniter.Mix.Task - def igniter(igniter, argv) do - # extract positional arguments according to `positional` above - {_arguments, argv} = positional_args!(argv) - # extract options according to `schema` and `aliases` above - options = setup_options(argv, igniter) + def igniter(igniter) do + options = setup_options(igniter) igniter |> configure_issuer(options) |> add_application_worker(options) end - defp setup_options(argv, igniter) do - argv - |> options!() + defp setup_options(igniter) do + igniter.args.options |> Keyword.update( :name, Module.module_name(igniter, "OpenIDProvider"), diff --git a/mix.exs b/mix.exs index 6f7cd6b..8cbddf4 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,7 @@ defmodule Oidcc.Mixfile do {:ex_doc, "~> 0.29", only: :dev, runtime: false}, {:credo, "~> 1.7", only: :dev, runtime: false}, {:dialyxir, "~> 1.4", only: :dev, runtime: false}, - {:igniter, "~> 0.5.43", optional: true} + {:igniter, "~> 0.6.3", optional: true} ] end @@ -77,7 +77,7 @@ defmodule Oidcc.Mixfile do [ source_ref: ref, main: "readme", - extras: ["README.md"], + extras: ["README.md" | Path.wildcard(Path.join(__DIR__, "guides/**/*.md"))], groups_for_modules: [Erlang: [~r/oidcc/], "Elixir": [~r/^Oidcc/]], logo: "assets/logo.svg", assets: %{"assets" => "assets"} diff --git a/src/oidcc.app.src b/src/oidcc.app.src index ad8557b..92deadf 100644 --- a/src/oidcc.app.src +++ b/src/oidcc.app.src @@ -1,6 +1,6 @@ {application, oidcc, [ {description, "OpenID Connect client library for the BEAM."}, - {vsn, "3.5.1"}, + {vsn, "3.5.2"}, {registered, []}, {applications, [kernel, stdlib, inets, ssl, public_key, telemetry, jose]}, {env, []}, diff --git a/src/oidcc_authorization.erl b/src/oidcc_authorization.erl index f148acd..8c29ea6 100644 --- a/src/oidcc_authorization.erl +++ b/src/oidcc_authorization.erl @@ -173,7 +173,7 @@ append_code_challenge(#{pkce_verifier := CodeVerifier} = Opts, QueryParams, Clie ProviderConfiguration, RequirePkce = maps:get(require_pkce, Opts, false), case CodeChallengeMethodsSupported of - undefined when RequirePkce =:= true -> + undefined when RequirePkce -> {error, no_supported_code_challenge}; undefined -> {ok, QueryParams}; @@ -199,7 +199,7 @@ append_code_challenge(#{pkce_verifier := CodeVerifier} = Opts, QueryParams, Clie {<<"code_challenge_method">>, <<"plain">>} | QueryParams ]}; - {false, false} when RequirePkce =:= true -> + {false, false} when RequirePkce -> {error, no_supported_code_challenge}; {false, false} -> {ok, QueryParams} @@ -324,7 +324,7 @@ attempt_request_object( Jwt = jose_jwt:from(Claims), case oidcc_jwt_util:sign(Jwt, SigningJwks, deprioritize_none_alg(SigningAlgSupported)) of - {error, no_supported_alg_or_key} when RequireSignedRequestObject =:= true -> + {error, no_supported_alg_or_key} when RequireSignedRequestObject -> {error, request_object_required}; {error, no_supported_alg_or_key} -> {ok, QueryParams ++ UrlExtension}; diff --git a/src/oidcc_token.erl b/src/oidcc_token.erl index ece75df..d743c13 100644 --- a/src/oidcc_token.erl +++ b/src/oidcc_token.erl @@ -1288,7 +1288,7 @@ add_pkce_verifier(BodyQs, #{pkce_verifier := PkceVerifier} = Opts, ClientContext RequirePkce = maps:get(require_pkce, Opts, false), case CodeChallengeMethodsSupported of - undefined when RequirePkce =:= true -> + undefined when RequirePkce -> {error, no_supported_code_challenge}; undefined -> {ok, BodyQs}; @@ -1299,7 +1299,7 @@ add_pkce_verifier(BodyQs, #{pkce_verifier := PkceVerifier} = Opts, ClientContext of true -> {ok, [{<<"code_verifier">>, PkceVerifier} | BodyQs]}; - false when RequirePkce =:= true -> + false when RequirePkce -> {error, no_supported_code_challenge}; false -> {ok, BodyQs} diff --git a/src/oidcc_userinfo.erl b/src/oidcc_userinfo.erl index 157ea85..320e5a9 100644 --- a/src/oidcc_userinfo.erl +++ b/src/oidcc_userinfo.erl @@ -179,9 +179,7 @@ retrieve(#oidcc_token_access{} = AccessTokenRecord, #oidcc_client_context{} = Cl {ok, Claims} ?= validate_userinfo_body(UserinfoResponse, ClientContext, Opts), lookup_distributed_claims(Claims, ClientContext, Opts) else - {error, {use_dpop_nonce, DpopNonce, _}} when - HasDpopNonce =:= false - -> + {error, {use_dpop_nonce, DpopNonce, _}} when not HasDpopNonce -> %% retry once if we didn't provide a nonce the first time retrieve(AccessTokenRecord, ClientContext, Opts#{dpop_nonce => DpopNonce}); {error, Reason} -> diff --git a/test/oidcc_provider_configuration_worker_test.erl b/test/oidcc_provider_configuration_worker_test.erl index 3d6fe53..1c0c570 100644 --- a/test/oidcc_provider_configuration_worker_test.erl +++ b/test/oidcc_provider_configuration_worker_test.erl @@ -22,6 +22,8 @@ stops_with_invalid_issuer_test() -> receive {'EXIT', Pid, {configuration_load_failed, _Error}} -> ok + after 10000 -> + ?assert(false) end, meck:unload(httpc),