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: support encrypted ID tokens and Userinfo responses #326

Merged
merged 1 commit into from
Jan 4, 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
20 changes: 6 additions & 14 deletions src/oidcc_authorization.erl
Original file line number Diff line number Diff line change
Expand Up @@ -252,20 +252,12 @@ attempt_request_object(QueryParams, #oidcc_client_context{
#jose_jwk{} -> oidcc_jwt_util:merge_jwks(Jwks, ClientJwks)
end,

SigningJwks =
case oidcc_jwt_util:client_secret_oct_keys(SigningAlgSupported, ClientSecret) of
none ->
JwksWithClientJwks;
SigningOctJwk ->
oidcc_jwt_util:merge_jwks(JwksWithClientJwks, SigningOctJwk)
end,
EncryptionJwks =
case oidcc_jwt_util:client_secret_oct_keys(EncryptionAlgSupported, ClientSecret) of
none ->
JwksWithClientJwks;
EncryptionOctJwk ->
oidcc_jwt_util:merge_jwks(JwksWithClientJwks, EncryptionOctJwk)
end,
SigningJwks = oidcc_jwt_util:merge_client_secret_oct_keys(
JwksWithClientJwks, SigningAlgSupported, ClientSecret
),
EncryptionJwks = oidcc_jwt_util:merge_client_secret_oct_keys(
JwksWithClientJwks, EncryptionAlgSupported, ClientSecret
),

MaxClockSkew =
case application:get_env(oidcc, max_clock_skew) of
Expand Down
86 changes: 79 additions & 7 deletions src/oidcc_jwt_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
-include_lib("jose/include/jose_jwt.hrl").

-export([client_secret_oct_keys/2]).
-export([merge_client_secret_oct_keys/3]).
-export([decrypt_and_verify/5]).
-export([decrypt_if_needed/4]).
-export([encrypt/4]).
-export([evaluate_for_all_keys/2]).
Expand Down Expand Up @@ -133,22 +135,37 @@ verify_claims(Claims, ExpClaims) ->
%% @private
-spec client_secret_oct_keys(AllowedAlgorithms, ClientSecret) -> jose_jwk:key() | none when
AllowedAlgorithms :: [binary()] | undefined,
ClientSecret :: binary().
ClientSecret :: binary() | unauthenticated.
client_secret_oct_keys(undefined, _ClientSecret) ->
none;
client_secret_oct_keys(_AllowedAlgorithms, unauthenticated) ->
none;
client_secret_oct_keys(AllowedAlgorithms, ClientSecret) ->
case
lists:member(<<"HS256">>, AllowedAlgorithms) or
lists:member(<<"HS384">>, AllowedAlgorithms) or
lists:member(<<"HS512">>, AllowedAlgorithms)
of
true ->
Jwk = jose_jwk:from_oct(ClientSecret),
Jwk#jose_jwk{fields = maps:merge(Jwk#jose_jwk.fields, #{<<"use">> => <<"sig">>})};
jose_jwk:from_oct(ClientSecret);
false ->
none
end.

%% @private
-spec merge_client_secret_oct_keys(Jwks :: jose_jwk:key(), AllowedAlgorithms, ClientSecret) ->
jose_jwk:key()
when
AllowedAlgorithms :: [binary()] | undefined,
ClientSecret :: binary() | unauthenticated.
merge_client_secret_oct_keys(Jwks, AllowedAlgorithms, ClientSecret) ->
case client_secret_oct_keys(AllowedAlgorithms, ClientSecret) of
none ->
Jwks;
OctKeys ->
merge_jwks(Jwks, OctKeys)
end.

%% @private
-spec refresh_jwks_fun(ProviderConfigurationWorkerName) ->
refresh_jwks_for_unknown_kid_fun()
Expand Down Expand Up @@ -232,6 +249,35 @@ sign(Jwt, Jwk, [Algorithm | RestAlgorithms], JwsFields0) ->
_ -> sign(Jwt, Jwk, RestAlgorithms, JwsFields0)
end.

%% private
-spec decrypt_and_verify(
Jwt :: binary(),
Jwks :: jose_jwk:key(),
SigningAlgs :: [binary()] | undefined,
EncryptionAlgs :: [binary()] | undefined,
EncryptionEncs :: [binary()] | undefined
) ->
{ok, {#jose_jwt{}, #jose_jwe{} | #jose_jws{}}} | {error, error()}.
decrypt_and_verify(Jwt, Jwks, SigningAlgs, EncryptionAlgs, EncryptionEncs) ->
%% we call jwe_peek_protected/1 before `decrypt/4' so that we can
%% handle unencrypted tokens in the case where SupportedAlgorithms /
%% SupportedEncValues are undefined (where `decrypt/4' returns
%% {error, no_supported_alg_or_key}).
case jwe_peek_protected(Jwt) of
{ok, Jwe} ->
case decrypt(Jwt, Jwks, EncryptionAlgs, EncryptionEncs) of
{ok, Decrypted} ->
verify_decrypted_token(Decrypted, SigningAlgs, Jwe, Jwks);
{error, Reason} ->
{error, Reason}
end;
{error, not_encrypted} ->
%% signed JWT
verify_signature(Jwt, SigningAlgs, Jwks);
{error, Reason} ->
{error, Reason}
end.

%% @private
-spec decrypt_if_needed(
Jwt :: binary(),
Expand All @@ -241,8 +287,14 @@ sign(Jwt, Jwk, [Algorithm | RestAlgorithms], JwsFields0) ->
) ->
{ok, binary()} | {error, no_supported_alg_or_key}.
decrypt_if_needed(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues) ->
case decrypt(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues) of
{ok, Decrypted} -> {ok, Decrypted};
maybe
%% we call jwe_peek_protected/1 before `decrypt/4' so that we can
%% handle unencrypted tokens in the case where SupportedAlgorithms /
%% SupportedEncValues are undefined (where `decrypt/4' returns
%% {error, no_supported_alg_or_key}).
{ok, _Jwe} ?= jwe_peek_protected(Jwt),
decrypt(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues)
else
{error, not_encrypted} -> {ok, Jwt};
{error, Reason} -> {error, Reason}
end.
Expand Down Expand Up @@ -308,6 +360,8 @@ decrypt(Jwt, #jose_jwk{} = Jwk, SupportedAlgorithms, SupportedEncValues) ->
case Jwk of
#jose_jwk{fields = #{<<"kid">> := CmpKid}} when CmpKid =/= Kid, Kid =/= none ->
{error, {no_matching_key_with_kid, Kid}};
#jose_jwk{fields = #{<<"use">> := NotEnc}} when NotEnc =/= <<"enc">> ->
{error, no_matching_key};
_ ->
try
{Token, _Jwe} = jose_jwe:block_decrypt(Jwk, Jwt),
Expand All @@ -329,6 +383,21 @@ verify_in_list(Value, List) ->
{error, no_matching_key}
end.

verify_decrypted_token(Jwt, SigningAlgs, Jwe, Jwks) ->
case verify_signature(Jwt, SigningAlgs, Jwks) of
{ok, Result} ->
%% encrypted + signed (nested) JWT
{ok, Result};
{error, invalid_jwt_token} ->
%% encrypted JWT
try
{ok, {jose_jwt:from_binary(Jwt), Jwe}}
catch
_ -> {error, invalid_jwt_token}
end;
{error, Reason} ->
{error, Reason}
end.
%% @private
-spec encrypt(
Jwt :: binary(),
Expand Down Expand Up @@ -359,10 +428,12 @@ encrypt(Jwt, Jwk, [_Algorithm | RestAlgorithms], SupportedEncValues, []) ->
encrypt(Jwt, Jwk, [Algorithm | _RestAlgorithms] = SupportedAlgorithms, SupportedEncValues, [
EncValue | RestEncValues
]) ->
JweParams0 = #{<<"alg">> => Algorithm, <<"enc">> => EncValue},
EncryptionCallback = fun
(#jose_jwk{fields = #{<<"use">> := <<"enc">>} = Fields} = Key) ->
(#jose_jwk{fields = #{<<"use">> := NotEnc}}) when NotEnc =/= <<"enc">> ->
error;
(#jose_jwk{fields = Fields} = Key) ->
try
JweParams0 = #{<<"alg">> => Algorithm, <<"enc">> => EncValue},
JweParams =
case maps:get(<<"kid">>, Fields, undefined) of
undefined -> JweParams0;
Expand All @@ -372,6 +443,7 @@ encrypt(Jwt, Jwk, [Algorithm | _RestAlgorithms] = SupportedAlgorithms, Supported
{_Jws, Token} = jose_jwe:compact(jose_jwk:block_encrypt(Jwt, Jwe, Key)),
{ok, Token}
catch
error:undef -> error;
error:{not_supported, _Alg} -> error
end;
(_Key) ->
Expand Down
31 changes: 17 additions & 14 deletions src/oidcc_token.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
-include("oidcc_provider_configuration.hrl").
-include("oidcc_token.hrl").

-include_lib("jose/include/jose_jwe.hrl").
-include_lib("jose/include/jose_jwk.hrl").
-include_lib("jose/include/jose_jws.hrl").
-include_lib("jose/include/jose_jwt.hrl").
Expand Down Expand Up @@ -895,13 +896,16 @@ validate_id_token(IdToken, ClientContext, any) ->
validate_id_token(IdToken, ClientContext, Opts) when is_map(Opts) ->
#oidcc_client_context{
provider_configuration = Configuration,
jwks = #jose_jwk{} = Jwks,
jwks = #jose_jwk{} = Jwks0,
client_id = ClientId,
client_secret = ClientSecret
client_secret = ClientSecret,
client_jwks = ClientJwks
} =
ClientContext,
#oidcc_provider_configuration{
id_token_signing_alg_values_supported = AllowAlgorithms,
id_token_encryption_alg_values_supported = EncryptionAlgs,
id_token_encryption_enc_values_supported = EncryptionEncs,
issuer = Issuer
} =
Configuration,
Expand All @@ -918,19 +922,16 @@ validate_id_token(IdToken, ClientContext, Opts) when is_map(Opts) ->
Bin when is_binary(Bin) ->
[{<<"nonce">>, Nonce} | ExpClaims0]
end,
JwksInclOct =
case ClientSecret of
unauthenticated ->
Jwks;
Secret ->
case oidcc_jwt_util:client_secret_oct_keys(AllowAlgorithms, Secret) of
none ->
Jwks;
OctJwk ->
oidcc_jwt_util:merge_jwks(Jwks, OctJwk)
end
Jwks1 =
case ClientJwks of
none -> Jwks0;
#jose_jwk{} -> oidcc_jwt_util:merge_jwks(Jwks0, ClientJwks)
end,
MaybeVerified = oidcc_jwt_util:verify_signature(IdToken, AllowAlgorithms, JwksInclOct),
Jwks2 = oidcc_jwt_util:merge_client_secret_oct_keys(Jwks1, AllowAlgorithms, ClientSecret),
Jwks = oidcc_jwt_util:merge_client_secret_oct_keys(Jwks2, EncryptionAlgs, ClientSecret),
MaybeVerified = oidcc_jwt_util:decrypt_and_verify(
IdToken, Jwks, AllowAlgorithms, EncryptionAlgs, EncryptionEncs
),
{ok, {#jose_jwt{fields = Claims}, Jws}} ?=
case MaybeVerified of
{ok, Valid} ->
Expand All @@ -950,6 +951,8 @@ validate_id_token(IdToken, ClientContext, Opts) when is_map(Opts) ->
#jose_jws{alg = {jose_jws_alg_none, none}} ->
{error, {none_alg_used, Claims}};
#jose_jws{} ->
{ok, Claims};
#jose_jwe{} ->
{ok, Claims}
end
end.
Expand Down
73 changes: 49 additions & 24 deletions src/oidcc_userinfo.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
-include("oidcc_provider_configuration.hrl").
-include("oidcc_token.hrl").

-include_lib("jose/include/jose_jwe.hrl").
-include_lib("jose/include/jose_jwk.hrl").
-include_lib("jose/include/jose_jws.hrl").
-include_lib("jose/include/jose_jwt.hrl").

-export([retrieve/3]).
Expand Down Expand Up @@ -188,24 +190,33 @@ validate_userinfo_body({json, Userinfo}, _ClientContext, Opts) ->
{ExpectedSubject, #{<<"sub">> := ExpectedSubject} = Map} -> {ok, Map};
{_, #{}} -> {error, bad_subject}
end;
validate_userinfo_body({jwt, UserinfoBody}, ClientContext, Opts) ->
validate_userinfo_body({jwt, UserinfoBody}, ClientContext, Opts0) ->
#oidcc_client_context{provider_configuration = Configuration, client_id = ClientId} =
ClientContext,
#oidcc_provider_configuration{issuer = Issuer} = Configuration,
ExpectedSubject = maps:get(expected_subject, Opts),
ExpectedClaims0 = [
ExpectedSubject = maps:get(expected_subject, Opts0),
%% only validate these claims if the token is signed:
%% https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.3.2
ExpectedSignedClaims = [
{<<"aud">>, ClientId},
{<<"iss">>, Issuer}
],
ExpectedClaims =
case maps:get(expected_subject, Opts) of
any -> ExpectedClaims0;
ExpectedSubject -> [{<<"sub">>, ExpectedSubject} | ExpectedClaims0]
case maps:get(expected_subject, Opts0) of
any -> [];
ExpectedSubject -> [{<<"sub">>, ExpectedSubject}]
end,
Opts = maps:merge(
#{
expected_signed_claims => ExpectedSignedClaims,
expected_claims => ExpectedClaims
},
Opts0
),
validate_userinfo_token(
UserinfoBody,
ClientContext,
maps:put(expected_claims, ExpectedClaims, Opts)
Opts
).

-spec validate_userinfo_token(Token, ClientContext, Opts) ->
Expand All @@ -217,45 +228,59 @@ when
#{
refresh_jwks => oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun(),
expected_subject => binary(),
expected_signed_claims => [{binary(), term()}],
expected_claims => [{binary(), term()}]
},
Claims :: oidcc_jwt_util:claims().
validate_userinfo_token(UserinfoToken, ClientContext, Opts) ->
RefreshJwksFun = maps:get(refresh_jwks, Opts, undefined),
ExpClaims = maps:get(expected_claims, Opts, []),
#oidcc_client_context{
provider_configuration = Configuration,
jwks = #jose_jwk{} = Jwks,
jwks = #jose_jwk{} = Jwks0,
client_id = ClientId,
client_secret = ClientSecret
client_secret = ClientSecret,
client_jwks = ClientJwks
} =
ClientContext,
#oidcc_provider_configuration{
userinfo_signing_alg_values_supported = AllowAlgorithms,
userinfo_encryption_alg_values_supported = EncryptionAlgs,
userinfo_encryption_enc_values_supported = EncryptionEncs,
issuer = Issuer
} =
Configuration,
maybe
JwksInclOct =
case ClientSecret of
unauthenticated ->
Jwks;
Secret ->
case oidcc_jwt_util:client_secret_oct_keys(AllowAlgorithms, Secret) of
none ->
Jwks;
OctJwk ->
oidcc_jwt_util:merge_jwks(Jwks, OctJwk)
end
Jwks1 = oidcc_jwt_util:merge_client_secret_oct_keys(Jwks0, AllowAlgorithms, ClientSecret),
Jwks2 = oidcc_jwt_util:merge_client_secret_oct_keys(Jwks1, EncryptionAlgs, ClientSecret),
Jwks =
case ClientJwks of
#jose_jwk{} ->
oidcc_jwt_util:merge_jwks(Jwks2, ClientJwks);
_ ->
Jwks2
end,
{ok, {#jose_jwt{fields = Claims}, JwsOrJwe}} ?=
oidcc_jwt_util:decrypt_and_verify(
UserinfoToken,
Jwks,
AllowAlgorithms,
EncryptionAlgs,
EncryptionEncs
),
ExpClaims =
case JwsOrJwe of
#jose_jws{} ->
maps:get(expected_claims, Opts, []) ++
maps:get(expected_signed_claims, Opts, []);
#jose_jwe{} ->
maps:get(expected_claims, Opts, [])
end,
{ok, {#jose_jwt{fields = Claims}, _Jws}} ?=
oidcc_jwt_util:verify_signature(UserinfoToken, AllowAlgorithms, JwksInclOct),
ok ?= oidcc_jwt_util:verify_claims(Claims, ExpClaims),
{ok, maps:remove(nonce, Claims)}
else
{error, {no_matching_key_with_kid, Kid}} when RefreshJwksFun =/= undefined ->
maybe
{ok, RefreshedJwks} ?= RefreshJwksFun(Jwks, Kid),
{ok, RefreshedJwks} ?= RefreshJwksFun(Jwks0, Kid),
RefreshedClientContext = ClientContext#oidcc_client_context{jwks = RefreshedJwks},
validate_userinfo_token(UserinfoToken, RefreshedClientContext, Opts)
end;
Expand Down
3 changes: 2 additions & 1 deletion test/oidcc_authorization_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ create_redirect_url_with_request_object_no_hmac_test() ->
ClientId = <<"client_id">>,
ClientSecret = <<"">>,

Jwks = jose_jwk:to_public(jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem")),
Jwks0 = jose_jwk:to_public(jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem")),
Jwks = Jwks0#jose_jwk{fields = #{<<"use">> => <<"sig">>}},

ClientJwks0 = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"),
ClientJwks = ClientJwks0#jose_jwk{fields = #{<<"use">> => <<"sig">>}},
Expand Down
Loading
Loading