Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: small features to support ConnectID.com.au profile #333

Merged
merged 2 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions src/oidcc_authorization.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
nonce => binary(),
pkce_verifier => binary(),
require_pkce => boolean(),
purpose => binary(),
require_purpose => boolean(),
redirect_uri => uri_string:uri_string(),
url_extension => oidcc_http_util:query_params(),
response_mode => binary()
Expand All @@ -38,6 +40,9 @@
%% <li>`scopes' - list of scopes to request (defaults to `[<<"openid">>]')</li>
%% <li>`state' - state to pass to the provider</li>
%% <li>`nonce' - nonce to pass to the provider</li>
%% <li>`purpose' - purpose of the authorization request, see
%% [https://cdn.connectid.com.au/specifications/oauth2-purpose-01.html]</li>
%% <li>`require_purpose' - whether to require a `purpose' value</li>
%% <li>`pkce_verifier' - pkce verifier (random string), see
%% [https://datatracker.ietf.org/doc/html/rfc7636#section-4.1]</li>
%% <li>`require_pkce' - whether to require PKCE when getting the token</li>
Expand All @@ -51,6 +56,7 @@
| par_required
| request_object_required
| pkce_verifier_required
| purpose_required
| no_supported_code_challenge
| oidcc_http_util:error().

Expand Down Expand Up @@ -105,32 +111,34 @@ create_redirect_url(#oidcc_client_context{} = ClientContext, Opts) ->
ClientContext :: oidcc_client_context:t(),
Opts :: opts().
redirect_params(#oidcc_client_context{client_id = ClientId} = ClientContext, Opts) ->
QueryParams =
QueryParams0 =
[
{<<"response_type">>, maps:get(response_type, Opts, <<"code">>)},
{<<"client_id">>, ClientId},
{<<"redirect_uri">>, maps:get(redirect_uri, Opts)}
],
QueryParams1 = maybe_append(<<"state">>, maps:get(state, Opts, undefined), QueryParams),
QueryParams1 = maybe_append(<<"state">>, maps:get(state, Opts, undefined), QueryParams0),
QueryParams2 = maybe_append(<<"nonce">>, maps:get(nonce, Opts, undefined), QueryParams1),
QueryParams3 =
QueryParams3 = maybe_append(<<"purpose">>, maps:get(purpose, Opts, undefined), QueryParams2),
QueryParams4 =
case maps:get(response_mode, Opts, <<"query">>) of
<<"query">> ->
QueryParams2;
QueryParams3;
ResponseMode when is_binary(ResponseMode) ->
[{<<"response_mode">>, ResponseMode} | QueryParams2]
[{<<"response_mode">>, ResponseMode} | QueryParams3]
end,
maybe
{ok, QueryParams4} ?=
ok ?= validate_purpose_required(Opts),
{ok, QueryParams5} ?=
append_code_challenge(
Opts, QueryParams3, ClientContext
Opts, QueryParams4, ClientContext
),
QueryParams5 = oidcc_scope:query_append_scope(
maps:get(scopes, Opts, [openid]), QueryParams4
QueryParams6 = oidcc_scope:query_append_scope(
maps:get(scopes, Opts, [openid]), QueryParams5
),
QueryParams6 = maybe_append_dpop_jkt(QueryParams5, ClientContext),
{ok, QueryParams7} ?= attempt_request_object(QueryParams6, ClientContext),
attempt_par(QueryParams7, ClientContext, Opts)
QueryParams7 = maybe_append_dpop_jkt(QueryParams6, ClientContext),
{ok, QueryParams} ?= attempt_request_object(QueryParams7, ClientContext),
attempt_par(QueryParams, ClientContext, Opts)
end.

-spec append_code_challenge(Opts, QueryParams, ClientContext) ->
Expand Down Expand Up @@ -191,6 +199,15 @@ maybe_append(_Key, undefined, QueryParams) ->
maybe_append(Key, Value, QueryParams) ->
[{Key, Value} | QueryParams].

-spec validate_purpose_required(Opts) -> ok | {error, purpose_required} when
Opts :: opts().
validate_purpose_required(#{purpose := Purpose}) when is_binary(Purpose) ->
ok;
validate_purpose_required(#{purpose_required := true}) ->
{error, purpose_required};
validate_purpose_required(_Opts) ->
ok.

-spec maybe_append_dpop_jkt(QueryParams, ClientContext) ->
QueryParams
when
Expand Down
57 changes: 56 additions & 1 deletion src/oidcc_profile.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
-export_type([opts_no_profiles/0]).
-export_type([error/0]).

-type profile() :: fapi2_security_profile | fapi2_message_signing.
-type profile() :: fapi2_security_profile | fapi2_message_signing | fapi2_connectid_au.
-type opts() :: #{
profiles => [profile()],
require_pkce => boolean(),
Expand Down Expand Up @@ -92,6 +92,61 @@ apply_profiles(
%% Also require everything from FAPI2 Security Profile
Opts = Opts0#{profiles => [fapi2_security_profile | RestProfiles]},
apply_profiles(ClientContext, Opts);
apply_profiles(
#oidcc_client_context{} = ClientContext0,
#{profiles := [fapi2_connectid_au | RestProfiles]} = Opts0
) ->
%% FAPI2 ConnectID profile
maybe
%% Require everything from FAPI2 Message Signing
{ok, ClientContext1, Opts1} ?=
apply_profiles(ClientContext0, Opts0#{
profiles => [fapi2_message_signing | RestProfiles]
}),
%% Require `purpose' field
Opts2 = Opts1#{require_purpose => true},
%% If a PAR endpoint is present in the mTLS aliases, use that as the default
maennchen marked this conversation as resolved.
Show resolved Hide resolved
#oidcc_client_context{provider_configuration = Configuration1} = ClientContext1,
Configuration2 =
case Configuration1#oidcc_provider_configuration.mtls_endpoint_aliases of
#{
<<"pushed_authorization_request_endpoint">> := MtlsParEndpoint
} ->
Configuration1#oidcc_provider_configuration{
pushed_authorization_request_endpoint = MtlsParEndpoint
};
_ ->
Configuration1
end,
%% If the token endpoint is present in the mTLS aliases, use that as the default
Configuration3 =
case Configuration2#oidcc_provider_configuration.mtls_endpoint_aliases of
#{
<<"token_endpoint">> := MtlsTokenEndpoint
} ->
Configuration2#oidcc_provider_configuration{
token_endpoint = MtlsTokenEndpoint
};
_ ->
Configuration2
end,
%% If the userinfo endpoint is present in the mTLS aliases, use that as the default
Configuration4 =
case Configuration3#oidcc_provider_configuration.mtls_endpoint_aliases of
#{
<<"userinfo_endpoint">> := MtlsUserinfoEndpoint
} ->
Configuration3#oidcc_provider_configuration{
userinfo_endpoint = MtlsUserinfoEndpoint
};
_ ->
Configuration3
end,
ClientContext2 = ClientContext1#oidcc_client_context{
provider_configuration = Configuration4
},
{ok, ClientContext2, Opts2}
end;
apply_profiles(#oidcc_client_context{}, #{profiles := [UnknownProfile | _]}) ->
{error, {unknown_profile, UnknownProfile}};
apply_profiles(#oidcc_client_context{} = ClientContext, #{profiles := []} = Opts0) ->
Expand Down
16 changes: 16 additions & 0 deletions test/oidcc_authorization_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ create_redirect_url_test() ->
Opts5 = maps:merge(BaseOpts, #{pkce_verifier => <<"foo">>}),
Opts6 = maps:merge(Opts5, #{require_pkce => true}),
Opts7 = maps:merge(BaseOpts, #{require_pkce => true}),
Opts8 = maps:merge(BaseOpts, #{purpose => <<"purpose">>}),
Opts9 = maps:merge(Opts8, #{purpose_required => true}),
Opts10 = maps:merge(BaseOpts, #{purpose_required => true}),

{ok, Url1} = oidcc_authorization:create_redirect_url(ClientContext, BaseOpts),
{ok, Url2} = oidcc_authorization:create_redirect_url(ClientContext, Opts1),
Expand All @@ -75,6 +78,8 @@ create_redirect_url_test() ->
{ok, Url7} = oidcc_authorization:create_redirect_url(PkcePlainClientContext, Opts5),
{ok, Url8} = oidcc_authorization:create_redirect_url(NoPkceClientContext, Opts5),
{ok, Url9} = oidcc_authorization:create_redirect_url(PkcePlainClientContext, Opts6),
{ok, Url10} = oidcc_authorization:create_redirect_url(ClientContext, Opts8),
{ok, Url11} = oidcc_authorization:create_redirect_url(ClientContext, Opts9),

ExpUrl1 =
<<"https://my.provider/auth?scope=openid&response_type=code&client_id=client_id&redirect_uri=https%3A%2F%2Fmy.server%2Freturn&test=id">>,
Expand Down Expand Up @@ -110,6 +115,12 @@ create_redirect_url_test() ->

?assertEqual(iolist_to_binary(Url9), iolist_to_binary(Url7)),

ExpUrl10 =
<<"https://my.provider/auth?scope=openid&purpose=purpose&response_type=code&client_id=client_id&redirect_uri=https%3A%2F%2Fmy.server%2Freturn&test=id">>,
?assertEqual(ExpUrl10, iolist_to_binary(Url10)),

?assertEqual(iolist_to_binary(Url11), iolist_to_binary(Url10)),

?assertEqual(
{error, no_supported_code_challenge},
oidcc_authorization:create_redirect_url(NoPkceClientContext, Opts6)
Expand All @@ -120,6 +131,11 @@ create_redirect_url_test() ->
oidcc_authorization:create_redirect_url(ClientContext, Opts7)
),

?assertEqual(
{error, purpose_required},
oidcc_authorization:create_redirect_url(ClientContext, Opts10)
),

ok.

create_redirect_url_with_request_object_test() ->
Expand Down
56 changes: 56 additions & 0 deletions test/oidcc_client_context_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,62 @@ apply_profiles_fapi2_message_signing_test() ->

ok.

apply_profiles_fapi2_connectid_au_test() ->
ClientContext0 = client_context_fixture(),
Opts0 = #{
profiles => [fapi2_connectid_au]
},

ProfileResult = oidcc_client_context:apply_profiles(ClientContext0, Opts0),

?assertMatch(
{ok, #oidcc_client_context{}, #{}},
ProfileResult
),

{ok, ClientContext, Opts} = ProfileResult,

?assertMatch(
#oidcc_client_context{
provider_configuration = #oidcc_provider_configuration{
token_endpoint = <<"https://my.provider/tls/token">>,
userinfo_endpoint = <<"https://my.provider/tls/userinfo">>,
response_types_supported = [<<"code">>],
response_modes_supported = [<<"jwt">>, <<"query.jwt">>],
id_token_signing_alg_values_supported = [<<"EdDSA">>],
userinfo_signing_alg_values_supported = [
<<"PS256">>,
<<"PS384">>,
<<"PS512">>,
<<"ES256">>,
<<"ES384">>,
<<"ES512">>,
<<"EdDSA">>
],
code_challenge_methods_supported = [<<"S256">>],
require_pushed_authorization_requests = true,
pushed_authorization_request_endpoint = undefined,
authorization_response_iss_parameter_supported = true
}
},
ClientContext
),

?assertMatch(
#{
preferred_auth_methods := [private_key_jwt, tls_client_auth],
require_pkce := true,
require_purpose := true,
trusted_audiences := [],
request_opts := #{
ssl := _
}
},
Opts
),

ok.

apply_profiles_unknown_test() ->
ClientContext = client_context_fixture(),
Opts = #{
Expand Down