Skip to content

Commit

Permalink
feat: create an httpc profile which disables keep-alive/pipelining
Browse files Browse the repository at this point in the history
As noted in
#318 (comment),
`httpc` by default will re-use existing connections. This is great if
you're using normal HTTPS, but if you're using client authentication
then you need to make sure that every time `httpc` connects to a host,
it's using the client authentication, which is impossible in practice.

This works around that, by creating a new profile which disables that
functionality. Using that profile for requests which provide SSL
overrides will ensure that each of those requests will use the client certificate.
  • Loading branch information
paulswartz committed Jan 8, 2024
1 parent 881185e commit a2fa80f
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 33 deletions.
5 changes: 4 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 6 additions & 0 deletions priv/test/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Regenerating `jwk_cert.pem`

``` bash
openssl x509 -signkey jwk.pem -in jwk.csr -req -days 3650 -out jwk_cert.pem
```

16 changes: 16 additions & 0 deletions priv/test/fixtures/jwk.csr
Original file line number Diff line number Diff line change
@@ -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-----
19 changes: 19 additions & 0 deletions priv/test/fixtures/jwk_cert.pem
Original file line number Diff line number Diff line change
@@ -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-----
1 change: 1 addition & 0 deletions src/oidcc.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -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"]},
Expand Down
56 changes: 56 additions & 0 deletions src/oidcc_app.erl
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion src/oidcc_auth_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
9 changes: 5 additions & 4 deletions src/oidcc_http_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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}
Expand Down
12 changes: 8 additions & 4 deletions test/oidcc_authorization_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 4 additions & 2 deletions test/oidcc_client_registration_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ register_test() ->
post,
{ReqEndpoint, _Header, "application/json", Body},
_HttpOpts,
_Opts
_Opts,
_Profile
) ->
RegistrationEndpoint = ReqEndpoint,

Expand Down Expand Up @@ -164,7 +165,8 @@ registration_invalid_response_test() ->
post,
{ReqEndpoint, _Header, "application/json", Body},
_HttpOpts,
_Opts
_Opts,
_Profile
) ->
RegistrationEndpoint = ReqEndpoint,

Expand Down
106 changes: 106 additions & 0 deletions test/oidcc_http_util_SUITE.erl
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion test/oidcc_provider_configuration_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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"}, [], ""}}
Expand Down
Loading

0 comments on commit a2fa80f

Please sign in to comment.