%%%------------------------------------------------------------------- %% @doc Functions to start an OpenID Connect Authorization %% @end %% @since 3.0.0 %%%------------------------------------------------------------------- -module(oidcc_authorization). -feature(maybe_expr, enable). -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). -include_lib("jose/include/jose_jwk.hrl"). -export([create_redirect_url/2]). -export_type([error/0]). -export_type([opts/0]). -type opts() :: #{ scopes => oidcc_scope:scopes(), state => binary(), nonce => binary(), pkce_verifier => binary(), redirect_uri := uri_string:uri_string(), url_extension => oidcc_http_util:query_params() }. %% Configure authorization redirect url %% %% See [https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest] %% %%

Parameters

%% %% -type error() :: {grant_type_not_supported, authorization_code}. %% @doc %% Create Auth Redirect URL %% %% For a high level interface using {@link oidcc_provider_configuration_worker} %% see {@link oidcc:create_redirect_url/4}. %% %%

Examples

%% %% ``` %% {ok, ClientContext} = %% oidcc_client_context:from_configuration_worker(provider_name, %% <<"client_id">>, %% <<"client_secret">>), %% %% {ok, RedirectUri} = %% oidcc_authorization:create_redirect_url(ClientContext, %% #{redirect_uri: <<"https://my.server/return"}), %% %% %% RedirectUri = https://my.provider/auth?scope=openid&response_type=code&client_id=client_id&redirect_uri=https%3A%2F%2Fmy.server%2Freturn %% ''' %% @end %% @since 3.0.0 -spec create_redirect_url(ClientContext, Opts) -> {ok, Uri} | {error, error()} when ClientContext :: oidcc_client_context:t(), Opts :: opts(), Uri :: uri_string:uri_string(). create_redirect_url(#oidcc_client_context{} = ClientContext, Opts) -> #oidcc_client_context{provider_configuration = ProviderConfiguration} = ClientContext, #oidcc_provider_configuration{ authorization_endpoint = AuthEndpoint, grant_types_supported = GrantTypesSupported } = ProviderConfiguration, case lists:member(<<"authorization_code">>, GrantTypesSupported) of true -> QueryParams = redirect_params(ClientContext, Opts), QueryString = uri_string:compose_query(QueryParams), {ok, [AuthEndpoint, <<"?">>, QueryString]}; false -> {error, {grant_type_not_supported, authorization_code}} end. -spec redirect_params(ClientContext, Opts) -> oidcc_http_util:query_params() when ClientContext :: oidcc_client_context:t(), Opts :: opts(). redirect_params(#oidcc_client_context{client_id = ClientId} = ClientContext, Opts) -> QueryParams = [ {<<"response_type">>, maps:get(response_type, Opts, <<"code">>)}, {<<"client_id">>, ClientId}, {<<"redirect_uri">>, maps:get(redirect_uri, Opts)} | maps:get(url_extension, Opts, []) ], QueryParams1 = maybe_append(<<"state">>, maps:get(state, Opts, undefined), QueryParams), QueryParams2 = maybe_append(<<"nonce">>, maps:get(nonce, Opts, undefined), QueryParams1), QueryParams3 = append_code_challenge( maps:get(pkce_verifier, Opts, none), QueryParams2, ClientContext ), QueryParams4 = oidcc_scope:query_append_scope( maps:get(scopes, Opts, [openid]), QueryParams3 ), attempt_request_object(QueryParams4, ClientContext). -spec append_code_challenge(PkceVerifier, QueryParams, ClientContext) -> oidcc_http_util:query_params() when PkceVerifier :: binary() | none, QueryParams :: oidcc_http_util:query_params(), ClientContext :: oidcc_client_context:t(). append_code_challenge(none, QueryParams, _ClientContext) -> QueryParams; append_code_challenge(CodeVerifier, QueryParams, ClientContext) -> #oidcc_client_context{provider_configuration = ProviderConfiguration} = ClientContext, #oidcc_provider_configuration{code_challenge_methods_supported = CodeChallengeMethodsSupported} = ProviderConfiguration, case CodeChallengeMethodsSupported of undefined -> QueryParams; Methods when is_list(Methods) -> case { lists:member(<<"S256">>, CodeChallengeMethodsSupported), lists:member(<<"plain">>, CodeChallengeMethodsSupported) } of {true, _PlainSupported} -> CodeChallenge = base64:encode(crypto:hash(sha256, CodeVerifier), #{ mode => urlsafe, padding => false }), [ {"code_challenge", CodeChallenge}, {"code_challenge_method", <<"S256">>} | QueryParams ]; {false, true} -> [ {"code_challenge", CodeVerifier}, {"code_challenge_method", <<"plain">>} | QueryParams ]; {false, false} -> QueryParams end end. -spec maybe_append(Key, Value, QueryParams) -> QueryParams when Key :: unicode:chardata(), Value :: unicode:chardata() | true | undefined, QueryParams :: oidcc_http_util:query_params(). maybe_append(_Key, undefined, QueryParams) -> QueryParams; maybe_append(Key, Value, QueryParams) -> [{Key, Value} | QueryParams]. -spec attempt_request_object(QueryParams, ClientContext) -> QueryParams when QueryParams :: oidcc_http_util:query_params(), ClientContext :: oidcc_client_context:t(). attempt_request_object(QueryParams, #oidcc_client_context{ provider_configuration = #oidcc_provider_configuration{request_parameter_supported = false} }) -> QueryParams; attempt_request_object(QueryParams, #oidcc_client_context{client_secret = unauthenticated}) -> QueryParams; attempt_request_object(QueryParams, #oidcc_client_context{ client_id = ClientId, client_secret = ClientSecret, client_jwks = ClientJwks, provider_configuration = #oidcc_provider_configuration{ issuer = Issuer, request_parameter_supported = true, request_object_signing_alg_values_supported = SigningAlgSupported0, request_object_encryption_alg_values_supported = EncryptionAlgSupported0, request_object_encryption_enc_values_supported = EncryptionEncSupported0 }, jwks = Jwks }) -> SigningAlgSupported = case SigningAlgSupported0 of undefined -> []; SigningAlgs -> SigningAlgs end, EncryptionAlgSupported = case EncryptionAlgSupported0 of undefined -> []; EncryptionAlgs -> EncryptionAlgs end, EncryptionEncSupported = case EncryptionEncSupported0 of undefined -> []; EncryptionEncs -> EncryptionEncs end, JwksWithClientJwks = case ClientJwks of none -> Jwks; #jose_jwk{} -> oidcc_jwt_util:merge_jwks(Jwks, ClientJwks) end, SigningJwks = case oidcc_jwt_util:client_secret_oct_keys(SigningAlgSupported, ClientSecret) of none -> Jwks; SigningOctJwk -> oidcc_jwt_util:merge_jwks(JwksWithClientJwks, SigningOctJwk) end, EncryptionJwks = case oidcc_jwt_util:client_secret_oct_keys(EncryptionAlgSupported, ClientSecret) of none -> Jwks; EncryptionOctJwk -> oidcc_jwt_util:merge_jwks(JwksWithClientJwks, EncryptionOctJwk) end, MaxClockSkew = case application:get_env(oidcc, max_clock_skew) of undefined -> 0; ClockSkew -> ClockSkew end, Claims = maps:merge( #{ <<"iss">> => ClientId, <<"aud">> => Issuer, <<"jti">> => random_string(32), <<"iat">> => os:system_time(seconds), <<"exp">> => os:system_time(seconds) + 30, <<"nbf">> => os:system_time(seconds) - MaxClockSkew }, maps:from_list(QueryParams) ), Jwt = jose_jwt:from(Claims), case oidcc_jwt_util:sign(Jwt, SigningJwks, deprioritize_none_alg(SigningAlgSupported)) of {error, no_supported_alg_or_key} -> QueryParams; {ok, SignedRequestObject} -> case oidcc_jwt_util:encrypt( SignedRequestObject, EncryptionJwks, deprioritize_none_alg(EncryptionAlgSupported), EncryptionEncSupported ) of {ok, EncryptedRequestObject} -> [{<<"request">>, EncryptedRequestObject} | essential_params(QueryParams)]; {error, no_supported_alg_or_key} -> [{<<"request">>, SignedRequestObject} | essential_params(QueryParams)] end end. -spec essential_params(QueryParams :: oidcc_http_util:query_params()) -> oidcc_http_util:query_params(). essential_params(QueryParams) -> lists:filter( fun ({<<"scope">>, _Value}) -> true; ({<<"response_type">>, _Value}) -> true; ({<<"client_id">>, _Value}) -> true; ({<<"redirect_uri">>, _Value}) -> true; (_Other) -> false end, QueryParams ). -spec deprioritize_none_alg(Algorithms :: [binary()]) -> [binary()]. deprioritize_none_alg(Algorithms) -> lists:usort( fun (<<"none">>, _B) -> false; (_A, <<"none">>) -> true; (_A, _B) -> true end, Algorithms ). -spec random_string(Bytes :: pos_integer()) -> binary(). random_string(Bytes) -> base64:encode(crypto:strong_rand_bytes(Bytes), #{mode => urlsafe, padding => false}).