Skip to content

Commit

Permalink
fixup! feat: JARM
Browse files Browse the repository at this point in the history
  • Loading branch information
paulswartz committed Dec 30, 2023
1 parent e3ac6b0 commit 68c56fb
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 121 deletions.
40 changes: 40 additions & 0 deletions priv/test/fixtures/fapi2-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,45 @@
"A128GCM",
"A192GCM",
"A256GCM"
],
"authorization_signing_alg_values_supported": [
"HS256",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES256K",
"ES384",
"ES512",
"EdDSA"
],
"authorization_encryption_alg_values_supported": [
"RSA1_5",
"RSA-OAEP",
"RSA-OAEP-256",
"RSA-OAEP-384",
"RSA-OAEP-512",
"ECDH-ES",
"ECDH-ES+A128KW",
"ECDH-ES+A192KW",
"ECDH-ES+A256KW",
"A128KW",
"A192KW",
"A256KW",
"A128GCMKW",
"A192GCMKW",
"A256GCMKW",
"dir"
],
"authorization_encryption_enc_values_supported": [
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512",
"A128GCM",
"A192GCM",
"A256GCM"
]
}
18 changes: 7 additions & 11 deletions src/oidcc_jwt_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
| invalid_jwt_token
| {no_matching_key_with_kid, Kid :: binary()}
| none_alg_used
| {none_alg_used, Jwt :: #jose_jwt{}, Jws :: #jose_jws{}}.
| {none_alg_used, Jwt :: #jose_jwt{}, Jws :: #jose_jws{}}
| not_encrypted.

-type claims() :: #{binary() => term()}.

Expand Down Expand Up @@ -240,20 +241,16 @@ sign(Jwt, Jwk, [Algorithm | RestAlgorithms], JwsFields0) ->
) ->
{ok, binary()} | {error, no_supported_alg_or_key}.
decrypt_if_needed(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues) ->
maybe
{ok, _Jwe} ?= jwe_peek_protected(Jwt),
decrypt(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues)
else
{error, not_encrypted} ->
{ok, Jwt};
{error, Reason} ->
{error, Reason}
case decrypt(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues) of
{ok, Decrypted} -> {ok, Decrypted};
{error, not_encrypted} -> {ok, Jwt};
{error, Reason} -> {error, Reason}
end.

-spec jwe_peek_protected(Jwt :: binary()) ->
{ok, #jose_jwe{}} | {error, not_encrypted | no_matching_key}.
jwe_peek_protected(Jwt) ->
%% jose_jwt:peek_protected(Jwt) doesn't work with encrypted signed tokens
%% jose_jwt:peek_protected(Jwt) doesn't work with encrypted tokens
maybe
[ProtectedEncoded, _, _, _, _] ?= binary:split(Jwt, <<".">>, [global]),
Protected = jose_jwa_base64url:decode(ProtectedEncoded),
Expand Down Expand Up @@ -419,7 +416,6 @@ peek_payload(Jwt) ->
catch
error:{badarg, [_Token]} ->
{error, invalid_jwt_token};
%% Some Keys crash if a non matching alg is provided
error:function_clause ->
{error, invalid_jwt_token}
end.
140 changes: 65 additions & 75 deletions src/oidcc_token.erl
Original file line number Diff line number Diff line change
Expand Up @@ -396,88 +396,78 @@ validate_jarm(Response, ClientContext, Opts) ->
} = ClientContext,
#oidcc_provider_configuration{
issuer = Issuer,
response_modes_supported = ResponseModesSupported,
authorization_signing_alg_values_supported = SigningAlgSupported0,
authorization_encryption_alg_values_supported = EncryptionAlgSupported0,
authorization_encryption_enc_values_supported = EncryptionEncSupported0
} =
Configuration,

case
lists:member(<<"jwt">>, ResponseModesSupported) or
lists:member(<<"query.jwt">>, ResponseModesSupported) or
lists:member(<<"form_post.jwt">>, ResponseModesSupported)
of
true ->
SigningAlgSupported =
case SigningAlgSupported0 of
undefined -> [];
SigningAlgs -> SigningAlgs
end,
EncryptionAlgSupported =
case EncryptionAlgSupported0 of
undefined -> [];
EncryptionAlgs -> EncryptionAlgs
end,
EncryptionEncSupported =
case EncryptionEncSupported0 of
undefined -> [];
EncryptionEncs -> EncryptionEncs
end,
JwksWithClientJwks =
case ClientJwks of
none -> Jwks;
#jose_jwk{} -> oidcc_jwt_util:merge_jwks(Jwks, ClientJwks)
end,
SigningAlgSupported =
case SigningAlgSupported0 of
undefined -> [];
SigningAlgs -> SigningAlgs
end,
EncryptionAlgSupported =
case EncryptionAlgSupported0 of
undefined -> [];
EncryptionAlgs -> EncryptionAlgs
end,
EncryptionEncSupported =
case EncryptionEncSupported0 of
undefined -> [];
EncryptionEncs -> EncryptionEncs
end,
JwksWithClientJwks =
case ClientJwks of
none -> Jwks;
#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,
%% https://openid.net/specs/oauth-v2-jarm-final.html#name-processing-rules
%% 1. decrypt if necessary
%% 2. validate <<"iss">> claim
%% 3. validate <<"aud">> claim
%% 4. validate <<"exp">> claim
%% 5. validate signature (valid, not <<"none">> alg)
%% 6. continue processing
maybe
{ok, DecryptedResponse} ?=
oidcc_jwt_util:decrypt_if_needed(
Response,
EncryptionJwks,
EncryptionAlgSupported,
EncryptionEncSupported
),
ExpClaims = [
{<<"iss">>, Issuer}
],
TrustedAudience = maps:get(trusted_audiences, Opts, any),
{ok, #jose_jwt{fields = PeekClaims}} ?=
oidcc_jwt_util:peek_payload(DecryptedResponse),
ok ?= oidcc_jwt_util:verify_claims(PeekClaims, ExpClaims),
ok ?= verify_aud_claim(PeekClaims, ClientId, TrustedAudience),
ok ?= verify_exp_claim(PeekClaims),
ok ?= verify_nbf_claim(PeekClaims),
{ok, {#jose_jwt{fields = Claims}, Jws}} ?=
oidcc_jwt_util:verify_signature(
DecryptedResponse, SigningAlgSupported, SigningJwks
),
ok ?= oidcc_jwt_util:verify_not_none_alg(Jws),
{ok, Claims}
end;
false ->
{error, {response_mode_not_supported, jwt}}
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,
%% https://openid.net/specs/oauth-v2-jarm-final.html#name-processing-rules
%% 1. decrypt if necessary
%% 2. validate <<"iss">> claim
%% 3. validate <<"aud">> claim
%% 4. validate <<"exp">> claim
%% 5. validate signature (valid, not <<"none">> alg)
%% 6. continue processing
maybe
{ok, DecryptedResponse} ?=
oidcc_jwt_util:decrypt_if_needed(
Response,
EncryptionJwks,
EncryptionAlgSupported,
EncryptionEncSupported
),
ExpClaims = [
{<<"iss">>, Issuer}
],
TrustedAudience = maps:get(trusted_audiences, Opts, any),
{ok, #jose_jwt{fields = PeekClaims}} ?=
oidcc_jwt_util:peek_payload(DecryptedResponse),
ok ?= oidcc_jwt_util:verify_claims(PeekClaims, ExpClaims),
ok ?= verify_aud_claim(PeekClaims, ClientId, TrustedAudience),
ok ?= verify_exp_claim(PeekClaims),
ok ?= verify_nbf_claim(PeekClaims),
{ok, {#jose_jwt{fields = Claims}, Jws}} ?=
oidcc_jwt_util:verify_signature(
DecryptedResponse, SigningAlgSupported, SigningJwks
),
ok ?= oidcc_jwt_util:verify_not_none_alg(Jws),
{ok, Claims}
end.

%% @doc Refresh Token
Expand Down
45 changes: 10 additions & 35 deletions test/oidcc_token_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1609,7 +1609,7 @@ trusted_audiences_test() ->
client_id = ClientId,
jwks = Jwk,
provider_configuration = #oidcc_provider_configuration{issuer = Issuer}
} = client_context_fixture(),
} = client_context_fapi2_fixture(),

ExtraAudience = <<"audience_member">>,
LocalEndpoint = <<"https://my.server/auth">>,
Expand Down Expand Up @@ -1695,7 +1695,7 @@ trusted_audiences_test() ->
ok.

retrieve_pkce_required_test() ->
ClientContext = client_context_fixture(),
ClientContext = client_context_fapi2_fixture(),
RedirectUri = <<"https://redirect.example/">>,

?assertEqual(
Expand All @@ -1709,30 +1709,22 @@ retrieve_pkce_required_test() ->
ok.

validate_jarm_test() ->
ClientContext0 = client_context_fixture(),
ClientContext0 = client_context_fapi2_fixture(),
#oidcc_client_context{
client_id = ClientId,
jwks = Jwk,
provider_configuration =
#oidcc_provider_configuration{
issuer = Issuer
} = ProviderConfiguration0
}
} = ClientContext0,
EncAlgValue = <<"RSA-OAEP-256">>,
EncEncValue = <<"A256GCM">>,
ProviderConfiguration = ProviderConfiguration0#oidcc_provider_configuration{
authorization_signing_alg_values_supported = [<<"RS256">>],
authorization_encryption_alg_values_supported = [EncAlgValue],
authorization_encryption_enc_values_supported = [EncEncValue],
response_modes_supported = [<<"jwt">>]
},
EncJwk0 = jose_jwk:generate_key({rsa, 2048}),
EncJwk = EncJwk0#jose_jwk{fields = #{<<"use">> => <<"enc">>}},
ClientContext = ClientContext0#oidcc_client_context{
jwks = oidcc_jwt_util:merge_jwks(Jwk, EncJwk),
provider_configuration = ProviderConfiguration
jwks = oidcc_jwt_util:merge_jwks(Jwk, EncJwk)
},

Jws = #{<<"alg">> => <<"RS256">>},
AuthCode = <<"123456">>,
JarmClaims = #{
Expand Down Expand Up @@ -1764,22 +1756,15 @@ validate_jarm_test() ->
ok.

validate_jarm_invalid_token_test() ->
ClientContext0 = client_context_fixture(),
ClientContext = client_context_fapi2_fixture(),
#oidcc_client_context{
client_id = ClientId,
jwks = Jwk,
provider_configuration =
#oidcc_provider_configuration{
issuer = Issuer
} = ProviderConfiguration0
} = ClientContext0,
ProviderConfiguration = ProviderConfiguration0#oidcc_provider_configuration{
authorization_signing_alg_values_supported = [<<"RS256">>],
response_modes_supported = [<<"jwt">>]
},
ClientContext = ClientContext0#oidcc_client_context{
provider_configuration = ProviderConfiguration
},
}
} = ClientContext,

Jws = #{<<"alg">> => <<"RS256">>},
RedirectUri = <<"https://redirect.example/">>,
Expand Down Expand Up @@ -1905,20 +1890,10 @@ validate_jarm_invalid_token_test() ->

ok.

validate_jarm_response_mode_not_supported_test() ->
ClientContext = client_context_fixture(),

?assertEqual(
{error, {response_mode_not_supported, jwt}},
oidcc_token:validate_jarm("", ClientContext, #{})
),

ok.

client_context_fixture() ->
client_context_fapi2_fixture() ->
PrivDir = code:priv_dir(oidcc),

{ok, ConfigurationBinary} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"),
{ok, ConfigurationBinary} = file:read_file(PrivDir ++ "/test/fixtures/fapi2-metadata.json"),
{ok, Configuration} = oidcc_provider_configuration:decode_configuration(
jose:decode(ConfigurationBinary)
),
Expand Down

0 comments on commit 68c56fb

Please sign in to comment.