diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..e10d3d3 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,216 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/" + ], + excluded: [~r"/_build/", ~r"/deps/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: true, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Readability.Specs, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.formatter.exs b/.formatter.exs index 067c54d..9ecf1cd 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,10 @@ [ - inputs: ["conformance/**/*.{ex,exs}"] + inputs: [ + "lib/**/*.ex", + "test/**/*.exs", + "conformance/**/*.{ex,exs}", + ".formatter.exs", + ".credo.exs", + "mix.exs" + ] ] diff --git a/.github/workflows/branch_main.yml b/.github/workflows/branch_main.yml index bdd8a7e..90f03d0 100644 --- a/.github/workflows/branch_main.yml +++ b/.github/workflows/branch_main.yml @@ -22,6 +22,7 @@ jobs: with: otpVersion: "${{ needs.detectToolVersions.outputs.otpVersion }}" rebarVersion: "${{ needs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: "${{ needs.detectToolVersions.outputs.elixirVersion }}" docs: name: "Docs" @@ -32,3 +33,4 @@ jobs: with: otpVersion: "${{ needs.detectToolVersions.outputs.otpVersion }}" rebarVersion: "${{ needs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: "${{ needs.detectToolVersions.outputs.elixirVersion }}" diff --git a/.github/workflows/part_docs.yml b/.github/workflows/part_docs.yml index 3f5a6d1..ff6ac9a 100644 --- a/.github/workflows/part_docs.yml +++ b/.github/workflows/part_docs.yml @@ -7,6 +7,9 @@ on: rebarVersion: required: true type: string + elixirVersion: + required: true + type: string releaseName: required: false type: string @@ -26,13 +29,21 @@ jobs: with: otp-version: ${{ inputs.otpVersion }} rebar3-version: ${{ inputs.rebarVersion }} + elixir-version: ${{ inputs.elixirVersion }} - uses: actions/cache@v3 with: path: _build - key: docs-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + key: docs-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + docs-build-{{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}- + - uses: actions/cache@v3 + with: + path: deps + key: docs-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-${{ hashFiles('rebar.lock') }} restore-keys: | - docs-{{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - - run: rebar3 ex_doc + docs-bdepsuild-{{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}- + - run: mix deps.get + - run: mix docs - uses: actions/upload-artifact@v3 with: name: docs diff --git a/.github/workflows/part_test.yml b/.github/workflows/part_test.yml index 51b67e7..894d304 100644 --- a/.github/workflows/part_test.yml +++ b/.github/workflows/part_test.yml @@ -7,6 +7,9 @@ on: rebarVersion: required: true type: string + elixirVersion: + required: true + type: string name: "Test" @@ -14,8 +17,8 @@ env: ERL_AFLAGS: "-enable-feature all" jobs: - format: - name: Check Formatting + rebar_format: + name: Check Rebar Formatting runs-on: ubuntu-latest @@ -29,11 +32,39 @@ jobs: - uses: actions/cache@v3 with: path: _build - key: format-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + key: rebar_format-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} restore-keys: | - format-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + rebar_format-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - run: rebar3 fmt --check + mix_format: + name: Check Mix Formatting + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + id: setupBEAM + with: + otp-version: ${{ inputs.otpVersion }} + rebar3-version: ${{ inputs.rebarVersion }} + elixir-version: ${{ inputs.elixirVersion }} + - uses: actions/cache@v3 + with: + path: _build + key: mix_format-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + mix_format-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - uses: actions/cache@v3 + with: + path: deps + key: mix_format-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + mix_format-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - run: mix deps.get + - run: mix format --check-formatted + eunit: name: Run EUnit @@ -89,12 +120,44 @@ jobs: name: ct-coverage path: _build/test/cover/ct.coverdata + mix_test: + name: Run Mix Tests + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + id: setupBEAM + with: + otp-version: ${{ inputs.otpVersion }} + rebar3-version: ${{ inputs.rebarVersion }} + elixir-version: ${{ inputs.elixirVersion }} + - uses: actions/cache@v3 + with: + path: _build + key: mix_test-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + mix_test-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - uses: actions/cache@v3 + with: + path: deps + key: mix_test-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + mix_test-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - run: mix deps.get + - run: mix test --cover --export-coverage mix_test + - uses: actions/upload-artifact@v3 + with: + name: mix_test-coverage + path: cover/mix_test.coverdata + coverage: name: Process Test Coverage runs-on: ubuntu-latest - needs: ["eunit", "conformance"] + needs: ["eunit", "conformance", "mix_test"] steps: - uses: actions/checkout@v3 @@ -117,6 +180,10 @@ jobs: with: name: eunit-coverage path: _build/test/cover/ + - uses: actions/download-artifact@v3 + with: + name: mix_test-coverage + path: _build/test/cover/ - run: rebar3 cover - uses: actions/upload-artifact@v3 with: @@ -128,7 +195,7 @@ jobs: runs-on: ubuntu-latest - needs: ["eunit", "conformance"] + needs: ["eunit", "conformance", "mix_test"] steps: - uses: actions/checkout@v3 @@ -151,6 +218,10 @@ jobs: with: name: eunit-coverage path: _build/test/cover/ + - uses: actions/download-artifact@v3 + with: + name: mix_test-coverage + path: _build/test/cover/ - uses: actions/upload-artifact@v3 with: name: coverage-report @@ -179,6 +250,62 @@ jobs: lint-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- - run: rebar3 lint + credo: + name: Run Credo + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + id: setupBEAM + with: + otp-version: ${{ inputs.otpVersion }} + rebar3-version: ${{ inputs.rebarVersion }} + elixir-version: ${{ inputs.elixirVersion }} + - uses: actions/cache@v3 + with: + path: _build + key: credo-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + credo-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - uses: actions/cache@v3 + with: + path: deps + key: credo-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + credo-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - run: mix deps.get + - run: mix credo + + dialyxir: + name: Run Dialyxir + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + id: setupBEAM + with: + otp-version: ${{ inputs.otpVersion }} + rebar3-version: ${{ inputs.rebarVersion }} + elixir-version: ${{ inputs.elixirVersion }} + - uses: actions/cache@v3 + with: + path: _build + key: dialyxir-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + dialyxir-build-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - uses: actions/cache@v3 + with: + path: deps + key: dialyxir-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ hashFiles('rebar.lock') }} + restore-keys: | + dialyxir-deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}- + - run: mix deps.get + - run: mix dialyzer + dialyzer: name: Dialyzer diff --git a/.github/workflows/part_tool_versioning.yml b/.github/workflows/part_tool_versioning.yml index f68db69..59150b3 100644 --- a/.github/workflows/part_tool_versioning.yml +++ b/.github/workflows/part_tool_versioning.yml @@ -7,6 +7,9 @@ on: rebarVersion: description: "The .tool-versions Rebar version" value: "${{ jobs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: + description: "The .tool-versions Elixir version" + value: "${{ jobs.detectToolVersions.outputs.elixirVersion }}" name: "Detect Tool Versions" @@ -19,6 +22,7 @@ jobs: outputs: otpVersion: "${{ env.OTP_VERSION }}" rebarVersion: "${{ env.REBAR_VERSION }}" + elixirVersion: "${{ env.ELIXIR_VERSION }}" steps: - uses: actions/checkout@v3 @@ -32,3 +36,7 @@ jobs: REBAR_VERSION="$(cat .tool-versions | grep rebar | cut -d' ' -f2-)" echo Rebar: $REBAR_VERSION echo "REBAR_VERSION=${REBAR_VERSION}" >> $GITHUB_ENV + + ELIXIR_VERSION="$(cat .tool-versions | grep elixir | cut -d' ' -f2-)" + echo Rebar: $ELIXIR_VERSION + echo "ELIXIR_VERSION=${ELIXIR_VERSION}" >> $GITHUB_ENV diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a7f950f..a3bad32 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,6 +21,7 @@ jobs: with: otpVersion: "${{ needs.detectToolVersions.outputs.otpVersion }}" rebarVersion: "${{ needs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: "${{ needs.detectToolVersions.outputs.elixirVersion }}" docs: name: "Docs" @@ -31,3 +32,4 @@ jobs: with: otpVersion: "${{ needs.detectToolVersions.outputs.otpVersion }}" rebarVersion: "${{ needs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: "${{ needs.detectToolVersions.outputs.elixirVersion }}" diff --git a/.github/workflows/tag-beta.yml b/.github/workflows/tag-beta.yml index c61fcc6..8b23575 100644 --- a/.github/workflows/tag-beta.yml +++ b/.github/workflows/tag-beta.yml @@ -27,4 +27,5 @@ jobs: with: otpVersion: "${{ needs.detectToolVersions.outputs.otpVersion }}" rebarVersion: "${{ needs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: "${{ needs.detectToolVersions.outputs.elixirVersion }}" releaseName: "${{ github.ref_name }}" diff --git a/.github/workflows/tag-stable.yml b/.github/workflows/tag-stable.yml index af31c20..f4beb93 100644 --- a/.github/workflows/tag-stable.yml +++ b/.github/workflows/tag-stable.yml @@ -28,4 +28,5 @@ jobs: with: otpVersion: "${{ needs.detectToolVersions.outputs.otpVersion }}" rebarVersion: "${{ needs.detectToolVersions.outputs.rebarVersion }}" + elixirVersion: "${{ needs.detectToolVersions.outputs.elixirVersion }}" releaseName: "${{ github.ref_name }}" diff --git a/.tool-versions b/.tool-versions index c07058f..34e95a5 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ erlang 26.0.2 rebar 3.22.1 +elixir 1.15.5 diff --git a/lib/oidcc.ex b/lib/oidcc.ex new file mode 100644 index 0000000..a1c1786 --- /dev/null +++ b/lib/oidcc.ex @@ -0,0 +1,3 @@ +defmodule Oidcc do + @moduledoc "foo" +end diff --git a/lib/oidcc/provider_configuration.ex b/lib/oidcc/provider_configuration.ex new file mode 100644 index 0000000..231bd29 --- /dev/null +++ b/lib/oidcc/provider_configuration.ex @@ -0,0 +1,97 @@ +defmodule Oidcc.ProviderConfiguration do + @moduledoc """ + Tooling to load and parse Openid Configuration + """ + + import Record, only: [defrecordp: 3, extract: 2] + + defrecordp :configuration, + :oidcc_provider_configuration, + extract(:oidcc_provider_configuration, + from: "include/oidcc_provider_configuration.hrl" + ) + + defstruct extract(:oidcc_provider_configuration, + from: "include/oidcc_provider_configuration.hrl" + ) + + @typedoc """ + Configuration Struct + + For details on the fields see: + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + * https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1 + """ + @type t() :: %__MODULE__{ + issuer: :uri_string.uri_string(), + authorization_endpoint: :uri_string.uri_string(), + token_endpoint: :uri_string.uri_string() | :undefined, + userinfo_endpoint: :uri_string.uri_string() | :undefined, + jwks_uri: :uri_string.uri_string() | :undefined, + registration_endpoint: :uri_string.uri_string() | :undefined, + scopes_supported: [String.t()] | :undefined, + response_types_supported: [String.t()], + response_modes_supported: [String.t()], + grant_types_supported: [String.t()], + acr_values_supported: [String.t()] | :undefined, + subject_types_supported: [:pairwise | :public], + id_token_signing_alg_values_supported: [String.t()], + id_token_encryption_alg_values_supported: [String.t()] | :undefined, + id_token_encryption_enc_values_supported: [String.t()] | :undefined, + userinfo_signing_alg_values_supported: [String.t()] | :undefined, + userinfo_encryption_alg_values_supported: [String.t()] | :undefined, + userinfo_encryption_enc_values_supported: [String.t()] | :undefined, + request_object_signing_alg_values_supported: [String.t()] | :undefined, + request_object_encryption_alg_values_supported: [String.t()] | :undefined, + request_object_encryption_enc_values_supported: [String.t()] | :undefined, + token_endpoint_auth_methods_supported: [String.t()], + token_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined, + display_values_supported: [String.t()] | :undefined, + claim_types_supported: [:normal | :aggregated | :distributed], + claims_supported: [String.t()] | :undefined, + service_documentation: :uri_string.uri_string() | :undefined, + claims_locales_supported: [String.t()] | :undefined, + ui_locales_supported: [String.t()] | :undefined, + claims_parameter_supported: boolean(), + request_parameter_supported: boolean(), + request_uri_parameter_supported: boolean(), + require_request_uri_registration: boolean(), + op_policy_uri: :uri_string.uri_string() | :undefined, + op_tos_uri: :uri_string.uri_string() | :undefined, + revocation_endpoint: :uri_string.uri_string() | :undefined, + revocation_endpoint_auth_methods_supported: [String.t()], + revocation_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined, + introspection_endpoint: :uri_string.uri_string() | :undefined, + introspection_endpoint_auth_methods_supported: [String.t()], + introspection_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined, + code_challenge_methods_supported: [String.t()] | :undefined, + extra_fields: %{String.t() => term()} + } + + @doc false + @spec record_to_struct(record :: :oidcc_provider_configuration.t()) :: t() + def record_to_struct(record), do: struct!(__MODULE__, configuration(record)) + + @doc """ + Load OpenID Configuration + + ## Examples + + iex> {:ok, { + ...> %ProviderConfiguration{issuer: "https://accounts.google.com"}, + ...> _expiry + ...> }} = Oidcc.ProviderConfiguration.load_configuration("https://accounts.google.com") + """ + @spec load_configuration( + issuer :: :uri_string.uri_string(), + opts :: :oidcc_provider_configuration.opts() + ) :: + {:ok, {configuration :: t(), expiry :: pos_integer()}} + | {:error, :oidcc_provider_configuration.error()} + def load_configuration(issuer, opts \\ %{}) do + with {:ok, {configuration, expiry}} <- + :oidcc_provider_configuration.load_configuration(issuer, opts) do + {:ok, {record_to_struct(configuration), expiry}} + end + end +end diff --git a/lib/oidcc/provider_configuration/worker.ex b/lib/oidcc/provider_configuration/worker.ex new file mode 100644 index 0000000..acd3a8b --- /dev/null +++ b/lib/oidcc/provider_configuration/worker.ex @@ -0,0 +1,139 @@ +defmodule Oidcc.ProviderConfiguration.Worker do + @moduledoc """ + OIDC Config Provider Worker + + Loads and continuously refreshes the OIDC configuration and JWKs + + ## Usage in Supervisor + + ```elixir + Supervisor.init([ + {Oidcc.ProviderConfiguration.Worker, %{issuer: "https://accounts.google.com/"}} + ], strategy: :one_for_one) + ``` + """ + + alias Oidcc.ProviderConfiguration + + @typedoc """ + See `:oidcc_provider_configuration_worker.opts/0` + """ + @type opts() :: %{ + optional(:name) => GenServer.name(), + required(:issuer) => :uri_string.uri_string(), + optional(:provider_configuration_opts) => :oidcc_provider_configuration.opts() + } + + @doc """ + Start Configuration Worker + + ## Examples + + iex> {:ok, _pid} = + ...> Oidcc.ProviderConfiguration.Worker.start_link(%{ + ...> issuer: "https://accounts.google.com/", + ...> name: MyApp.GoogleConfigProvider + ...> }) + """ + @spec start_link(opts :: :oidcc_provider_configuration_worker.opts()) :: GenServer.on_start() + def start_link(opts) + + def start_link(%{name: name} = opts) when is_atom(name), + do: start_link(%{opts | name: {:local, name}}) + + def start_link(opts), do: :oidcc_provider_configuration_worker.start_link(opts) + + @spec child_spec(opts :: :oidcc_provider_configuration_worker.opts()) :: Supervisor.child_spec() + def child_spec(opts), + do: + Supervisor.child_spec( + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [opts]} + }, + [] + ) + + @doc """ + Get Configuration + + ## Examples + + iex> {:ok, pid} = + ...> Oidcc.ProviderConfiguration.Worker.start_link(%{ + ...> issuer: "https://accounts.google.com/" + ...> }) + ...> %Oidcc.ProviderConfiguration{issuer: "https://accounts.google.com"} = + ...> Oidcc.ProviderConfiguration.Worker.get_provider_configuration(pid) + """ + @spec get_provider_configuration(name :: GenServer.name()) :: ProviderConfiguration.t() + def get_provider_configuration(name), + do: + name + |> :oidcc_provider_configuration_worker.get_provider_configuration() + |> ProviderConfiguration.record_to_struct() + + @doc """ + Get Parsed Jwks + + ## Examples + + iex> {:ok, pid} = + ...> Oidcc.ProviderConfiguration.Worker.start_link(%{ + ...> issuer: "https://accounts.google.com/" + ...> }) + ...> %JOSE.JWK{} = + ...> Oidcc.ProviderConfiguration.Worker.get_jwks(pid) + """ + @spec get_jwks(name :: GenServer.name()) :: JOSE.JWK.t() + def get_jwks(name), + do: + name + |> :oidcc_provider_configuration_worker.get_jwks() + |> JOSE.JWK.from_record() + + @doc """ + Refresh Configuration + + ## Examples + + iex> {:ok, pid} = + ...> Oidcc.ProviderConfiguration.Worker.start_link(%{ + ...> issuer: "https://accounts.google.com/" + ...> }) + ...> :ok = Oidcc.ProviderConfiguration.Worker.refresh_configuration(pid) + """ + @spec refresh_configuration(name :: GenServer.name()) :: :ok + def refresh_configuration(name), + do: :oidcc_provider_configuration_worker.refresh_configuration(name) + + @doc """ + Refresh JWKs + + ## Examples + + iex> {:ok, pid} = + ...> Oidcc.ProviderConfiguration.Worker.start_link(%{ + ...> issuer: "https://accounts.google.com/" + ...> }) + ...> :ok = Oidcc.ProviderConfiguration.Worker.refresh_jwks(pid) + """ + @spec refresh_jwks(name :: GenServer.name()) :: :ok + def refresh_jwks(name), + do: :oidcc_provider_configuration_worker.refresh_jwks(name) + + @doc """ + Refresh JWKs if the provided `Kid` is not matching any currently loaded keys + + ## Examples + + iex> {:ok, pid} = + ...> Oidcc.ProviderConfiguration.Worker.start_link(%{ + ...> issuer: "https://accounts.google.com/" + ...> }) + ...> :ok = Oidcc.ProviderConfiguration.Worker.refresh_jwks_for_unknown_kid(pid, "kid") + """ + @spec refresh_jwks_for_unknown_kid(name :: GenServer.name(), kid :: String.t()) :: :ok + def refresh_jwks_for_unknown_kid(name, kid), + do: :oidcc_provider_configuration_worker.refresh_jwks_for_unknown_kid(name, kid) +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..26c584d --- /dev/null +++ b/mix.exs @@ -0,0 +1,77 @@ +defmodule Oidcc.Mixfile do + use Mix.Project + + {:ok, [{:application, :oidcc, props}]} = :file.consult(~c"src/oidcc.app.src") + @props Keyword.take(props, [:applications, :description, :env, :mod, :licenses, :vsn]) + + def project() do + [ + app: :oidcc, + version: to_string(@props[:vsn]), + elixir: "~> 1.15", + erlc_options: erlc_options(Mix.env()), + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps(), + name: "Oidcc", + source_url: "https://github.com/Erlang-Openid/oidcc", + docs: &docs/0, + description: to_string(@props[:description]), + package: package(), + aliases: [docs: ["compile", &rebar3_doc_chunks/1, "docs"]] + ] + end + + def application() do + [extra_applications: [:inets, :ssl]] + end + + defp deps() do + [ + {:telemetry, "~> 1.2"}, + {:jose, "~> 1.11"}, + {:jsx, "~> 3.1"}, + {:ex_doc, "~> 0.29.4", only: :dev, runtime: false}, + {:credo, "~> 1.7", only: :dev, runtime: false}, + {:dialyxir, "~> 1.4", only: :dev, runtime: false} + ] + end + + defp erlc_options(:prod), do: [] + + defp erlc_options(_enc), + do: [:debug_info, :warn_unused_import, :warn_export_vars, :warnings_as_errors, :verbose] + + defp package() do + [ + maintainers: ["Jonatan Männchen"], + build_tools: ["rebar3", "mix"], + files: [ + "include", + "lib", + "LICENSE*", + "mix.exs", + "README*", + "rebar.config", + "src" + ], + licenses: @props[:licenses], + links: %{"Github" => "https://github.com/Erlang-Openid/oidcc"} + ] + end + + defp docs do + {ref, 0} = System.cmd("git", ["rev-parse", "--verify", "--quiet", "HEAD"]) + + [ + source_ref: ref, + main: "Oidcc", + extras: ["README.md"], + groups_for_modules: [Erlang: [~r/oidcc/], "Elixir": [~r/Oidcc/]] + ] + end + + defp rebar3_doc_chunks(_args) do + {_out, 0} = System.cmd("rebar3", ["edoc"], into: IO.stream()) + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..4870549 --- /dev/null +++ b/mix.lock @@ -0,0 +1,17 @@ +%{ + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, + "jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, +} diff --git a/rebar.config b/rebar.config index 097c514..d3905e6 100644 --- a/rebar.config +++ b/rebar.config @@ -6,26 +6,20 @@ {project_plugins, [ coveralls, - rebar3_ex_doc, erlfmt, rebar3_hank, - rebar3_hex, - rebar3_lint, - rebar3_sbom + rebar3_lint ]}. {validate_app_modules, true}. -{dialyzer, [{warnings, [no_return, unmatched_returns, error_handling, no_underspecs]}]}. - -{ex_doc, [ - {extras, ["README.md", "LICENSE"]}, - {main, "README.md"}, - {source_url, "https://github.com/Erlang-Openid/oidcc"} +{edoc_opts, [ + {doclet, edoc_doclet_chunks}, + {layout, edoc_layout_chunks}, + {preprocess, true}, + {dir, "_build/dev/lib/oidcc/doc"} ]}. -{hex, [{doc, #{provider => ex_doc}}]}. - {hank, [{ignore, [{"test/**/*_SUITE.erl", [unnecessary_function_arguments]}, "include/**/*.hrl"]}]}. {erlfmt, [write]}. @@ -40,6 +34,4 @@ {cover_opts, [verbose]}. -{shell, - % {config, "config/sys.config"}, - [{apps, [oidcc]}]}. +{shell, [{apps, [oidcc]}]}. diff --git a/test/oidcc/provider_configuration/worker_test.exs b/test/oidcc/provider_configuration/worker_test.exs new file mode 100644 index 0000000..404525c --- /dev/null +++ b/test/oidcc/provider_configuration/worker_test.exs @@ -0,0 +1,72 @@ +defmodule Oidcc.ProviderConfiguration.WorkerTest do + use ExUnit.Case + + alias Oidcc.ProviderConfiguration + alias Oidcc.ProviderConfiguration.Worker + + doctest Worker + + describe inspect(&Worker.start_link/1) do + test "works" do + start_supervised!( + {Worker, %{issuer: "https://accounts.google.com/", name: __MODULE__.GoogleProvider}} + ) + end + end + + describe inspect(&Worker.get_provider_configuration/1) do + test "works" do + pid = + start_supervised!( + {Worker, %{issuer: "https://accounts.google.com/", name: __MODULE__.GoogleProvider}} + ) + + assert %ProviderConfiguration{issuer: "https://accounts.google.com"} = + Worker.get_provider_configuration(pid) + end + end + + describe inspect(&Worker.get_jwks/1) do + test "works" do + start_supervised!( + {Worker, %{issuer: "https://accounts.google.com/", name: __MODULE__.GoogleProvider}} + ) + + assert %JOSE.JWK{} = + Worker.get_jwks(__MODULE__.GoogleProvider) + end + end + + describe inspect(&Worker.refresh_configuration/1) do + test "works" do + pid = + start_supervised!( + {Worker, %{issuer: "https://accounts.google.com/", name: __MODULE__.GoogleProvider}} + ) + + assert :ok = Worker.refresh_configuration(pid) + end + end + + describe inspect(&Worker.refresh_jwks/1) do + test "works" do + pid = + start_supervised!( + {Worker, %{issuer: "https://accounts.google.com/", name: __MODULE__.GoogleProvider}} + ) + + assert :ok = Worker.refresh_jwks(pid) + end + end + + describe inspect(&Worker.refresh_jwks_for_unknown_kid/2) do + test "works" do + pid = + start_supervised!( + {Worker, %{issuer: "https://accounts.google.com/", name: __MODULE__.GoogleProvider}} + ) + + assert :ok = Worker.refresh_jwks_for_unknown_kid(pid, "kid") + end + end +end diff --git a/test/oidcc/provider_configuration_test.exs b/test/oidcc/provider_configuration_test.exs new file mode 100644 index 0000000..199dad3 --- /dev/null +++ b/test/oidcc/provider_configuration_test.exs @@ -0,0 +1,14 @@ +defmodule Oidcc.ProviderConfigurationTest do + use ExUnit.Case + + alias Oidcc.ProviderConfiguration + + doctest ProviderConfiguration + + describe inspect(&ProviderConfiguration.load_configuration/2) do + test "works" do + assert {:ok, {%ProviderConfiguration{issuer: "https://accounts.google.com"}, _expiry}} = + ProviderConfiguration.load_configuration("https://accounts.google.com", %{}) + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()