diff --git a/mix.exs b/mix.exs index 1e63564..2c3131b 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,10 @@ defmodule Oidcc.Mixfile do ] end - def application, do: [extra_applications: extra_applications(Mix.env())] + def application, do: [ + mod: {:oidcc_app, []}, + extra_applications: extra_applications(Mix.env()) + ] defp extra_applications(env) defp extra_applications(:dev), do: [:inets, :ssl, :edoc, :xmerl] diff --git a/priv/test/fixtures/README.md b/priv/test/fixtures/README.md new file mode 100644 index 0000000..4184cb8 --- /dev/null +++ b/priv/test/fixtures/README.md @@ -0,0 +1,6 @@ +# Regenerating `jwk_cert.pem` + +``` bash +openssl x509 -signkey jwk.pem -in jwk.csr -req -days 3650 -out jwk_cert.pem +``` + diff --git a/priv/test/fixtures/jwk.csr b/priv/test/fixtures/jwk.csr new file mode 100644 index 0000000..948444b --- /dev/null +++ b/priv/test/fixtures/jwk.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICezCCAWMCAQAwNjEkMCIGA1UECgwbRXJsYW5nIEVjb3N5c3RlbSBGb3VuZGF0 +aW9uMQ4wDAYDVQQDDAVPaWRjYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKIBNjF96IT2TkwDlkXJ/uneGbYfg/5YqwOZtzscwSDKRGmevVQPiD+8kTG9 +0j8ie7CryjjHJTxtxLq93H6gg74OWmVCffTf2pA0dMGizg3Ua0QPPXmwtHZfmKbJ +cKelCSPTDngQQkkomn+2ROs4xXtDmxeyjKovk/ECOEOV005KTfv0Nh0ZqZlxgmHI +Ot0XBFD4II1pESeiL3l8RE4RLDPq10V3jlWnfNORnNNAY0HgbryuggZGVifcxpnB +DAcRL5BPGaw5lCZn5Yul4ts8JoLpqLcglHbWVoTJnSUxlSKEI/kteOvMiQqwoUPG +KnuG1sktCEm3Wv+hUeq/1B3S7J8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBY +WZ6HCP6Yrws9/jOWWYS3JOEilIjqLfxgtEM7tOz8zID225DLV0m75UFkl7JIwwxY +Tx4U2FhoDqfVLbarrw31kZ2tbMRELdt9zLZbTv4b9QsB1Q+fXLn5x8W5m6qXK7kh +WIfMfbpUwmuIlcUMxwWuEN3a5XSuHbOqsaY7V9H0c4YSVdyE2C5M2VP0oUECCPjC +p3D6c47qHRkWYY2ssutK2U9cW5IusEUrcjyVIoOcW14pUjkcd3e+lr9S/59onAY1 +Pkb2wd8CsEvdsr+P58uXleWwuHBxwybwAySp5GRvkuEPuuI1YUoDuwkgOeY8Y+te +6LBUBw2DW+Z0QBSleoqs +-----END CERTIFICATE REQUEST----- diff --git a/priv/test/fixtures/jwk_cert.pem b/priv/test/fixtures/jwk_cert.pem new file mode 100644 index 0000000..e0bd43c --- /dev/null +++ b/priv/test/fixtures/jwk_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIUGnShYZbN8W/ZJ5no7hh/WRLKougwDQYJKoZIhvcNAQEL +BQAwNjEkMCIGA1UECgwbRXJsYW5nIEVjb3N5c3RlbSBGb3VuZGF0aW9uMQ4wDAYD +VQQDDAVPaWRjYzAeFw0yNDAxMDcxNjQwMTBaFw0zNDAxMDQxNjQwMTBaMDYxJDAi +BgNVBAoMG0VybGFuZyBFY29zeXN0ZW0gRm91bmRhdGlvbjEOMAwGA1UEAwwFT2lk +Y2MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCiATYxfeiE9k5MA5ZF +yf7p3hm2H4P+WKsDmbc7HMEgykRpnr1UD4g/vJExvdI/Inuwq8o4xyU8bcS6vdx+ +oIO+DlplQn3039qQNHTBos4N1GtEDz15sLR2X5imyXCnpQkj0w54EEJJKJp/tkTr +OMV7Q5sXsoyqL5PxAjhDldNOSk379DYdGamZcYJhyDrdFwRQ+CCNaREnoi95fERO +ESwz6tdFd45Vp3zTkZzTQGNB4G68roIGRlYn3MaZwQwHES+QTxmsOZQmZ+WLpeLb +PCaC6ai3IJR21laEyZ0lMZUihCP5LXjrzIkKsKFDxip7htbJLQhJt1r/oVHqv9Qd +0uyfAgMBAAGjITAfMB0GA1UdDgQWBBQJXpMge7QiKlfQFkpIx9ailJL21TANBgkq +hkiG9w0BAQsFAAOCAQEAfRspbVWaRIC0ZQv8Y3TrmqzxKcmyHi/ixVn3fW9Ygeq2 +Uasq6r0XE52gnU+Lb/3X8J0n0ENE1ovPjczjxAtrXwdM1l59C1YR7trVZJfRzNGy +2ItO7efI3fCLYPxk4OkTeSubvuxklvyVALSo5dgsZg/7PLy3Vgkzz7XPfJPtFKQ+ +xAOmul26zaJPNz49KT+m/2z77WoJHEyhEleJDo1DUABUwplI6BNecUW6VU+1BiCo +x0Oc3CF+DkU5cKBHulRm5XP+8KvAW8Az52ZNpUGe4YkFKLsyipgFiqiE182QYtVA +vWrEMdmPNr9xbPb5GGg3lropINwy4T8w/WKEdjPttg== +-----END CERTIFICATE----- diff --git a/src/oidcc.app.src b/src/oidcc.app.src index 889531b..d57ac1f 100644 --- a/src/oidcc.app.src +++ b/src/oidcc.app.src @@ -3,6 +3,7 @@ {vsn, "3.1.2-beta.1"}, {registered, []}, {applications, [kernel, stdlib, inets, ssl, public_key, telemetry, jose]}, + {mod, {oidcc_app, []}}, {env, []}, {modules, []}, {licenses, ["Apache-2.0"]}, diff --git a/src/oidcc_app.erl b/src/oidcc_app.erl new file mode 100644 index 0000000..c2b9795 --- /dev/null +++ b/src/oidcc_app.erl @@ -0,0 +1,56 @@ +-module(oidcc_app). + +-export([start/2]). +-export([stop/1]). +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([httpc_profile/0]). + +-behaviour(application). +-behaviour(gen_server). + +%% @private +httpc_profile() -> + oidcc. + +%% Application Callbacks + +%% @private +start(_StartType, StartArgs) -> + gen_server:start_link(oidcc_app, StartArgs, []). + +%% @private +stop(_State) -> + ok. + +%% GenServer Callbacks +%% @private +init(_Args) -> + {ok, Pid} = inets:start(httpc, [{profile, httpc_profile()}]), + % disable keep-alive + httpc:set_options( + [ + {pipeline_timeout, 0}, + {keep_alive_timeout, 0}, + {max_sessions, 1} + ], + Pid + ), + + {ok, Pid, hibernate}. + +handle_call(_Call, _From, State) -> + {stop, unexpected_call, State}. + +handle_cast(_Call, State) -> + {stop, unexpected_cast, State}. + +handle_info(_Call, State) -> + {stop, unexpected_info, State}. + +terminate(_Reason, Pid) -> + inets:stop(httpc, Pid), + ok. diff --git a/src/oidcc_auth_util.erl b/src/oidcc_auth_util.erl index 860f511..0f6f8a3 100644 --- a/src/oidcc_auth_util.erl +++ b/src/oidcc_auth_util.erl @@ -32,7 +32,7 @@ -spec add_client_authentication( QueryList, Header, SupportedAuthMethods, AllowAlgorithms, Opts, ClientContext ) -> - {ok, {oidcc_http_util:query_params(), [oidcc_http_util:http_header()]}} + {ok, {oidcc_http_util:query_params(), [oidcc_http_util:http_header()]}, auth_method()} | {error, error()} when QueryList :: oidcc_http_util:query_params(), diff --git a/src/oidcc_http_util.erl b/src/oidcc_http_util.erl index 55d44b9..adb5072 100644 --- a/src/oidcc_http_util.erl +++ b/src/oidcc_http_util.erl @@ -92,10 +92,10 @@ request(Method, Request, TelemetryOpts, RequestOpts) -> SslOpts = maps:get(ssl, RequestOpts, undefined), HttpOpts0 = [{timeout, Timeout}], - HttpOpts = + {HttpOpts, HttpProfile} = case SslOpts of - undefined -> HttpOpts0; - _Opts -> [{ssl, SslOpts} | HttpOpts0] + undefined -> {HttpOpts0, default}; + _Opts -> {[{ssl, SslOpts} | HttpOpts0], oidcc_app:httpc_profile()} end, telemetry:span( @@ -108,7 +108,8 @@ request(Method, Request, TelemetryOpts, RequestOpts) -> Method, Request, HttpOpts, - [{body_format, binary}] + [{body_format, binary}], + HttpProfile ), {ok, BodyAndFormat} ?= extract_successful_response(Response), {{ok, {BodyAndFormat, Headers}}, TelemetryExtraMeta} diff --git a/test/oidcc_authorization_test.erl b/test/oidcc_authorization_test.erl index 518bc2a..b11444f 100644 --- a/test/oidcc_authorization_test.erl +++ b/test/oidcc_authorization_test.erl @@ -739,7 +739,8 @@ create_redirect_url_with_par_url_test() -> post, {ReqParEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> ?assertMatch(<<"https://my.server/par">>, ReqParEndpoint), ?assertMatch(none, proplists:lookup("authorization", Header)), @@ -838,7 +839,8 @@ create_redirect_url_with_par_error_when_required_test() -> post, {_Endpoint, _Header, _ContentType, _Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> {ok, {{"HTTP/1.1", 400, "OK"}, [{"content-type", "application/json"}], ParResponseData}} end, @@ -892,7 +894,8 @@ create_redirect_url_with_par_invalid_response_test() -> post, {_Endpoint, _Header, _ContentType, _Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> {ok, {{"HTTP/1.1", 201, "OK"}, [{"content-type", "application/json"}], ParResponseData}} end, @@ -957,7 +960,8 @@ create_redirect_url_with_par_client_secret_jwt_request_object_test() -> post, {_Endpoint, _Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> BodyParsed = uri_string:dissect_query(Body), BodyMap = maps:from_list(BodyParsed), diff --git a/test/oidcc_client_registration_test.erl b/test/oidcc_client_registration_test.erl index c397ebd..077861a 100644 --- a/test/oidcc_client_registration_test.erl +++ b/test/oidcc_client_registration_test.erl @@ -46,7 +46,8 @@ register_test() -> post, {ReqEndpoint, _Header, "application/json", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> RegistrationEndpoint = ReqEndpoint, @@ -164,7 +165,8 @@ registration_invalid_response_test() -> post, {ReqEndpoint, _Header, "application/json", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> RegistrationEndpoint = ReqEndpoint, diff --git a/test/oidcc_http_util_SUITE.erl b/test/oidcc_http_util_SUITE.erl new file mode 100644 index 0000000..09d331b --- /dev/null +++ b/test/oidcc_http_util_SUITE.erl @@ -0,0 +1,106 @@ +-module(oidcc_http_util_SUITE). + +-export([all/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([bad_ssl/1]). +-export([client_cert/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +all() -> + [ + bad_ssl, + client_cert + ]. + +init_per_suite(_Config) -> + {ok, _} = application:ensure_all_started(oidcc), + []. + +end_per_suite(_Config) -> + ok = application:stop(oidcc). + +telemetry_opts() -> + #{ + topic => [oidcc, oidcc_http_util_SUITE] + }. + +bad_ssl(_Config) -> + ?assertMatch( + {error, {failed_connect, _}}, + oidcc_http_util:request(get, {"https://expired.badssl.com/", []}, telemetry_opts(), #{}) + ), + + ?assertMatch( + {error, {failed_connect, _}}, + oidcc_http_util:request(get, {"https://wrong.host.badssl.com/", []}, telemetry_opts(), #{}) + ), + + ?assertMatch( + {error, {failed_connect, _}}, + oidcc_http_util:request(get, {"https://self-signed.badssl.com/", []}, telemetry_opts(), #{}) + ), + + ?assertMatch( + {error, {failed_connect, _}}, + oidcc_http_util:request( + get, {"https://untrusted-root.badssl.com/", []}, telemetry_opts(), #{} + ) + ), + + ?assertMatch( + {error, {failed_connect, _}}, + oidcc_http_util:request( + get, {"https://tls-v1-1.badssl.com:1011/", []}, telemetry_opts(), #{} + ) + ), + + ok. + +client_cert(_Config) -> + PrivDir = code:priv_dir(oidcc), + KeyFile = + PrivDir ++ + "/test/fixtures/jwk.pem", + CertFile = + PrivDir ++ + "/test/fixtures/jwk_cert.pem", + CertsKeys = [ + #{ + certfile => CertFile, + keyfile => KeyFile + } + ], + ?assertMatch( + {ok, { + {json, #{ + <<"SSL_CLIENT_I_DN">> := <<"CN=Oidcc,O=Erlang Ecosystem Foundation">> + }}, + _ + }}, + oidcc_http_util:request( + get, {"https://certauth.idrix.fr/json/", []}, telemetry_opts(), #{ + ssl => [ + {verify, verify_peer}, + {cacerts, public_key:cacerts_get()}, + {certs_keys, CertsKeys} + ] + } + ) + ), + + ?assertMatch( + {error, {http_error, 403, <<"">>}}, + oidcc_http_util:request( + get, {"https://certauth.idrix.fr/json/", []}, telemetry_opts(), #{ + ssl => [ + {verify, verify_peer}, + {cacerts, public_key:cacerts_get()} + ] + } + ) + ), + + ok. diff --git a/test/oidcc_provider_configuration_test.erl b/test/oidcc_provider_configuration_test.erl index 4c859d8..fb95b0f 100644 --- a/test/oidcc_provider_configuration_test.erl +++ b/test/oidcc_provider_configuration_test.erl @@ -593,7 +593,7 @@ document_overrides_quirk_test() -> uri_concatenation_test() -> ok = meck:new(httpc, [no_link]), HttpFun = - fun(get, {ReqEndpoint, _Header}, _HttpOpts, _Opts) -> + fun(get, {ReqEndpoint, _Header}, _HttpOpts, _Opts, _Profile) -> self() ! {req, ReqEndpoint}, {ok, {{"HTTP/1.1", 501, "Not Implemented"}, [], ""}} diff --git a/test/oidcc_token_test.erl b/test/oidcc_token_test.erl index f4084a4..e38dc8c 100644 --- a/test/oidcc_token_test.erl +++ b/test/oidcc_token_test.erl @@ -76,7 +76,8 @@ retrieve_none_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> ?assertEqual(<>, iolist_to_binary(ReqTokenEndpoint)), ?assertMatch({"authorization", _}, proplists:lookup("authorization", Header)), @@ -206,7 +207,8 @@ retrieve_rs256_with_rotation_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, ?assertMatch(none, proplists:lookup("authorization", Header)), @@ -308,7 +310,8 @@ retrieve_hs256_test() -> post, {ReqTokenEndpoint, _Header, "application/x-www-form-urlencoded", _Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/json"}], TokenData}} @@ -390,7 +393,8 @@ retrieve_hs256_with_max_clock_skew_test() -> post, {ReqTokenEndpoint, _Header, "application/x-www-form-urlencoded", _Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/json"}], TokenData}} @@ -490,7 +494,8 @@ auth_method_client_secret_jwt_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, ?assertMatch(none, proplists:lookup("authorization", Header)), @@ -615,7 +620,8 @@ auth_method_client_secret_jwt_with_max_clock_skew_test() -> post, {ReqTokenEndpoint, _, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, BodyMap = maps:from_list(uri_string:dissect_query(Body)), @@ -714,7 +720,8 @@ auth_method_private_key_jwt_no_supported_alg_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, @@ -819,7 +826,8 @@ auth_method_private_key_jwt_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, ?assertMatch(none, proplists:lookup("authorization", Header)), @@ -960,7 +968,8 @@ auth_method_private_key_jwt_with_dpop_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, ?assertMatch(none, proplists:lookup("authorization", Header)), @@ -1147,7 +1156,8 @@ auth_method_private_key_jwt_with_dpop_and_nonce_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, ?assertMatch(none, proplists:lookup("authorization", Header)), @@ -1320,7 +1330,8 @@ auth_method_private_key_jwt_with_invalid_dpop_nonce_test() -> post, {_Endpoint, _Header, "application/x-www-form-urlencoded", _Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> {ok, { {"HTTP/1.1", 400, "OK"}, @@ -1445,7 +1456,8 @@ preferred_auth_methods_test() -> post, {ReqTokenEndpoint, Header, "application/x-www-form-urlencoded", Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> TokenEndpoint = ReqTokenEndpoint, ?assertMatch({"authorization", _}, proplists:lookup("authorization", Header)), @@ -1646,7 +1658,8 @@ trusted_audiences_test() -> post, {_TokenEndpoint, _Header, "application/x-www-form-urlencoded", _Body}, _HttpOpts, - _Opts + _Opts, + _Profile ) -> {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/json"}], TokenData}} end, diff --git a/test/oidcc_userinfo_test.erl b/test/oidcc_userinfo_test.erl index 1892c1f..e0c0ffd 100644 --- a/test/oidcc_userinfo_test.erl +++ b/test/oidcc_userinfo_test.erl @@ -26,7 +26,7 @@ json_test() -> BadSub = <<"123789">>, HttpFun = - fun(get, {Url, _Header}, _HttpOpts, _Opts) -> + fun(get, {Url, _Header}, _HttpOpts, _Opts, _Profile) -> Url = UserInfoEndpoint, {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/json"}], HttpBody}} end, @@ -141,7 +141,7 @@ jwt_test() -> ), HttpFun = - fun(get, {Url, _Header}, _HttpOpts, _Opts) -> + fun(get, {Url, _Header}, _HttpOpts, _Opts, _Profile) -> Url = UserInfoEndpoint, {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/jwt"}], HttpBody}} end, @@ -247,7 +247,7 @@ jwt_encrypted_not_signed_test() -> ), HttpFun = - fun(get, {_Url, _Header}, _HttpOpts, _Opts) -> + fun(get, {_Url, _Header}, _HttpOpts, _Opts, _Profile) -> {ok, {{"HTTP/1.1", 200, "OK"}, [{"content-type", "application/jwt"}], HttpBody}} end, ok = meck:new(httpc), @@ -641,7 +641,7 @@ dpop_proof_test() -> ), HttpFun = - fun(get, {Url, Header}, _HttpOpts, _Opts) -> + fun(get, {Url, Header}, _HttpOpts, _Opts, _Profile) -> Url = UserInfoEndpoint, {_, Authorization} = proplists:lookup("authorization", Header), @@ -742,7 +742,7 @@ dpop_proof_case_insensitive_token_type_test() -> AccessToken = <<"opensesame">>, HttpFun = - fun(get, {Url, Header}, _HttpOpts, _Opts) -> + fun(get, {Url, Header}, _HttpOpts, _Opts, _Profile) -> Url = UserInfoEndpoint, {_, Authorization} = proplists:lookup("authorization", Header), @@ -816,7 +816,7 @@ dpop_proof_with_nonce_test() -> }), HttpFun = - fun(get, {Url, Header}, _HttpOpts, _Opts) -> + fun(get, {Url, Header}, _HttpOpts, _Opts, _Profile) -> Url = UserInfoEndpoint, {_, Authorization} = proplists:lookup("authorization", Header), @@ -933,7 +933,7 @@ dpop_proof_with_invalid_nonce_test() -> }), HttpFun = - fun(get, _UrlHeader, _HttpOpts, _Opts) -> + fun(get, _UrlHeader, _HttpOpts, _Opts, _Profile) -> {ok, { {"HTTP/1.1", 400, "Bad Request"}, [{"content-type", "application/json"}, {"dpop-nonce", DpopNonce}],