From 373903d5989704d5bfa4a24f5967c8ba525882de Mon Sep 17 00:00:00 2001 From: Paul Swartz Date: Sun, 17 Dec 2023 08:11:19 -0500 Subject: [PATCH 1/3] fix: return PKCE challenge keys as binaries --- src/oidcc_authorization.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/oidcc_authorization.erl b/src/oidcc_authorization.erl index f870cda..33f311a 100644 --- a/src/oidcc_authorization.erl +++ b/src/oidcc_authorization.erl @@ -136,14 +136,14 @@ append_code_challenge(CodeVerifier, QueryParams, ClientContext) -> mode => urlsafe, padding => false }), [ - {"code_challenge", CodeChallenge}, - {"code_challenge_method", <<"S256">>} + {<<"code_challenge">>, CodeChallenge}, + {<<"code_challenge_method">>, <<"S256">>} | QueryParams ]; {false, true} -> [ - {"code_challenge", CodeVerifier}, - {"code_challenge_method", <<"plain">>} + {<<"code_challenge">>, CodeVerifier}, + {<<"code_challenge_method">>, <<"plain">>} | QueryParams ]; {false, false} -> From 89dc3a0d91bfbfcb1232fbc2373127c08caf9d09 Mon Sep 17 00:00:00 2001 From: Paul Swartz Date: Sat, 16 Dec 2023 20:23:51 -0500 Subject: [PATCH 2/3] feat: Pushed Authorization Request (PAR) --- src/oidcc_auth_util.erl | 30 ++- src/oidcc_authorization.erl | 99 +++++++- test/oidcc_authorization_SUITE.erl | 12 +- test/oidcc_authorization_test.erl | 385 +++++++++++++++++++++++++++++ 4 files changed, 506 insertions(+), 20 deletions(-) diff --git a/src/oidcc_auth_util.erl b/src/oidcc_auth_util.erl index 3575bb3..5dbabd1 100644 --- a/src/oidcc_auth_util.erl +++ b/src/oidcc_auth_util.erl @@ -48,7 +48,9 @@ add_client_authentication( case select_preferred_auth(PreferredAuthMethods, SupportedAuthMethods) of {ok, AuthMethod} -> case - add_authentication(QueryList0, Header0, AuthMethod, AllowAlgorithms, ClientContext) + add_authentication( + QueryList0, Header0, AuthMethod, AllowAlgorithms, Opts, ClientContext + ) of {ok, {QueryList, Header}} -> {ok, {QueryList, Header}}; @@ -71,6 +73,7 @@ add_client_authentication( Header, AuthMethod, AllowAlgorithms, + Opts, ClientContext ) -> {ok, {oidcc_http_util:query_params(), [oidcc_http_util:http_header()]}} @@ -80,12 +83,14 @@ when Header :: [oidcc_http_util:http_header()], AuthMethod :: auth_method(), AllowAlgorithms :: [binary()] | undefined, + Opts :: map(), ClientContext :: oidcc_client_context:t(). add_authentication( QsBodyList, Header, none, _AllowArgs, + _Opts, #oidcc_client_context{client_id = ClientId} ) -> NewBodyList = [{<<"client_id">>, ClientId} | QsBodyList], @@ -95,6 +100,7 @@ add_authentication( _Header, _Method, _AllowAlgs, + _Opts, #oidcc_client_context{client_secret = unauthenticated} ) -> {error, auth_method_not_possible}; @@ -103,6 +109,7 @@ add_authentication( Header, client_secret_basic, _AllowAlgs, + _Opts, #oidcc_client_context{client_id = ClientId, client_secret = ClientSecret} ) -> NewHeader = [oidcc_http_util:basic_auth_header(ClientId, ClientSecret) | Header], @@ -112,6 +119,7 @@ add_authentication( Header, client_secret_post, _AllowAlgs, + _Opts, #oidcc_client_context{client_id = ClientId, client_secret = ClientSecret} ) -> NewBodyList = @@ -122,6 +130,7 @@ add_authentication( Header, client_secret_jwt, AllowAlgorithms, + Opts, ClientContext ) -> #oidcc_client_context{ @@ -139,6 +148,7 @@ add_authentication( {ok, ClientAssertion} ?= signed_client_assertion( AllowAlgorithms, + Opts, ClientContext, OctJwk ), @@ -152,6 +162,7 @@ add_authentication( Header, private_key_jwt, AllowAlgorithms, + Opts, ClientContext ) -> #oidcc_client_context{ @@ -162,7 +173,7 @@ add_authentication( [_ | _] ?= AllowAlgorithms, #jose_jwk{} ?= ClientJwks, {ok, ClientAssertion} ?= - signed_client_assertion(AllowAlgorithms, ClientContext, ClientJwks), + signed_client_assertion(AllowAlgorithms, Opts, ClientContext, ClientJwks), {ok, add_jwt_bearer_assertion(ClientAssertion, QsBodyList, Header, ClientContext)} else _ -> @@ -186,23 +197,26 @@ select_preferred_auth(PreferredAuthMethods, AuthMethodsSupported) -> {error, no_supported_auth_method} end. --spec signed_client_assertion(AllowAlgorithms, ClientContext, Jwk) -> +-spec signed_client_assertion(AllowAlgorithms, Opts, ClientContext, Jwk) -> {ok, binary()} | {error, term()} when AllowAlgorithms :: [binary()], Jwk :: jose_jwk:key(), + Opts :: map(), ClientContext :: oidcc_client_context:t(). -signed_client_assertion(AllowAlgorithms, ClientContext, Jwk) -> - Jwt = jose_jwt:from(token_request_claims(ClientContext)), +signed_client_assertion(AllowAlgorithms, Opts, ClientContext, Jwk) -> + Jwt = jose_jwt:from(token_request_claims(Opts, ClientContext)), oidcc_jwt_util:sign(Jwt, Jwk, AllowAlgorithms). --spec token_request_claims(ClientContext) -> oidcc_jwt_util:claims() when +-spec token_request_claims(Opts, ClientContext) -> oidcc_jwt_util:claims() when + Opts :: map(), ClientContext :: oidcc_client_context:t(). -token_request_claims(#oidcc_client_context{ +token_request_claims(Opts, #oidcc_client_context{ client_id = ClientId, provider_configuration = #oidcc_provider_configuration{token_endpoint = TokenEndpoint} }) -> + Audience = maps:get(audience, Opts, TokenEndpoint), MaxClockSkew = case application:get_env(oidcc, max_clock_skew) of undefined -> 0; @@ -212,7 +226,7 @@ token_request_claims(#oidcc_client_context{ #{ <<"iss">> => ClientId, <<"sub">> => ClientId, - <<"aud">> => TokenEndpoint, + <<"aud">> => Audience, <<"jti">> => random_string(32), <<"iat">> => os:system_time(seconds), <<"exp">> => os:system_time(seconds) + 30, diff --git a/src/oidcc_authorization.erl b/src/oidcc_authorization.erl index 33f311a..243e93d 100644 --- a/src/oidcc_authorization.erl +++ b/src/oidcc_authorization.erl @@ -42,7 +42,8 @@ %%
  • `url_extension' - add custom query parameters to the authorization url
  • %% --type error() :: {grant_type_not_supported, authorization_code}. +-type error() :: + {grant_type_not_supported, authorization_code} | par_required | oidcc_http_util:error(). %% @doc %% Create Auth Redirect URL @@ -78,18 +79,20 @@ create_redirect_url(#oidcc_client_context{} = ClientContext, Opts) -> } = ProviderConfiguration, - case lists:member(<<"authorization_code">>, GrantTypesSupported) of - true -> - QueryParams0 = redirect_params(ClientContext, Opts), - QueryParams = QueryParams0 ++ maps:get(url_extension, Opts, []), - QueryString = uri_string:compose_query(QueryParams), - - {ok, [AuthEndpoint, <<"?">>, QueryString]}; + maybe + true ?= lists:member(<<"authorization_code">>, GrantTypesSupported), + {ok, QueryParams0} ?= redirect_params(ClientContext, Opts), + QueryParams = QueryParams0 ++ maps:get(url_extension, Opts, []), + QueryString = uri_string:compose_query(QueryParams), + {ok, [AuthEndpoint, <<"?">>, QueryString]} + else + {error, Reason} -> + {error, Reason}; false -> {error, {grant_type_not_supported, authorization_code}} end. --spec redirect_params(ClientContext, Opts) -> oidcc_http_util:query_params() when +-spec redirect_params(ClientContext, Opts) -> {ok, oidcc_http_util:query_params()} when ClientContext :: oidcc_client_context:t(), Opts :: opts(). redirect_params(#oidcc_client_context{client_id = ClientId} = ClientContext, Opts) -> @@ -107,7 +110,8 @@ redirect_params(#oidcc_client_context{client_id = ClientId} = ClientContext, Opt QueryParams4 = oidcc_scope:query_append_scope( maps:get(scopes, Opts, [openid]), QueryParams3 ), - attempt_request_object(QueryParams4, ClientContext). + QueryParams5 = attempt_request_object(QueryParams4, ClientContext), + attempt_par(QueryParams5, ClientContext, Opts). -spec append_code_challenge(PkceVerifier, QueryParams, ClientContext) -> oidcc_http_util:query_params() @@ -257,6 +261,81 @@ attempt_request_object(QueryParams, #oidcc_client_context{ end end. +-spec attempt_par(QueryParams, ClientContext, Opts) -> + {ok, QueryParams} | {error, error()} +when + QueryParams :: oidcc_http_util:query_params(), + ClientContext :: oidcc_client_context:t(), + Opts :: opts(). +attempt_par( + _QueryParams, + #oidcc_client_context{ + provider_configuration = #oidcc_provider_configuration{ + require_pushed_authorization_requests = true, + pushed_authorization_request_endpoint = undefined + } + }, + _Opts +) -> + {error, par_required}; +attempt_par( + QueryParams, + #oidcc_client_context{ + provider_configuration = #oidcc_provider_configuration{ + pushed_authorization_request_endpoint = undefined + } + }, + _Opts +) -> + {ok, QueryParams}; +attempt_par( + QueryParams, + #oidcc_client_context{ + client_id = ClientId, + provider_configuration = + #oidcc_provider_configuration{ + issuer = Issuer, + token_endpoint_auth_methods_supported = SupportedAuthMethods, + token_endpoint_auth_signing_alg_values_supported = SigningAlgs, + pushed_authorization_request_endpoint = PushedAuthorizationRequestEndpoint + } + } = ClientContext, + Opts +) -> + Header0 = [{"accept", "application/json"}], + + TelemetryOpts = #{ + topic => [oidcc, par_request], extra_meta => #{issuer => Issuer, client_id => ClientId} + }, + + RequestOpts = maps:get(request_opts, Opts, #{}), + %% https://datatracker.ietf.org/doc/html/rfc9126#section-2 + %% > To address that ambiguity, the issuer identifier URL of the authorization + %% > server according to [RFC8414] SHOULD be used as the value of the audience. + AuthenticationOpts = #{audience => Issuer}, + + maybe + {ok, {Body, Header}} ?= + oidcc_auth_util:add_client_authentication( + QueryParams, + Header0, + SupportedAuthMethods, + SigningAlgs, + AuthenticationOpts, + ClientContext + ), + Request = + {PushedAuthorizationRequestEndpoint, Header, "application/x-www-form-urlencoded", + uri_string:compose_query(Body)}, + {ok, {{json, ParResponse}, _Headers}} ?= + oidcc_http_util:request(post, Request, TelemetryOpts, RequestOpts), + #{<<"request_uri">> := ParRequestUri} ?= ParResponse, + {ok, [{<<"request_uri">>, ParRequestUri}, {<<"client_id">>, ClientId}]} + else + {error, Reason} -> {error, Reason}; + #{} = JsonResponse -> {error, {http_error, 201, JsonResponse}} + end. + -spec essential_params(QueryParams :: oidcc_http_util:query_params()) -> oidcc_http_util:query_params(). essential_params(QueryParams) -> diff --git a/test/oidcc_authorization_SUITE.erl b/test/oidcc_authorization_SUITE.erl index ac9644b..18e77e8 100644 --- a/test/oidcc_authorization_SUITE.erl +++ b/test/oidcc_authorization_SUITE.erl @@ -1,5 +1,8 @@ -module(oidcc_authorization_SUITE). +-include("oidcc_client_context.hrl"). +-include("oidcc_provider_configuration.hrl"). + -export([all/0]). -export([create_redirect_url_inl_gov/1]). @@ -14,10 +17,15 @@ create_redirect_url_inl_gov(_Config) -> issuer => <<"https://identity-preview.inl.gov">> }), - {ok, ClientContext} = oidcc_client_context:from_configuration_worker( + {ok, #oidcc_client_context{provider_configuration = ProviderConfiguration} = ClientContext0} = oidcc_client_context:from_configuration_worker( InlGovPid, <<"client_id">>, <<"client_secret">> ), - + %% we only want to test the URL generation, not the PAR request + ClientContext = ClientContext0#oidcc_client_context{ + provider_configuration = ProviderConfiguration#oidcc_provider_configuration{ + pushed_authorization_request_endpoint = undefined + } + }, {ok, Url} = oidcc_authorization:create_redirect_url(ClientContext, #{ redirect_uri => <<"https://my.server/return">> }), diff --git a/test/oidcc_authorization_test.erl b/test/oidcc_authorization_test.erl index 30ee98a..cb3ee95 100644 --- a/test/oidcc_authorization_test.erl +++ b/test/oidcc_authorization_test.erl @@ -617,3 +617,388 @@ create_redirect_url_with_request_object_only_none_alg_unsecured_test() -> jose:unsecured_signing(false), ok. + +create_redirect_url_with_par_required_no_url_test() -> + PrivDir = code:priv_dir(oidcc), + + {ok, ValidConfigString} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"), + {ok, Configuration0} = oidcc_provider_configuration:decode_configuration( + jose:decode(ValidConfigString) + ), + + Configuration = Configuration0#oidcc_provider_configuration{ + require_pushed_authorization_requests = true + }, + + ClientId = <<"client_id">>, + ClientSecret = <<"at_least_32_character_client_secret">>, + + Jwks0 = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"), + Jwks = Jwks0#jose_jwk{fields = #{<<"use">> => <<"enc">>}}, + + RedirectUri = <<"https://my.server/return">>, + + ClientContext = + oidcc_client_context:from_manual(Configuration, Jwks, ClientId, ClientSecret), + + ?assertMatch( + {error, par_required}, + oidcc_authorization:create_redirect_url(ClientContext, #{ + redirect_uri => RedirectUri + }) + ), + + ok. + +create_redirect_url_with_par_url_test() -> + PrivDir = code:priv_dir(oidcc), + + {ok, _} = application:ensure_all_started(oidcc), + + {ok, ValidConfigString} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"), + {ok, Configuration0} = oidcc_provider_configuration:decode_configuration( + jose:decode(ValidConfigString) + ), + + Configuration = Configuration0#oidcc_provider_configuration{ + pushed_authorization_request_endpoint = <<"https://my.server/par">> + }, + + ParResponseData = + jsx:encode(#{ + <<"request_uri">> => <<"urn:ietf:params:oauth:request_uri:par_response">>, + <<"expires_in">> => 60 + }), + + ClientId = <<"client_id">>, + ClientSecret = <<"at_least_32_character_client_secret">>, + + Jwks0 = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"), + Jwks = Jwks0#jose_jwk{fields = #{<<"use">> => <<"enc">>}}, + + RedirectUri = <<"https://my.server/return">>, + PkceVerifier = <<"pkce_verifier">>, + State = <<"state">>, + Nonce = <<"nonce">>, + + ClientContext = + oidcc_client_context:from_manual(Configuration, Jwks, ClientId, ClientSecret), + + ok = meck:new(httpc, [no_link]), + HttpFun = + fun( + post, + {ReqParEndpoint, Header, "application/x-www-form-urlencoded", Body}, + _HttpOpts, + _Opts + ) -> + ?assertMatch(<<"https://my.server/par">>, ReqParEndpoint), + ?assertMatch(none, proplists:lookup("authorization", Header)), + ?assertMatch({"accept", "application/json"}, proplists:lookup("accept", Header)), + BodyMap = maps:from_list(uri_string:dissect_query(Body)), + + ?assertMatch( + #{ + <<"response_type">> := <<"code">>, + <<"client_id">> := ClientId, + <<"client_secret">> := ClientSecret, + <<"scope">> := <<"openid">>, + <<"redirect_uri">> := RedirectUri, + <<"code_challenge">> := _, + <<"code_challenge_method">> := <<"S256">>, + <<"state">> := State, + <<"nonce">> := Nonce + }, + BodyMap + ), + + {ok, {{"HTTP/1.1", 201, "OK"}, [{"content-type", "application/json"}], ParResponseData}} + end, + ok = meck:expect(httpc, request, HttpFun), + + RedirectUrlResponse = oidcc_authorization:create_redirect_url(ClientContext, #{ + redirect_uri => RedirectUri, + pkce_verifier => PkceVerifier, + state => State, + nonce => Nonce + }), + + true = meck:validate(httpc), + + meck:unload(httpc), + + ?assertMatch( + {ok, _}, + RedirectUrlResponse + ), + + {ok, Url} = + RedirectUrlResponse, + #{ + query := QueryString + } = uri_string:parse(Url), + QueryParams0 = uri_string:dissect_query(QueryString), + QueryParams1 = lists:map( + fun({Key, Value}) -> {list_to_binary(Key), list_to_binary(Value)} end, QueryParams0 + ), + QueryParams = maps:from_list(QueryParams1), + ?assertEqual( + #{ + <<"request_uri">> => <<"urn:ietf:params:oauth:request_uri:par_response">>, + <<"client_id">> => ClientId + }, + QueryParams + ), + + ok. + +create_redirect_url_with_par_error_when_required_test() -> + PrivDir = code:priv_dir(oidcc), + + {ok, _} = application:ensure_all_started(oidcc), + + {ok, ValidConfigString} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"), + {ok, Configuration0} = oidcc_provider_configuration:decode_configuration( + jose:decode(ValidConfigString) + ), + + Configuration = Configuration0#oidcc_provider_configuration{ + require_pushed_authorization_requests = true, + pushed_authorization_request_endpoint = <<"https://my.server/par">> + }, + + ParResponseData = + jsx:encode(#{ + <<"error">> => <<"invalid_request">> + }), + + ClientId = <<"client_id">>, + ClientSecret = <<"at_least_32_character_client_secret">>, + + Jwks0 = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"), + Jwks = Jwks0#jose_jwk{fields = #{<<"use">> => <<"enc">>}}, + + RedirectUri = <<"https://my.server/return">>, + + ClientContext = + oidcc_client_context:from_manual(Configuration, Jwks, ClientId, ClientSecret), + + ok = meck:new(httpc, [no_link]), + HttpFun = + fun( + post, + {_Endpoint, _Header, _ContentType, _Body}, + _HttpOpts, + _Opts + ) -> + {ok, {{"HTTP/1.1", 400, "OK"}, [{"content-type", "application/json"}], ParResponseData}} + end, + ok = meck:expect(httpc, request, HttpFun), + + ?assertMatch( + {error, {http_error, 400, _}}, + oidcc_authorization:create_redirect_url(ClientContext, #{ + redirect_uri => RedirectUri + }) + ), + + true = meck:validate(httpc), + + meck:unload(httpc), + + ok. + +create_redirect_url_with_par_invalid_response_test() -> + PrivDir = code:priv_dir(oidcc), + + {ok, _} = application:ensure_all_started(oidcc), + + {ok, ValidConfigString} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"), + {ok, Configuration0} = oidcc_provider_configuration:decode_configuration( + jose:decode(ValidConfigString) + ), + + Configuration = Configuration0#oidcc_provider_configuration{ + require_pushed_authorization_requests = false, + pushed_authorization_request_endpoint = <<"https://my.server/par">> + }, + + %% no request_uri + ParResponseData = jsx:encode(#{}), + + ClientId = <<"client_id">>, + ClientSecret = <<"at_least_32_character_client_secret">>, + + Jwks0 = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"), + Jwks = Jwks0#jose_jwk{fields = #{<<"use">> => <<"enc">>}}, + + RedirectUri = <<"https://my.server/return">>, + + ClientContext = + oidcc_client_context:from_manual(Configuration, Jwks, ClientId, ClientSecret), + + ok = meck:new(httpc, [no_link]), + HttpFun = + fun( + post, + {_Endpoint, _Header, _ContentType, _Body}, + _HttpOpts, + _Opts + ) -> + {ok, {{"HTTP/1.1", 201, "OK"}, [{"content-type", "application/json"}], ParResponseData}} + end, + ok = meck:expect(httpc, request, HttpFun), + + ?assertMatch( + {error, {http_error, 201, _}}, + oidcc_authorization:create_redirect_url(ClientContext, #{ + redirect_uri => RedirectUri + }) + ), + + true = meck:validate(httpc), + + meck:unload(httpc), + + ok. + +create_redirect_url_with_par_client_secret_jwt_request_object_test() -> + %% https://datatracker.ietf.org/doc/html/rfc9126#section-2 + %% > To address that ambiguity, the issuer identifier URL of the authorization + %% > server according to [RFC8414] SHOULD be used as the value of the audience. + PrivDir = code:priv_dir(oidcc), + + {ok, ValidConfigString} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"), + {ok, #oidcc_provider_configuration{issuer = Issuer} = Configuration0} = oidcc_provider_configuration:decode_configuration( + jose:decode(ValidConfigString) + ), + + Configuration = Configuration0#oidcc_provider_configuration{ + pushed_authorization_request_endpoint = <<"https://my.server/par">>, + token_endpoint_auth_methods_supported = [<<"client_secret_jwt">>], + token_endpoint_auth_signing_alg_values_supported = [<<"HS256">>], + request_parameter_supported = true, + request_object_signing_alg_values_supported = [ + <<"HS256">> + ], + request_object_encryption_alg_values_supported = [<<"RSA-OAEP-256">>], + request_object_encryption_enc_values_supported = [<<"A256GCM">>] + }, + + ParResponseData = + jsx:encode(#{ + <<"request_uri">> => <<"urn:ietf:params:oauth:request_uri:par_response">>, + <<"expires_in">> => 60 + }), + + ClientId = <<"client_id">>, + ClientSecret = <<"at_least_32_character_client_secret">>, + + Jwks0 = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"), + Jwks = Jwks0#jose_jwk{fields = #{<<"use">> => <<"enc">>}}, + + RedirectUri = <<"https://my.server/return">>, + + ClientContext = + oidcc_client_context:from_manual(Configuration, Jwks, ClientId, ClientSecret), + + ok = meck:new(httpc, [no_link]), + HttpFun = + fun( + post, + {_Endpoint, _Header, "application/x-www-form-urlencoded", Body}, + _HttpOpts, + _Opts + ) -> + BodyMap = maps:from_list(uri_string:dissect_query(Body)), + + ?assertMatch( + #{ + <<"client_id">> := ClientId, + <<"client_assertion_type">> := + <<"urn:ietf:params:oauth:client-assertion-type:jwt-bearer">>, + <<"client_assertion">> := _, + <<"request">> := _ + }, + BodyMap + ), + + ClientAssertion = maps:get(<<"client_assertion">>, BodyMap), + + {true, ClientAssertionJwt, ClientAssertionJws} = jose_jwt:verify( + jose_jwk:from_oct(ClientSecret), ClientAssertion + ), + + ?assertMatch(#jose_jws{alg = {jose_jws_alg_hmac, 'HS256'}}, ClientAssertionJws), + + ?assertMatch( + #jose_jwt{ + fields = #{ + <<"aud">> := Issuer, + <<"exp">> := _, + <<"iat">> := _, + <<"iss">> := ClientId, + <<"jti">> := _, + <<"nbf">> := _, + <<"sub">> := ClientId + } + }, + ClientAssertionJwt + ), + + {SignedToken, Jwe} = jose_jwe:block_decrypt(Jwks, maps:get(<<"request">>, BodyMap)), + + ?assertMatch(#jose_jwe{alg = {jose_jwe_alg_rsa, _}}, Jwe), + + {true, Jwt, Jws} = jose_jwt:verify(jose_jwk:from_oct(ClientSecret), SignedToken), + + ?assertMatch(#jose_jws{alg = {jose_jws_alg_hmac, 'HS256'}}, Jws), + + ?assertMatch( + #jose_jwt{ + fields = #{ + <<"aud">> := Issuer, + <<"client_id">> := ClientId, + <<"exp">> := _, + <<"iat">> := _, + <<"iss">> := ClientId, + <<"jti">> := _, + <<"nbf">> := _, + <<"redirect_uri">> := RedirectUri, + <<"response_type">> := <<"code">>, + <<"scope">> := <<"openid">> + } + }, + Jwt + ), + + {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/json"}], ParResponseData}} + end, + ok = meck:expect(httpc, request, HttpFun), + + {ok, Url} = oidcc_authorization:create_redirect_url(ClientContext, #{ + redirect_uri => RedirectUri + }), + + true = meck:validate(httpc), + + meck:unload(httpc), + + ?assertMatch(<<"https://my.provider/auth?request_uri=", _/binary>>, iolist_to_binary(Url)), + + #{query := QueryString} = uri_string:parse(Url), + QueryParams0 = uri_string:dissect_query(QueryString), + QueryParams1 = lists:map( + fun({Key, Value}) -> {list_to_binary(Key), list_to_binary(Value)} end, QueryParams0 + ), + QueryParams = maps:from_list(QueryParams1), + + ?assertMatch( + #{ + <<"client_id">> := <<"client_id">>, + <<"request_uri">> := <<"urn:ietf:params:oauth:request_uri:par_response">> + }, + QueryParams + ), + + ok. From e87bdd875cf8b594092e8ee8889e338a541c0171 Mon Sep 17 00:00:00 2001 From: Paul Swartz Date: Mon, 18 Dec 2023 09:59:03 -0500 Subject: [PATCH 3/3] fixup! feat: Pushed Authorization Request (PAR) --- README.md | 1 + include/oidcc_client_registration.hrl | 2 ++ lib/oidcc/client_registration.ex | 1 + src/oidcc_client_registration.erl | 6 +++++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b35865c..559af38 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ The refactoring for `v3` and the certification is funded as an * Authorization (Code Flow) * [Request Object](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) * [PKCE](https://oauth.net/2/pkce/) + * [Pushed Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9126) * Token * Authorization: `client_secret_basic`, `client_secret_post`, `client_secret_jwt`, and `private_key_jwt` diff --git a/include/oidcc_client_registration.hrl b/include/oidcc_client_registration.hrl index 3724c2a..db96c94 100644 --- a/include/oidcc_client_registration.hrl +++ b/include/oidcc_client_registration.hrl @@ -65,6 +65,8 @@ request_uris = undefined :: [uri_string:uri_string()] | undefined, %% OpenID Connect RP-Initiated Logout 1.0 post_logout_redirect_uris = undefined :: [uri_string:uri_string()] | undefined, + %% OAuth 2.0 Pushed Authorization Requests + require_pushed_authorization_requests = false :: boolean(), %% Unknown Fields extra_fields = #{} :: #{binary() => term()} }). diff --git a/lib/oidcc/client_registration.ex b/lib/oidcc/client_registration.ex index fe7eed2..3b413e8 100644 --- a/lib/oidcc/client_registration.ex +++ b/lib/oidcc/client_registration.ex @@ -80,6 +80,7 @@ defmodule Oidcc.ClientRegistration do initiate_login_uri: :uri_string.uri_string() | :undefined, request_uris: [:uri_string.uri_string()] | :undefined, post_logout_redirect_uris: [:uri_string.uri_string()] | :undefined, + require_pushed_authorization_requests: boolean(), extra_fields: %{String.t() => term()} } diff --git a/src/oidcc_client_registration.erl b/src/oidcc_client_registration.erl index bb36c3f..a3c9b12 100644 --- a/src/oidcc_client_registration.erl +++ b/src/oidcc_client_registration.erl @@ -108,6 +108,8 @@ request_uris :: [uri_string:uri_string()] | undefined, %% OpenID Connect RP-Initiated Logout 1.0 post_logout_redirect_uris :: [uri_string:uri_string()] | undefined, + %% OAuth 2.0 Pushed Authorization Requests + require_pushed_authorization_requests :: boolean(), %% Unknown Fields extra_fields :: #{binary() => term()} }. @@ -293,6 +295,7 @@ encode(#oidcc_client_registration{ initiate_login_uri = InitiateLoginUri, request_uris = RequestUris, post_logout_redirect_uris = PostLogoutRedirectUris, + require_pushed_authorization_requests = RequirePushedAuthorizationRequests, extra_fields = ExtraFields }) -> Map0 = #{ @@ -332,7 +335,8 @@ encode(#oidcc_client_registration{ default_acr_values => DefaultAcrValues, initiate_login_uri => InitiateLoginUri, request_uris => RequestUris, - post_logout_redirect_uris => PostLogoutRedirectUris + post_logout_redirect_uris => PostLogoutRedirectUris, + require_pushed_authorization_requests => RequirePushedAuthorizationRequests }, Map1 = maps:merge(Map0, ExtraFields), Map = maps:filter(