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

tls_client_auth #328

Merged
merged 7 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions include/oidcc_provider_configuration.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
dpop_signing_alg_values_supported = undefined :: [binary()] | undefined,
%% RFC 9101 The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)
require_signed_request_object = false :: boolean(),
%% RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
mtls_endpoint_aliases = #{} :: #{binary() => uri_string:uri_string()},
%% RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
tls_client_certificate_bound_access_tokens = false :: boolean(),
%% Unknown Fields
extra_fields = #{} :: #{binary() => term()}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/oidcc/provider_configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ defmodule Oidcc.ProviderConfiguration do
authorization_encryption_enc_values_supported: [String.t()] | :undefined,
dpop_signing_alg_values_supported: [String.t()] | :undefined,
require_signed_request_object: boolean(),
mtls_endpoint_aliases: %{binary() => :uri_string.uri_string()},
tls_client_certificate_bound_access_tokens: boolean(),
extra_fields: %{String.t() => term()}
}

Expand Down
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: [
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
```

10 changes: 10 additions & 0 deletions priv/test/fixtures/fapi2-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
"userinfo_endpoint": "https://my.provider/userinfo",
"revocation_endpoint": "https://my.provider/revoke",
"jwks_uri": "https://my.provider/jwks",
"mtls_endpoint_aliases": {
"authorization_endpoint": "https://my.provider/tls/auth",
"registration_endpoint": "https://my.provider/tls/register",
"device_authorization_endpoint": "https://my.provider/tls/device/code",
"token_endpoint": "https://my.provider/tls/token",
"introspection_endpoint": "https://my.provider/tls/introspection",
"userinfo_endpoint": "https://my.provider/tls/userinfo"
},
"response_types_supported": [
"code",
"token",
Expand Down Expand Up @@ -42,8 +50,10 @@
"client_secret_post",
"client_secret_basic",
"private_key_jwt",
"tls_client_auth",
"unsupporeted_auth"
],
"tls_client_certificate_bound_access_tokens": true,
"claims_supported": [
"aud",
"email",
Expand Down
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-----
48 changes: 45 additions & 3 deletions src/oidcc_auth_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@
-export_type([auth_method/0, error/0]).

-type auth_method() ::
none | client_secret_basic | client_secret_post | client_secret_jwt | private_key_jwt.
none
| client_secret_basic
| client_secret_post
| client_secret_jwt
| private_key_jwt
| tls_client_auth.

-type error() :: no_supported_auth_method.

-export([add_client_authentication/6]).
-export([add_dpop_proof_header/5]).
-export([add_authorization_header/6]).
-export([maybe_mtls_endpoint/4]).

%% @private
-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 All @@ -42,6 +48,7 @@ add_client_authentication(
) ->
PreferredAuthMethods = maps:get(preferred_auth_methods, Opts, [
private_key_jwt,
tls_client_auth,
client_secret_jwt,
client_secret_post,
client_secret_basic,
Expand All @@ -55,7 +62,7 @@ add_client_authentication(
)
of
{ok, {QueryList, Header}} ->
{ok, {QueryList, Header}};
{ok, {QueryList, Header}, AuthMethod};
{error, _} ->
add_client_authentication(
QueryList0,
Expand Down Expand Up @@ -180,6 +187,22 @@ add_authentication(
else
_ ->
{error, auth_method_not_possible}
end;
add_authentication(
QsBodyList,
Header,
tls_client_auth,
_AllowAlgorithms,
Opts,
#oidcc_client_context{client_id = ClientId}
) ->
case Opts of
#{request_opts := #{ssl := _}} ->
%% only supported if custom SSL params are provided
NewBodyList = [{<<"client_id">>, ClientId} | QsBodyList],
{ok, {NewBodyList, Header}};
_ ->
{error, auth_method_not_possible}
end.

-spec select_preferred_auth(PreferredAuthMethods, AuthMethodsSupported) ->
Expand Down Expand Up @@ -315,6 +338,25 @@ add_authorization_header(
[oidcc_http_util:bearer_auth_header(AccessToken)]
end.

%% @private
-spec maybe_mtls_endpoint(
Endpoint, auth_method(), MtlsEndpointName, ClientContext
) -> Endpoint when
Endpoint :: uri_string:uri_string(),
MtlsEndpointName :: binary(),
ClientContext :: oidcc_client_context:t().
maybe_mtls_endpoint(Endpoint, tls_client_auth, MtlsEndpointName, ClientContext) ->
case
ClientContext#oidcc_client_context.provider_configuration#oidcc_provider_configuration.mtls_endpoint_aliases
of
#{MtlsEndpointName := MtlsEndpoint} ->
MtlsEndpoint;
_ ->
Endpoint
end;
maybe_mtls_endpoint(Endpoint, _AuthMethod, _EndpointName, _ClientContext) ->
Endpoint.

-spec dpop_proof(Method, Endpoint, Claims, ClientContext) -> {ok, binary()} | error when
Method :: post | get,
Endpoint :: uri_string:uri_string(),
Expand Down
12 changes: 9 additions & 3 deletions src/oidcc_authorization.erl
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ attempt_par(
issuer = Issuer,
token_endpoint_auth_methods_supported = SupportedAuthMethods,
token_endpoint_auth_signing_alg_values_supported = SigningAlgs,
pushed_authorization_request_endpoint = PushedAuthorizationRequestEndpoint
pushed_authorization_request_endpoint = PushedAuthorizationRequestEndpoint0
}
} = ClientContext,
Opts
Expand All @@ -356,10 +356,10 @@ attempt_par(
%% 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},
AuthenticationOpts = maps:put(audience, Issuer, Opts),

maybe
{ok, {Body0, Header}} ?=
{ok, {Body0, Header}, AuthMethod} ?=
oidcc_auth_util:add_client_authentication(
QueryParams,
Header0,
Expand All @@ -370,6 +370,12 @@ attempt_par(
),
%% ensure no duplicate parameters (such as client_id)
Body = lists:ukeysort(1, Body0),
PushedAuthorizationRequestEndpoint = oidcc_auth_util:maybe_mtls_endpoint(
PushedAuthorizationRequestEndpoint0,
AuthMethod,
<<"pushed_authorization_request_endpoint">>,
ClientContext
),
Request =
{PushedAuthorizationRequestEndpoint, Header, "application/x-www-form-urlencoded",
uri_string:compose_query(Body)},
Expand Down
44 changes: 44 additions & 0 deletions src/oidcc_decode_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
-export([parse_setting_number/2]).
-export([parse_setting_uri/2]).
-export([parse_setting_uri_https/2]).
-export([parse_setting_uri_map/2]).
-export([parse_setting_uri_https_map/2]).

-export_type([error/0]).

Expand Down Expand Up @@ -93,6 +95,48 @@ parse_setting_uri_https(Setting, Field) when is_binary(Setting) ->
parse_setting_uri_https(_Setting, Field) ->
{error, {invalid_config_property, {uri_https, Field}}}.

%% @private
-spec parse_setting_uri_map(Setting :: term(), Field :: atom()) ->
{ok, #{binary() => uri_string:uri_string()}} | {error, error()}.
parse_setting_uri_map(Setting, Field) ->
do_parse_setting_uri_map(Setting, Field, fun parse_setting_uri/2).

%% @private
-spec parse_setting_uri_https_map(Setting :: term(), Field :: atom()) ->
{ok, #{binary() => uri_string:uri_string()}} | {error, error()}.
parse_setting_uri_https_map(Setting, Field) ->
do_parse_setting_uri_map(Setting, Field, fun parse_setting_uri_https/2).

do_parse_setting_uri_map(#{} = Setting, Field, Parser) ->
SettingList = maps:to_list(Setting),
case
lists:foldl(
fun
(_Elem, {error, Reason}) ->
{error, Reason};
({BinKey, Value}, {ok, Acc}) when is_binary(BinKey) ->
case Parser(Value, Field) of
{ok, SettingValue} ->
{ok, [{BinKey, SettingValue} | Acc]};
{error, Reason} ->
{error, Reason}
end;
(_, _) ->
{error, {invalid_config_property, {uri_map, Field}}}
end,

{ok, []},
SettingList
)
of
{ok, ParsedList} ->
{ok, maps:from_list(ParsedList)};
{error, Reason} ->
{error, Reason}
end;
do_parse_setting_uri_map(_Setting, Field, _Parser) ->
{error, {invalid_config_property, {uri_map, Field}}}.

%% @private
-spec parse_setting_binary(Setting :: term(), Field :: atom()) ->
{ok, binary()} | {error, error()}.
Expand Down
7 changes: 5 additions & 2 deletions src/oidcc_http_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@

-type request_opts() :: #{
timeout => timeout(),
ssl => [ssl:tls_option()]
ssl => [ssl:tls_option()],
httpc_profile => atom() | pid()
}.
%% See {@link httpc:request/5}
%%
Expand Down Expand Up @@ -90,6 +91,7 @@ request(Method, Request, TelemetryOpts, RequestOpts) ->
TelemetryExtraMeta = maps:get(extra_meta, TelemetryOpts, #{}),
Timeout = maps:get(timeout, RequestOpts, timer:minutes(1)),
SslOpts = maps:get(ssl, RequestOpts, undefined),
HttpProfile = maps:get(httpc_profile, RequestOpts, default),

HttpOpts0 = [{timeout, Timeout}],
HttpOpts =
Expand All @@ -108,7 +110,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
36 changes: 32 additions & 4 deletions src/oidcc_profile.erl
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
profiles => [profile()],
require_pkce => boolean(),
trusted_audiences => [binary()] | any,
preferred_auth_methods => [oidcc_auth_util:auth_method()]
preferred_auth_methods => [oidcc_auth_util:auth_method()],
request_opts => oidcc_http_util:request_opts()
}.
-type opts_no_profiles() :: #{
require_pkce => boolean(),
trusted_audiences => [binary()] | any,
preferred_auth_methods => [oidcc_auth_util:auth_method()]
preferred_auth_methods => [oidcc_auth_util:auth_method()],
request_opts => oidcc_http_util:request_opts()
}.
-type error() :: {unknown_profile, atom()}.

Expand Down Expand Up @@ -60,8 +62,17 @@ apply_profiles(
),
Opts2 = Opts1#{profiles => RestProfiles},
Opts3 = map_put_new(trusted_audiences, [], Opts2),
%% TODO include tls_client_auth">> here when it's supported by the library.
Opts = map_put_new(preferred_auth_methods, [private_key_jwt], Opts3),
Opts4 = map_put_new(preferred_auth_methods, [private_key_jwt, tls_client_auth], Opts3),
Opts5 = put_tls_defaults(Opts4),
Opts = limit_tls_ciphers(
[
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
],
Opts5
),
apply_profiles(ClientContext, Opts);
apply_profiles(
#oidcc_client_context{} = ClientContext0,
Expand Down Expand Up @@ -182,6 +193,23 @@ limit_signing_alg_values(AlgSupported, ClientContext0) ->
},
ClientContext.

put_tls_defaults(Opts) ->
RequestOpts0 = maps:get(request_opts, Opts, #{}),
SslOpts0 = maps:get(ssl, RequestOpts0, []),
SslOpts1 = SslOpts0 ++ httpc:ssl_verify_host_options(true),
SslOpts = lists:ukeysort(1, SslOpts1),
RequestOpts = RequestOpts0#{ssl => SslOpts},
Opts#{request_opts => RequestOpts}.

limit_tls_ciphers(SupportedCipherStrs, Opts) ->
RequestOpts0 = maps:get(request_opts, Opts, #{}),
SslOpts0 = maps:get(ssl, RequestOpts0, []),
SupportedCiphers = lists:map(fun ssl:str_to_suite/1, SupportedCipherStrs),
SslOpts1 = [{ciphers, SupportedCiphers} | SslOpts0],
SslOpts = lists:ukeysort(1, SslOpts1),
RequestOpts = RequestOpts0#{ssl => SslOpts},
Opts#{request_opts => RequestOpts}.

limit_values(_Limit, undefined) ->
undefined;
limit_values(Limit, Values) ->
Expand Down
Loading
Loading