%%%-------------------------------------------------------------------
%% @doc Authentication Utilities
%% @end
%%%-------------------------------------------------------------------
-module(oidcc_auth_util).
-feature(maybe_expr, enable).
-include("oidcc_client_context.hrl").
-include("oidcc_provider_configuration.hrl").
-include_lib("jose/include/jose_jwk.hrl").
-export_type([auth_method/0, error/0]).
-type auth_method() ::
none | client_secret_basic | client_secret_post | client_secret_jwt | private_key_jwt.
-type error() :: no_supported_auth_method.
-export([add_client_authentication/6]).
%% @private
-spec add_client_authentication(
QueryList, Header, SupportedAuthMethods, AllowAlgorithms, Opts, ClientContext
) ->
{ok, {oidcc_http_util:query_params(), [oidcc_http_util:http_header()]}}
| {error, error()}
when
QueryList :: oidcc_http_util:query_params(),
Header :: [oidcc_http_util:http_header()],
SupportedAuthMethods :: [binary()] | undefined,
AllowAlgorithms :: [binary()] | undefined,
Opts :: map(),
ClientContext :: oidcc_client_context:t().
add_client_authentication(_QueryList, _Header, undefined, _AllowAlgs, _Opts, _ClientContext) ->
{error, no_supported_auth_method};
add_client_authentication(
QueryList0, Header0, SupportedAuthMethods, AllowAlgorithms, Opts, ClientContext
) ->
PreferredAuthMethods = maps:get(preferred_auth_methods, Opts, [
private_key_jwt,
client_secret_jwt,
client_secret_post,
client_secret_basic,
none
]),
case select_preferred_auth(PreferredAuthMethods, SupportedAuthMethods) of
{ok, AuthMethod} ->
case
add_authentication(QueryList0, Header0, AuthMethod, AllowAlgorithms, ClientContext)
of
{ok, {QueryList, Header}} ->
{ok, {QueryList, Header}};
{error, _} ->
add_client_authentication(
QueryList0,
Header0,
SupportedAuthMethods -- [atom_to_binary(AuthMethod)],
AllowAlgorithms,
Opts,
ClientContext
)
end;
{error, Reason} ->
{error, Reason}
end.
-spec add_authentication(
QueryList,
Header,
AuthMethod,
AllowAlgorithms,
ClientContext
) ->
{ok, {oidcc_http_util:query_params(), [oidcc_http_util:http_header()]}}
| {error, auth_method_not_possible}
when
QueryList :: oidcc_http_util:query_params(),
Header :: [oidcc_http_util:http_header()],
AuthMethod :: auth_method(),
AllowAlgorithms :: [binary()] | undefined,
ClientContext :: oidcc_client_context:t().
add_authentication(
QsBodyList,
Header,
none,
_AllowArgs,
#oidcc_client_context{client_id = ClientId}
) ->
NewBodyList = [{<<"client_id">>, ClientId} | QsBodyList],
{ok, {NewBodyList, Header}};
add_authentication(
_QsBodyList,
_Header,
_Method,
_AllowAlgs,
#oidcc_client_context{client_secret = unauthenticated}
) ->
{error, auth_method_not_possible};
add_authentication(
QsBodyList,
Header,
client_secret_basic,
_AllowAlgs,
#oidcc_client_context{client_id = ClientId, client_secret = ClientSecret}
) ->
NewHeader = [oidcc_http_util:basic_auth_header(ClientId, ClientSecret) | Header],
{ok, {QsBodyList, NewHeader}};
add_authentication(
QsBodyList,
Header,
client_secret_post,
_AllowAlgs,
#oidcc_client_context{client_id = ClientId, client_secret = ClientSecret}
) ->
NewBodyList =
[{<<"client_id">>, ClientId}, {<<"client_secret">>, ClientSecret} | QsBodyList],
{ok, {NewBodyList, Header}};
add_authentication(
QsBodyList,
Header,
client_secret_jwt,
AllowAlgorithms,
ClientContext
) ->
#oidcc_client_context{
client_secret = ClientSecret
} = ClientContext,
maybe
[_ | _] ?= AllowAlgorithms,
#jose_jwk{} =
OctJwk ?=
oidcc_jwt_util:client_secret_oct_keys(
AllowAlgorithms,
ClientSecret
),
{ok, ClientAssertion} ?=
signed_client_assertion(
AllowAlgorithms,
ClientContext,
OctJwk
),
{ok, add_jwt_bearer_assertion(ClientAssertion, QsBodyList, Header, ClientContext)}
else
_ ->
{error, auth_method_not_possible}
end;
add_authentication(
QsBodyList,
Header,
private_key_jwt,
AllowAlgorithms,
ClientContext
) ->
#oidcc_client_context{
client_jwks = ClientJwks
} = ClientContext,
maybe
[_ | _] ?= AllowAlgorithms,
#jose_jwk{} ?= ClientJwks,
{ok, ClientAssertion} ?=
signed_client_assertion(AllowAlgorithms, ClientContext, ClientJwks),
{ok, add_jwt_bearer_assertion(ClientAssertion, QsBodyList, Header, ClientContext)}
else
_ ->
{error, auth_method_not_possible}
end.
-spec select_preferred_auth(PreferredAuthMethods, AuthMethodsSupported) ->
{ok, auth_method()} | {error, error()}
when
PreferredAuthMethods :: [auth_method(), ...],
AuthMethodsSupported :: [binary()].
select_preferred_auth(PreferredAuthMethods, AuthMethodsSupported) ->
PreferredAuthMethodSearchFun = fun(AuthMethod) ->
lists:member(atom_to_binary(AuthMethod), AuthMethodsSupported)
end,
case lists:search(PreferredAuthMethodSearchFun, PreferredAuthMethods) of
{value, AuthMethod} ->
{ok, AuthMethod};
false ->
{error, no_supported_auth_method}
end.
-spec signed_client_assertion(AllowAlgorithms, ClientContext, Jwk) ->
{ok, binary()} | {error, term()}
when
AllowAlgorithms :: [binary()],
Jwk :: jose_jwk:key(),
ClientContext :: oidcc_client_context:t().
signed_client_assertion(AllowAlgorithms, ClientContext, Jwk) ->
Jwt = jose_jwt:from(token_request_claims(ClientContext)),
oidcc_jwt_util:sign(Jwt, Jwk, AllowAlgorithms).
-spec token_request_claims(ClientContext) -> oidcc_jwt_util:claims() when
ClientContext :: oidcc_client_context:t().
token_request_claims(#oidcc_client_context{
client_id = ClientId,
provider_configuration = #oidcc_provider_configuration{token_endpoint = TokenEndpoint}
}) ->
MaxClockSkew =
case application:get_env(oidcc, max_clock_skew) of
undefined -> 0;
{ok, ClockSkew} -> ClockSkew
end,
#{
<<"iss">> => ClientId,
<<"sub">> => ClientId,
<<"aud">> => TokenEndpoint,
<<"jti">> => random_string(32),
<<"iat">> => os:system_time(seconds),
<<"exp">> => os:system_time(seconds) + 30,
<<"nbf">> => os:system_time(seconds) - MaxClockSkew
}.
-spec add_jwt_bearer_assertion(ClientAssertion, Body, Header, ClientContext) -> {Body, Header} when
ClientAssertion :: binary(),
Body :: oidcc_http_util:query_params(),
Header :: [oidcc_http_util:http_header()],
ClientContext :: oidcc_client_context:t().
add_jwt_bearer_assertion(ClientAssertion, Body, Header, ClientContext) ->
#oidcc_client_context{client_id = ClientId} = ClientContext,
{
[
{<<"client_assertion_type">>,
<<"urn:ietf:params:oauth:client-assertion-type:jwt-bearer">>},
{<<"client_assertion">>, ClientAssertion},
{<<"client_id">>, ClientId}
| Body
],
Header
}.
-spec random_string(Bytes :: pos_integer()) -> binary().
random_string(Bytes) ->
base64:encode(crypto:strong_rand_bytes(Bytes), #{mode => urlsafe, padding => false}).