src/oidcc_provider_configuration.erl

-module(oidcc_provider_configuration).

-feature(maybe_expr, enable).

-include("internal/doc.hrl").
?MODULEDOC("""
Tooling to load and parse Openid Configuration.

## Records

To use the record, import the definition:

```erlang
-include_lib(["oidcc/include/oidcc_provider_configuration.hrl"]).
```

## Telemetry

See [`Oidcc.ProviderConfiguration`](`m:'Elixir.Oidcc.ProviderConfiguration'`).
""").
?MODULEDOC(#{since => <<"3.0.0">>}).

-include("oidcc_provider_configuration.hrl").

-export([decode_configuration/1]).
-export([decode_configuration/2]).
-export([load_configuration/1]).
-export([load_configuration/2]).
-export([load_jwks/2]).

-export_type([error/0]).
-export_type([opts/0]).
-export_type([quirks/0]).
-export_type([t/0]).

?DOC("""
Allow Specification Non-compliance.

## Exceptions

* `allow_unsafe_http` - Allow unsafe HTTP. Use this for development
  providers and **never in production**.
* `document_overrides` - a map to merge with the real OIDD document,
  in case the OP left out some values.
* `issuer_regex` - Optional regex pattern to match against the issuer claim
  instead of requiring an exact match. This may be necessary for certain providers that do not
  conform to the OpenID specification, such as Microsoft Entra ID where
  the issuer is 'https://login.microsoftonline.com/{tenantid}/v2.0' in the
  [OpenID configuration](https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration).
""").
?DOC(#{since => <<"3.1.0">>}).
-type quirks() :: #{
    allow_unsafe_http => boolean(),
    document_overrides => map(),
    issuer_regex => binary()
}.

?DOC("""
Configure configuration loading / parsing.

## Parameters

* `fallback_expiry` - How long to keep configuration cached if the server doesn't specify expiry.
* `request_opts` - config for HTTP request.
""").
?DOC(#{since => <<"3.0.0">>}).
-type opts() :: #{
    fallback_expiry => timeout(),
    request_opts => oidcc_http_util:request_opts(),
    quirks => quirks()
}.

?DOC("""
Record containing OpenID and OAuth 2.0 Configuration.

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
* https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata

All unrecognized fields are stored in `extra_fields`.
""").
?DOC(#{since => <<"3.0.0">>}).
-type t() ::
    #oidcc_provider_configuration{
        issuer :: uri_string:uri_string(),
        issuer_regex :: binary() | undefined,
        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 :: [binary()] | undefined,
        response_types_supported :: [binary()],
        response_modes_supported :: [binary()],
        grant_types_supported :: [binary()],
        acr_values_supported :: [binary()] | undefined,
        subject_types_supported :: [pairwise | public],
        id_token_signing_alg_values_supported :: [binary()],
        id_token_encryption_alg_values_supported ::
            [binary()] | undefined,
        id_token_encryption_enc_values_supported ::
            [binary()] | undefined,
        userinfo_signing_alg_values_supported :: [binary()] | undefined,
        userinfo_encryption_alg_values_supported ::
            [binary()] | undefined,
        userinfo_encryption_enc_values_supported ::
            [binary()] | undefined,
        request_object_signing_alg_values_supported ::
            [binary()] | undefined,
        request_object_encryption_alg_values_supported ::
            [binary()] | undefined,
        request_object_encryption_enc_values_supported ::
            [binary()] | undefined,
        token_endpoint_auth_methods_supported :: [binary()],
        token_endpoint_auth_signing_alg_values_supported ::
            [binary()] | undefined,
        display_values_supported :: [binary()] | undefined,
        claim_types_supported :: [normal | aggregated | distributed],
        claims_supported :: [binary()] | undefined,
        service_documentation :: uri_string:uri_string() | undefined,
        claims_locales_supported :: [binary()] | undefined,
        ui_locales_supported :: [binary()] | 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 :: [binary()],
        revocation_endpoint_auth_signing_alg_values_supported ::
            [binary()] | undefined,
        introspection_endpoint :: uri_string:uri_string() | undefined,
        introspection_endpoint_auth_methods_supported :: [binary()],
        introspection_endpoint_auth_signing_alg_values_supported ::
            [binary()] | undefined,
        code_challenge_methods_supported :: [binary()] | undefined,
        end_session_endpoint :: uri_string:uri_string() | undefined,
        require_pushed_authorization_requests :: boolean(),
        pushed_authorization_request_endpoint :: uri_string:uri_string() | undefined,
        authorization_signing_alg_values_supported :: [binary()] | undefined,
        authorization_encryption_alg_values_supported :: [binary()] | undefined,
        authorization_encryption_enc_values_supported :: [binary()] | undefined,
        authorization_response_iss_parameter_supported :: boolean(),
        dpop_signing_alg_values_supported :: [binary()] | undefined,
        require_signed_request_object :: boolean(),
        mtls_endpoint_aliases :: #{binary() => uri_string:uri_string()},
        extra_fields :: #{binary() => term()}
    }.

?DOC(#{since => <<"3.0.0">>}).
-type error() ::
    invalid_content_type
    | {issuer_mismatch, Issuer :: binary()}
    | oidcc_decode_util:error()
    | oidcc_http_util:error().

-define(DEFAULT_CONFIG_EXPIRY, timer:minutes(15)).

-telemetry_event(#{
    event => [oidcc, load_configuration, start],
    description => <<"Emitted at the start of loading the provider configuration">>,
    measurements => <<"#{system_time => non_neg_integer()}">>,
    metadata => <<"#{issuer => uri_string:uri_string()}">>
}).

-telemetry_event(#{
    event => [oidcc, load_configuration, stop],
    description => <<"Emitted at the end of loading the provider configuration">>,
    measurements => <<"#{duration => integer(), monotonic_time => integer()}">>,
    metadata => <<"#{issuer => uri_string:uri_string()}">>
}).

-telemetry_event(#{
    event => [oidcc, load_configuration, exception],
    description => <<"Emitted at the end of loading the provider configuration">>,
    measurements => <<"#{duration => integer(), monotonic_time => integer()}">>,
    metadata => <<"#{issuer => uri_string:uri_string()}">>
}).

-telemetry_event(#{
    event => [oidcc, load_jwks, start],
    description => <<"Emitted at the start of loading the provider jwks">>,
    measurements => <<"#{system_time => non_neg_integer()}">>,
    metadata => <<"#{jwks_uri => uri_string:uri_string()}">>
}).

-telemetry_event(#{
    event => [oidcc, load_jwks, stop],
    description => <<"Emitted at the end of loading the provider jwks">>,
    measurements => <<"#{duration => integer(), monotonic_time => integer()}">>,
    metadata => <<"#{jwks_uri => uri_string:uri_string()}">>
}).

-telemetry_event(#{
    event => [oidcc, load_jwks, exception],
    description => <<"Emitted at the end of loading the provider jwks">>,
    measurements => <<"#{duration => integer(), monotonic_time => integer()}">>,
    metadata => <<"#{jwks_uri => uri_string:uri_string()}">>
}).

?DOC("""
Load OpenID Configuration into a `t:oidcc_provider_configuration:t/0` record.

## Examples

```erlang
{ok, #oidcc_provider_configuration{}} =
  oidcc_provider_configuration:load_configuration("https://accounts.google.com").
```
""").
?DOC(#{since => <<"3.0.0">>}).
-spec load_configuration(Issuer, Opts) ->
    {ok, {Configuration :: t(), Expiry :: pos_integer()}} | {error, error()}
when
    Issuer :: uri_string:uri_string(),
    Opts :: opts().
load_configuration(Issuer0, Opts) ->
    Issuer = binary:list_to_bin([Issuer0]),
    TelemetryOpts = #{topic => [oidcc, load_configuration], extra_meta => #{issuer => Issuer}},
    RequestOpts = maps:get(request_opts, Opts, #{}),

    RequestUrl = url_join(".well-known/openid-configuration", Issuer),
    Request = {RequestUrl, []},

    Quirks = maps:get(quirks, Opts, #{}),
    % this quirk is deprecated, but we keep the support for backwards compatibility.
    AllowIssuerMismatch = maps:get(allow_issuer_mismatch, Quirks, false),

    DefaultExpiry = maps:get(fallback_expiry, Opts, ?DEFAULT_CONFIG_EXPIRY),

    maybe
        {ok, {{json, ConfigurationMap}, Headers}} ?=
            oidcc_http_util:request(get, Request, TelemetryOpts, RequestOpts),
        Expiry = oidcc_http_util:headers_to_cache_deadline(Headers, DefaultExpiry),
        {ok,
            #oidcc_provider_configuration{issuer = ConfigIssuer, issuer_regex = ConfigIssuerRegex} =
                Configuration} ?=
            decode_configuration(ConfigurationMap, #{quirks => Quirks}),
        case ConfigIssuer of
            Issuer ->
                {ok, {Configuration, Expiry}};
            _ when is_binary(ConfigIssuerRegex) ->
                case re:run(Issuer, ConfigIssuerRegex, [{capture, none}]) of
                    match ->
                        {ok, {Configuration, Expiry}};
                    nomatch ->
                        {error, {issuer_mismatch, ConfigIssuer}}
                end;
            _DifferentIssuer when AllowIssuerMismatch -> {ok, {Configuration, Expiry}};
            DifferentIssuer when not AllowIssuerMismatch ->
                {error, {issuer_mismatch, DifferentIssuer}}
        end
    else
        {error, Reason} ->
            {error, Reason};
        {ok, {{_Format, _Body}, _Headers}} ->
            {error, invalid_content_type}
    end.

?DOC("See `load_configuration/2`.").
?DOC(#{since => <<"3.1.0">>}).
-spec load_configuration(Issuer) ->
    {ok, {Configuration :: t(), Expiry :: pos_integer()}} | {error, error()}
when
    Issuer :: uri_string:uri_string().
load_configuration(Issuer) -> load_configuration(Issuer, #{}).

?DOC("""
Load JWKs into a `t:jose_jwk:key/0` record.

## Examples

```erlang
{ok, #jose_jwk{}} =
  oidcc_provider_configuration:load_jwks("https://www.googleapis.com/oauth2/v3/certs").
```
""").
?DOC(#{since => <<"3.0.0">>}).
-spec load_jwks(JwksUri, Opts) ->
    {ok, {Jwks :: jose_jwk:key(), Expiry :: pos_integer()}} | {error, term()}
when
    JwksUri :: uri_string:uri_string(),
    Opts :: opts().
load_jwks(JwksUri, Opts) ->
    TelemetryOpts = #{topic => [oidcc, load_jwks], extra_meta => #{jwks_uri => JwksUri}},
    RequestOpts = maps:get(request_opts, Opts, #{}),

    DefaultExpiry = maps:get(fallback_expiry, Opts, ?DEFAULT_CONFIG_EXPIRY),

    maybe
        {ok, {{json, JwksBinary}, Headers}} ?=
            oidcc_http_util:request(get, {JwksUri, []}, TelemetryOpts, RequestOpts),
        Expiry = oidcc_http_util:headers_to_cache_deadline(Headers, DefaultExpiry),
        Jwks = jose_jwk:from(JwksBinary),
        {ok, {Jwks, Expiry}}
    else
        {error, Reason} -> {error, Reason};
        {ok, {{_Format, _Body}, _Headers}} -> {error, invalid_content_type}
    end.

?DOC("""
Decode JSON into a `t:oidcc_provider_configuration:t/0` record.

## Examples

```erlang
{ok, {{"HTTP/1.1",200,"OK"}, _Headers, Body}} =
  httpc:request("https://accounts.google.com/.well-known/openid-configuration"),

{ok, DecodedJson} = your_json_lib:decode(Body),

{ok, #oidcc_provider_configuration{}} =
  oidcc_provider_configuration:decode_configuration(DecodedJson).
```
""").
?DOC(#{since => <<"3.1.0">>}).
-spec decode_configuration(Configuration, Opts) -> {ok, t()} | {error, error()} when
    Configuration :: map(), Opts :: opts().
decode_configuration(Configuration0, Opts) ->
    Quirks = maps:get(quirks, Opts, #{}),
    AllowUnsafeHttp = maps:get(allow_unsafe_http, Quirks, false),
    IssuerRegex = maps:get(issuer_regex, Quirks, undefined),

    DocumentOverrides = maps:get(document_overrides, Quirks, #{}),
    Configuration = maps:merge(Configuration0, DocumentOverrides),

    maybe
        {ok, {
            #{
                issuer := Issuer,
                authorization_endpoint := AuthorizationEndpoint,
                authorization_endpoint := AuthorizationEndpoint,
                token_endpoint := TokenEndpoint,
                userinfo_endpoint := UserinfoEndpoint,
                jwks_uri := JwksUri,
                registration_endpoint := RegistrationEndpoint,
                scopes_supported := ScopesSupported,
                response_types_supported := ResponseTypesSupported,
                response_modes_supported := ResponseModesSupported,
                grant_types_supported := GrantTypesSupported,
                acr_values_supported := AcrValuesSupported,
                subject_types_supported := SubjectTypesSupported,
                id_token_signing_alg_values_supported := IdTokenSigningAlgValuesSupported,
                id_token_encryption_alg_values_supported := IdTokenEncryptionAlgValuesSupported,
                id_token_encryption_enc_values_supported := IdTokenEncryptionEncValuesSupported,
                userinfo_signing_alg_values_supported := UserinfoSigningAlgValuesSupported,
                userinfo_encryption_alg_values_supported := UserinfoEncryptionAlgValuesSupported,
                userinfo_encryption_enc_values_supported := UserinfoEncryptionEncValuesSupported,
                request_object_signing_alg_values_supported :=
                    RequestObjectSigningAlgValuesSupported,
                request_object_encryption_alg_values_supported :=
                    RequestObjectEncryptionAlgValuesSupported,
                request_object_encryption_enc_values_supported :=
                    RequestObjectEncryptionEncValuesSupported,
                token_endpoint_auth_methods_supported := TokenEndpointAuthMethodsSupported,
                token_endpoint_auth_signing_alg_values_supported :=
                    TokenEndpointAuthSigningAlgValuesSupported,
                display_values_supported := DisplayValuesSupported,
                claim_types_supported := ClaimTypesSupported,
                claims_supported := ClaimsSupported,
                service_documentation := ServiceDocumentation,
                claims_locales_supported := ClaimsLocalesSupported,
                ui_locales_supported := UiLocalesSupported,
                claims_parameter_supported := ClaimsParameterSupported,
                request_parameter_supported := RequestParameterSupported,
                request_uri_parameter_supported := RequestUriParameterSupported,
                require_request_uri_registration := RequireRequestUriRegistration,
                op_policy_uri := OpPolicyUri,
                op_tos_uri := OpTosUri,
                revocation_endpoint := RevocationEndpoint,
                revocation_endpoint_auth_methods_supported :=
                    RevocationEndpointAuthMethodsSupported,
                revocation_endpoint_auth_signing_alg_values_supported :=
                    RevocationEndpointAuthSigningAlgValuesSupported,
                introspection_endpoint := IntrospectionEndpoint,
                introspection_endpoint_auth_methods_supported :=
                    IntrospectionEndpointAuthMethodsSupported,
                introspection_endpoint_auth_signing_alg_values_supported :=
                    IntrospectionEndpointAuthSigningAlgValuesSupported,
                code_challenge_methods_supported := CodeChallengeMethodsSupported,
                end_session_endpoint := EndSessionEndpoint,
                require_pushed_authorization_requests := RequirePushedAuthorizationRequests,
                pushed_authorization_request_endpoint := PushedAuthorizationRequestEndpoint,
                authorization_signing_alg_values_supported :=
                    AuthorizationSigningAlgValuesSupported,
                authorization_encryption_alg_values_supported :=
                    AuthorizationEncryptionAlgValuesSupported,
                authorization_encryption_enc_values_supported :=
                    AuthorizationEncryptionEncValuesSupported,
                authorization_response_iss_parameter_supported :=
                    AuthorizationResponseIssParameterSupported,
                dpop_signing_alg_values_supported := DpopSigningAlgValuesSupported,
                require_signed_request_object := RequireSignedRequestObject,
                mtls_endpoint_aliases := MtlsEndpointAliases,
                tls_client_certificate_bound_access_tokens := TlsClientCertificateBoundAccessTokens
            },
            ExtraFields
        }} ?=
            oidcc_decode_util:extract(
                Configuration,
                [
                    {required, issuer, fun oidcc_decode_util:parse_setting_uri/2},
                    {required, authorization_endpoint, fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, token_endpoint, undefined,
                        fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, userinfo_endpoint, undefined,
                        case AllowUnsafeHttp of
                            true -> fun oidcc_decode_util:parse_setting_uri/2;
                            false -> fun oidcc_decode_util:parse_setting_uri_https/2
                        end},
                    {required, jwks_uri, fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, registration_endpoint, undefined,
                        fun oidcc_decode_util:parse_setting_uri/2},
                    {required, scopes_supported, fun parse_scopes_supported/2},
                    {required, response_types_supported,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, response_modes_supported, [<<"query">>, <<"fragment">>],
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, grant_types_supported, [<<"authorization_code">>, <<"implicit">>],
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, acr_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {required, subject_types_supported, fun parse_subject_types_supported/2},
                    {required, id_token_signing_alg_values_supported,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, id_token_encryption_alg_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, id_token_encryption_enc_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, userinfo_signing_alg_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, userinfo_encryption_alg_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, userinfo_encryption_enc_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, request_object_signing_alg_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, request_object_encryption_alg_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, request_object_encryption_enc_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, token_endpoint_auth_methods_supported, [<<"client_secret_basic">>],
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, token_endpoint_auth_signing_alg_values_supported, undefined,
                        fun parse_token_signing_alg_values_no_none/2},
                    {optional, display_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, claim_types_supported, [normal], fun parse_claim_types_supported/2},
                    {optional, claims_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, service_documentation, undefined,
                        fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, claims_locales_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, ui_locales_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, claims_parameter_supported, false,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, request_parameter_supported, false,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, request_uri_parameter_supported, true,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, require_request_uri_registration, false,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, op_policy_uri, undefined, fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, op_tos_uri, undefined, fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, revocation_endpoint, undefined,
                        fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, revocation_endpoint_auth_methods_supported,
                        [<<"client_secret_basic">>],
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, revocation_endpoint_auth_signing_alg_values_supported, undefined,
                        fun parse_token_signing_alg_values_no_none/2},
                    {optional, introspection_endpoint, undefined,
                        fun oidcc_decode_util:parse_setting_uri/2},
                    {optional, introspection_endpoint_auth_methods_supported,
                        [<<"client_secret_basic">>],
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, introspection_endpoint_auth_signing_alg_values_supported, undefined,
                        fun parse_token_signing_alg_values_no_none/2},
                    {optional, code_challenge_methods_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, end_session_endpoint, undefined,
                        case AllowUnsafeHttp of
                            true -> fun oidcc_decode_util:parse_setting_uri/2;
                            false -> fun oidcc_decode_util:parse_setting_uri_https/2
                        end},
                    {optional, require_pushed_authorization_requests, false,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, pushed_authorization_request_endpoint, undefined,
                        case AllowUnsafeHttp of
                            true -> fun oidcc_decode_util:parse_setting_uri/2;
                            false -> fun oidcc_decode_util:parse_setting_uri_https/2
                        end},
                    {optional, authorization_signing_alg_values_supported, undefined,
                        fun parse_token_signing_alg_values_no_none/2},
                    {optional, authorization_encryption_alg_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, authorization_encryption_enc_values_supported, undefined,
                        fun oidcc_decode_util:parse_setting_binary_list/2},
                    {optional, authorization_response_iss_parameter_supported, false,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, dpop_signing_alg_values_supported, undefined,
                        fun parse_token_signing_alg_values_no_none/2},
                    {optional, require_signed_request_object, false,
                        fun oidcc_decode_util:parse_setting_boolean/2},
                    {optional, mtls_endpoint_aliases, #{},
                        case AllowUnsafeHttp of
                            true -> fun oidcc_decode_util:parse_setting_uri_map/2;
                            false -> fun oidcc_decode_util:parse_setting_uri_https_map/2
                        end},
                    {optional, tls_client_certificate_bound_access_tokens, false,
                        fun oidcc_decode_util:parse_setting_boolean/2}
                ],
                #{}
            ),
        {ok, #oidcc_provider_configuration{
            issuer = Issuer,
            issuer_regex = IssuerRegex,
            authorization_endpoint = AuthorizationEndpoint,
            token_endpoint = TokenEndpoint,
            userinfo_endpoint = UserinfoEndpoint,
            jwks_uri = JwksUri,
            registration_endpoint = RegistrationEndpoint,
            scopes_supported = ScopesSupported,
            response_types_supported = ResponseTypesSupported,
            response_modes_supported = ResponseModesSupported,
            grant_types_supported = GrantTypesSupported,
            acr_values_supported = AcrValuesSupported,
            subject_types_supported = SubjectTypesSupported,
            id_token_signing_alg_values_supported =
                IdTokenSigningAlgValuesSupported,
            id_token_encryption_alg_values_supported =
                IdTokenEncryptionAlgValuesSupported,
            id_token_encryption_enc_values_supported =
                IdTokenEncryptionEncValuesSupported,
            userinfo_signing_alg_values_supported =
                UserinfoSigningAlgValuesSupported,
            userinfo_encryption_alg_values_supported =
                UserinfoEncryptionAlgValuesSupported,
            userinfo_encryption_enc_values_supported =
                UserinfoEncryptionEncValuesSupported,
            request_object_signing_alg_values_supported =
                RequestObjectSigningAlgValuesSupported,
            request_object_encryption_alg_values_supported =
                RequestObjectEncryptionAlgValuesSupported,
            request_object_encryption_enc_values_supported =
                RequestObjectEncryptionEncValuesSupported,
            token_endpoint_auth_methods_supported =
                TokenEndpointAuthMethodsSupported,
            token_endpoint_auth_signing_alg_values_supported =
                TokenEndpointAuthSigningAlgValuesSupported,
            display_values_supported = DisplayValuesSupported,
            claim_types_supported = ClaimTypesSupported,
            claims_supported = ClaimsSupported,
            service_documentation = ServiceDocumentation,
            claims_locales_supported = ClaimsLocalesSupported,
            ui_locales_supported = UiLocalesSupported,
            claims_parameter_supported = ClaimsParameterSupported,
            request_parameter_supported = RequestParameterSupported,
            request_uri_parameter_supported =
                RequestUriParameterSupported,
            require_request_uri_registration =
                RequireRequestUriRegistration,
            op_policy_uri = OpPolicyUri,
            op_tos_uri = OpTosUri,
            revocation_endpoint = RevocationEndpoint,
            revocation_endpoint_auth_methods_supported =
                RevocationEndpointAuthMethodsSupported,
            revocation_endpoint_auth_signing_alg_values_supported =
                RevocationEndpointAuthSigningAlgValuesSupported,
            introspection_endpoint = IntrospectionEndpoint,
            introspection_endpoint_auth_methods_supported =
                IntrospectionEndpointAuthMethodsSupported,
            introspection_endpoint_auth_signing_alg_values_supported =
                IntrospectionEndpointAuthSigningAlgValuesSupported,
            code_challenge_methods_supported =
                CodeChallengeMethodsSupported,
            end_session_endpoint = EndSessionEndpoint,
            require_pushed_authorization_requests = RequirePushedAuthorizationRequests,
            pushed_authorization_request_endpoint = PushedAuthorizationRequestEndpoint,
            authorization_signing_alg_values_supported = AuthorizationSigningAlgValuesSupported,
            authorization_encryption_alg_values_supported =
                AuthorizationEncryptionAlgValuesSupported,
            authorization_encryption_enc_values_supported =
                AuthorizationEncryptionEncValuesSupported,
            authorization_response_iss_parameter_supported =
                AuthorizationResponseIssParameterSupported,
            dpop_signing_alg_values_supported = DpopSigningAlgValuesSupported,
            require_signed_request_object = RequireSignedRequestObject,
            mtls_endpoint_aliases = MtlsEndpointAliases,
            tls_client_certificate_bound_access_tokens = TlsClientCertificateBoundAccessTokens,
            extra_fields = ExtraFields
        }}
    end.

?DOC("See `decode_configuration/2`.").
?DOC(#{since => <<"3.0.0">>}).
-spec decode_configuration(Configuration) -> {ok, t()} | {error, error()} when
    Configuration :: map().
decode_configuration(Configuration) -> decode_configuration(Configuration, #{}).

-spec parse_scopes_supported(Setting :: term(), Field :: atom()) ->
    {ok, [binary()]} | {error, error()}.
parse_scopes_supported(Setting, Field) ->
    case oidcc_decode_util:parse_setting_binary_list(Setting, Field) of
        {ok, Scopes} ->
            case lists:member(<<"openid">>, Scopes) of
                true ->
                    {ok, Scopes};
                false ->
                    {error, {invalid_config_property, {scopes_including_openid, Field}}}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

-spec parse_subject_types_supported(Setting :: term(), Field :: atom()) ->
    {ok, [binary()]} | {error, error()}.
parse_subject_types_supported(Setting, Field) ->
    oidcc_decode_util:parse_setting_list_enum(
        Setting,
        Field,
        fun
            (<<"pairwise">>) ->
                {ok, pairwise};
            (<<"public">>) ->
                {ok, public};
            (_SubjectType) ->
                error
        end
    ).

-spec parse_token_signing_alg_values_no_none(Setting :: term(), Field :: atom()) ->
    {ok, [binary()]} | {error, error()}.
parse_token_signing_alg_values_no_none(Setting, Field) ->
    case oidcc_decode_util:parse_setting_binary_list(Setting, Field) of
        {ok, SigningAlgValues} ->
            case
                lists:any(
                    fun
                        (<<"none">>) ->
                            true;
                        (_) ->
                            false
                    end,
                    SigningAlgValues
                )
            of
                false ->
                    {ok, SigningAlgValues};
                true ->
                    {error, {invalid_config_property, {alg_no_none, Field}}}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

-spec parse_claim_types_supported(Setting :: term(), Field :: atom()) ->
    {ok, [binary()]} | {error, error()}.
parse_claim_types_supported(Setting, Field) ->
    oidcc_decode_util:parse_setting_list_enum(
        Setting,
        Field,
        fun
            (<<"normal">>) ->
                {ok, normal};
            (<<"aggregated">>) ->
                {ok, aggregated};
            (<<"distributed">>) ->
                {ok, distributed};
            (_ClaimType) ->
                error
        end
    ).

-spec url_join(RefURI :: uri_string:uri_string(), BaseURI :: uri_string:uri_string()) ->
    uri_string:uri_string().
url_join(RefURI, BaseURI) ->
    BaseURIBinary = iolist_to_binary(BaseURI),
    case binary_part(BaseURIBinary, byte_size(BaseURIBinary) - 1, 1) of
        <<"/">> -> uri_string:resolve(RefURI, BaseURI);
        _ -> uri_string:resolve(RefURI, [BaseURI, "/"])
    end.