src/oidcc_provider_configuration.erl

%%%-------------------------------------------------------------------
%% @doc Tooling to load and parse Openid Configuration
%%
%% <h2>Records</h2>
%%
%% To use the record, import the definition:
%%
%% ```
%% -include_lib(["oidcc/include/oidcc_provider_configuration.hrl"]).
%% '''
%%
%% <h2>Telemetry</h2>
%%
%% See {@link 'Elixir.Oidcc.ProviderConfiguration'}
%% @end
%% @since 3.0.0
%%%-------------------------------------------------------------------
-module(oidcc_provider_configuration).

-feature(maybe_expr, enable).

-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]).

-type quirks() :: #{
    allow_issuer_mismatch => boolean(),
    allow_unsafe_http => boolean()
}.
%% Allow Specification Non-compliance
%%
%% <h2>Exceptions</h2>
%%
%% <ul>
%%   <li>`allow_issuer_mismatch' - Allow issuer mismatch between config issuer
%%     and function parameter</li>
%%   <li>`allow_unsafe_http' - Allow unsafe HTTP. Use this for development
%%     providers and <strong>never in production</strong>.</li>
%% </ul>

-type opts() :: #{
    fallback_expiry => timeout(),
    request_opts => oidcc_http_util:request_opts(),
    quirks => quirks()
}.
%% Configure configuration loading / parsing
%%
%% <h2>Parameters</h2>
%%
%% <ul>
%%   <li>`fallback_expiry' - How long to keep configuration cached if the server doesn't specify expiry</li>
%%   <li>`request_opts' - config for HTTP request</li>
%% </ul>

-type t() ::
    #oidcc_provider_configuration{
        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 :: [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,
        extra_fields :: #{binary() => term()}
    }.
%% 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] and
%% [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata]
%%
%% All unrecognized fields are stored in `extra_fields'.

-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 {@link oidcc_provider_configuration:t()} record
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, #oidcc_provider_configuration{}} =
%%   oidcc_provider_configuration:load_configuration("https://accounts.google.com").
%% '''
%% @end
%% @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 = uri_string:resolve(".well-known/openid-configuration", Issuer),
    Request = {RequestUrl, []},

    Quirks = maps:get(quirks, Opts, #{}),
    AllowIssuerMismatch = maps:get(allow_issuer_mismatch, Quirks, false),

    maybe
        {ok, {{json, ConfigurationMap}, Headers}} ?=
            oidcc_http_util:request(get, Request, TelemetryOpts, RequestOpts),
        Expiry = headers_to_deadline(Headers, Opts),
        {ok, #oidcc_provider_configuration{issuer = ConfigIssuer} = Configuration} ?=
            decode_configuration(ConfigurationMap, #{quirks => Quirks}),
        case ConfigIssuer of
            Issuer ->
                {ok, {Configuration, Expiry}};
            _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.

%% @see load_configuration/2
%% @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 {@link jose_jwk:key()} record
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, #jose_jwk{}} =
%%   oidcc_provider_configuration:load_jwks("https://www.googleapis.com/oauth2/v3/certs").
%% '''
%% @end
%% @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, #{}),

    maybe
        {ok, {{json, JwksBinary}, Headers}} ?=
            oidcc_http_util:request(get, {JwksUri, []}, TelemetryOpts, RequestOpts),
        Expiry = headers_to_deadline(Headers, Opts),
        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 {@link oidcc_provider_configuration:t()} record
%%
%% <h2>Examples</h2>
%%
%% ```
%% {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).
%% '''
%% @end
%% @since 3.1.0
-spec decode_configuration(Configuration, Opts) -> {ok, t()} | {error, error()} when
    Configuration :: map(), Opts :: opts().
decode_configuration(Configuration, Opts) ->
    Quirks = maps:get(quirks, Opts, #{}),
    AllowUnsafeHttp = maps:get(allow_unsafe_http, Quirks, false),

    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
            },
            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, undefined,
                        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}
                ],
                #{}
            ),
        {ok, #oidcc_provider_configuration{
            issuer = Issuer,
            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,
            extra_fields = ExtraFields
        }}
    end.

%% @see decode_configuration/2
%% @since 3.0.0
-spec decode_configuration(Configuration) -> {ok, t()} | {error, error()} when
    Configuration :: map().
decode_configuration(Configuration) -> decode_configuration(Configuration, #{}).

-spec headers_to_deadline(Headers, Opts) -> pos_integer() when
    Headers :: [{Header :: binary(), Value :: binary()}], Opts :: opts().
headers_to_deadline(Headers, Opts) ->
    DefaultExpiry = maps:get(fallback_expiry, Opts, ?DEFAULT_CONFIG_EXPIRY),
    case proplists:lookup("cache-control", Headers) of
        {"cache-control", Cache} ->
            try
                cache_deadline(Cache, DefaultExpiry)
            catch
                _:_ ->
                    DefaultExpiry
            end;
        none ->
            DefaultExpiry
    end.

-spec cache_deadline(Cache :: iodata(), Fallback :: pos_integer()) -> pos_integer().
cache_deadline(Cache, Fallback) ->
    Entries =
        binary:split(iolist_to_binary(Cache), [<<",">>, <<"=">>, <<" ">>], [global, trim_all]),
    MaxAge =
        fun
            (<<"0">>, Res) ->
                Res;
            (Entry, true) ->
                erlang:convert_time_unit(binary_to_integer(Entry), second, millisecond);
            (<<"max-age">>, _) ->
                true;
            (_, Res) ->
                Res
        end,
    lists:foldl(MaxAge, Fallback, Entries).

-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
    ).