Skip to content

Commit

Permalink
feat: preferred_auth_methods argument for request_token
Browse files Browse the repository at this point in the history
Fixes #289
  • Loading branch information
paulswartz committed Nov 21, 2023
1 parent 4cdb654 commit 78455e4
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 32 deletions.
57 changes: 25 additions & 32 deletions src/oidcc_token.erl
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
pkce_verifier => binary(),
nonce => binary() | any,
scope => oidcc_scope:scopes(),
preferred_auth_methods => [auth_method(), ...],
refresh_jwks => oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun(),
redirect_uri := uri_string:uri_string(),
request_opts => oidcc_http_util:request_opts()
Expand Down Expand Up @@ -834,8 +835,18 @@ retrieve_a_token(QsBodyIn, PkceVerifier, ClientContext, Opts, TelemetryOpts, Aut

MaybeAuthMethod =
case AuthenticateClient of
true -> select_preferred_auth(SupportedAuthMethods);
false -> {ok, none}
true ->
[_ | _] =
PreferredAuthMethods = maps:get(preferred_auth_methods, Opts, [
private_key_jwt,
client_secret_jwt,
client_secret_post,
client_secret_basic,
none
]),
select_preferred_auth(PreferredAuthMethods, SupportedAuthMethods);
false ->
{ok, none}
end,

case MaybeAuthMethod of
Expand Down Expand Up @@ -872,39 +883,21 @@ retrieve_a_token(QsBodyIn, PkceVerifier, ClientContext, Opts, TelemetryOpts, Aut
{error, Reason}
end.

-spec select_preferred_auth(AuthMethodsSupported) ->
-spec select_preferred_auth(PreferredAuthMethods, AuthMethodsSupported) ->
{ok, auth_method()} | {error, error()}
when
PreferredAuthMethods :: [auth_method(), ...],
AuthMethodsSupported :: [binary(), ...].
select_preferred_auth(AuthMethodsSupported) ->
MethodPriority = #{
none => 0,
client_secret_basic => 1,
client_secret_post => 2,
client_secret_jwt => 3,
private_key_jwt => 4
},
KnownAuthMethods = lists:filtermap(
fun
(<<"none">>) -> {true, none};
(<<"client_secret_basic">>) -> {true, client_secret_basic};
(<<"client_secret_post">>) -> {true, client_secret_post};
(<<"client_secret_jwt">>) -> {true, client_secret_jwt};
(<<"private_key_jwt">>) -> {true, private_key_jwt};
(_Other) -> false
end,
AuthMethodsSupported
),
SortedAuthMethods = lists:usort(
fun(A, B) ->
maps:get(A, MethodPriority) > maps:get(B, MethodPriority)
end,
KnownAuthMethods
),

case SortedAuthMethods of
[] -> {error, no_supported_auth_method};
[PriorityAuthMethod | _Rest] -> {ok, PriorityAuthMethod}
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 add_authentication(
Expand Down
102 changes: 102 additions & 0 deletions test/oidcc_token_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -945,3 +945,105 @@ auth_method_client_secret_jwt_no_alg_test() ->
meck:unload(httpc),

ok.

preferred_auth_methods_test() ->
PrivDir = code:priv_dir(oidcc),

{ok, _} = application:ensure_all_started(oidcc),

{ok, ConfigurationBinary} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"),
{ok, Configuration0} = oidcc_provider_configuration:decode_configuration(
jose:decode(ConfigurationBinary)
),

#oidcc_provider_configuration{token_endpoint = TokenEndpoint, issuer = Issuer} =
Configuration = Configuration0#oidcc_provider_configuration{
token_endpoint_auth_methods_supported = [
<<"client_secret_jwt">>, <<"client_secret_basic">>
],
token_endpoint_auth_signing_alg_values_supported = [<<"HS256">>]
},

ClientId = <<"client_id">>,
ClientSecret = <<"client_secret">>,
LocalEndpoint = <<"https://my.server/auth">>,
AuthCode = <<"1234567890">>,
AccessToken = <<"access_token">>,
RefreshToken = <<"refresh_token">>,
Claims =
#{
<<"iss">> => Issuer,
<<"sub">> => <<"sub">>,
<<"aud">> => ClientId,
<<"iat">> => erlang:system_time(second),
<<"exp">> => erlang:system_time(second) + 10,
<<"at_hash">> => <<"hrOQHuo3oE6FR82RIiX1SA">>
},

Jwk = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"),

Jwt = jose_jwt:from(Claims),
Jws = #{<<"alg">> => <<"RS256">>},
{_Jws, Token} =
jose_jws:compact(
jose_jwt:sign(Jwk, Jws, Jwt)
),

TokenData =
jsx:encode(#{
<<"access_token">> => AccessToken,
<<"token_type">> => <<"Bearer">>,
<<"id_token">> => Token,
<<"scope">> => <<"profile openid">>,
<<"refresh_token">> => RefreshToken
}),

ClientContext = oidcc_client_context:from_manual(Configuration, Jwk, ClientId, ClientSecret),

ok = meck:new(httpc, [no_link]),
HttpFun =
fun(
post,
{ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body},
_HttpOpts,
_Opts
) ->
TokenEndpoint = ReqTokenEndpoint,
?assertMatch({"authorization", _}, proplists:lookup("authorization", Header)),
BodyMap = maps:from_list(uri_string:dissect_query(Body)),

?assertMatch(
#{
<<"grant_type">> := <<"authorization_code">>,
<<"code">> := AuthCode,
<<"redirect_uri">> := LocalEndpoint
},
BodyMap
),

?assertMatch(none, maps:get("client_assertion", BodyMap, none)),

{ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/json"}], TokenData}}
end,
ok = meck:expect(httpc, request, HttpFun),

?assertMatch(
{ok, #oidcc_token{
id = #oidcc_token_id{token = Token, claims = Claims},
access = #oidcc_token_access{token = AccessToken},
refresh = #oidcc_token_refresh{token = RefreshToken},
scope = [<<"profile">>, <<"openid">>]
}},
oidcc_token:retrieve(
AuthCode,
ClientContext,
#{redirect_uri => LocalEndpoint, preferred_auth_methods => [client_secret_basic]}
)
),

true = meck:validate(httpc),

meck:unload(httpc),

ok.
>>>>>>> 04a3485 (feat: `preferred_auth_methods` argument for `request_token`)

0 comments on commit 78455e4

Please sign in to comment.