Support in code the old keycloak format

That was not keycloak format it was an
extension to the oauth spec introuduced
a few years ago. To get a token from
keycloak using this format, a.k.a.
requesting party token, one has to specify
a different claim type called
urn:ietf:params:oauth:grant-type:uma-ticket
This commit is contained in:
Marcial Rosales 2025-02-10 13:44:48 +01:00
parent 1179d3a3ec
commit 3041d6c253
4 changed files with 60 additions and 49 deletions

View File

@ -22,6 +22,14 @@
%% End of Key JWT fields
%% UMA claim-type returns a RPT which is a token
%% where scopes are located under a map of list of objects which have
%% the scopes in the "scopes" attribute
%% Used by Keycloak, WSO2 and others.
%% https://en.wikipedia.org/wiki/User-Managed_Access#cite_note-docs.wso2.com-19
-define(SCOPES_LOCATION_IN_REQUESTING_PARTY_TOKEN, <<"authorization.permissions.scopes">>).
-type raw_jwt_token() :: binary() | #{binary() => any()}.
-type decoded_jwt_token() :: #{binary() => any()}.

View File

@ -30,7 +30,9 @@
-import(rabbit_oauth2_rar, [extract_scopes_from_rich_auth_request/2]).
-import(rabbit_oauth2_scope, [filter_matching_scope_prefix_and_drop_it/2]).
-import(rabbit_oauth2_scope, [
filter_matching_scope_prefix/2,
filter_matching_scope_prefix_and_drop_it/2]).
-ifdef(TEST).
-compile(export_all).
@ -240,15 +242,30 @@ extract_scopes_from_scope_claim(Payload) ->
-spec normalize_token_scope(
ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map().
normalize_token_scope(ResourceServer, Payload) ->
Payload1 = extract_scopes_from_rich_auth_request(ResourceServer,
extract_scopes_using_scope_aliases(ResourceServer,
extract_scopes_from_additional_scopes_key(ResourceServer,
extract_scopes_from_scope_claim(Payload)))),
FilteredScopes = filter_matching_scope_prefix_and_drop_it(
get_scope(Payload1), ResourceServer#resource_server.scope_prefix),
set_scope(FilteredScopes, Payload1).
filter_duplicates(
filter_matching_scope_prefix(ResourceServer,
extract_scopes_from_rich_auth_request(ResourceServer,
extract_scopes_using_scope_aliases(ResourceServer,
extract_scopes_from_additional_scopes_key(ResourceServer,
extract_scopes_from_requesting_party_token(ResourceServer,
extract_scopes_from_scope_claim(Payload))))))).
filter_duplicates(#{?SCOPE_JWT_FIELD := Scopes} = Payload) ->
set_scope(lists:usort(Scopes), Payload);
filter_duplicates(Payload) -> Payload.
-spec extract_scopes_from_requesting_party_token(
ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map().
extract_scopes_from_requesting_party_token(ResourceServer, Payload) ->
Path = ?SCOPES_LOCATION_IN_REQUESTING_PARTY_TOKEN,
case extract_token_value(ResourceServer, Payload, Path,
fun extract_scope_list_from_token_value/2) of
[] ->
Payload;
AdditionalScopes ->
set_scope(lists:flatten(AdditionalScopes) ++ get_scope(Payload), Payload)
end.
-spec extract_scopes_using_scope_aliases(
ResourceServer :: resource_server(), Payload :: map()) -> map().
@ -322,9 +339,9 @@ extract_token_value_from_map(R, Map, Acc, [KeyStr | Rest], Mapper) when is_map(M
{ok, L} when is_list(L) -> extract_token_value_from_list(R, L, Acc, Rest, Mapper);
{ok, Value} when Rest =:= [] -> Acc ++ Mapper(R, Value);
_ -> Acc
end;
extract_token_value_from_map(_, _, Acc, _, _Mapper) ->
Acc.
end.
%extract_token_value_from_map(_, _, Acc, _, _Mapper) ->
% Acc.
extract_token_value_from_list(_, [], Acc, [], _Mapper) ->
Acc;
@ -355,35 +372,13 @@ split_path(Path) when is_binary(Path) ->
ResourceServer :: resource_server(), Payload :: map()) -> map().
extract_scopes_from_additional_scopes_key(
#resource_server{additional_scopes_key = Key} = ResourceServer, Payload)
when is_list(Key) or is_binary(Key) ->
Paths = case Key of
B when is_binary(B) -> binary:split(B, <<" ">>, [global, trim_all]);
L when is_list(L) -> L
end,
when is_binary(Key) ->
Paths = binary:split(Key, <<" ">>, [global, trim_all]),
AdditionalScopes = [ extract_token_value(ResourceServer,
Payload, Path, fun extract_scope_list_from_token_value/2) || Path <- Paths],
set_scope(lists:flatten(AdditionalScopes) ++ get_scope(Payload), Payload);
extract_scopes_from_additional_scopes_key(_, Payload) -> Payload.
extract_additional_scopes(ResourceServer, ComplexClaim) ->
ResourceServerId = ResourceServer#resource_server.id,
case ComplexClaim of
L when is_list(L) -> L;
M when is_map(M) ->
case maps:get(ResourceServerId, M, undefined) of
undefined -> [];
Ks when is_list(Ks) ->
[erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- Ks];
ClaimBin when is_binary(ClaimBin) ->
UnprefixedClaims = binary:split(ClaimBin, <<" ">>, [global, trim_all]),
[erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- UnprefixedClaims];
_ -> []
end;
Bin when is_binary(Bin) ->
binary:split(Bin, <<" ">>, [global, trim_all]);
_ -> []
end.
%% A token may be present in the password credential or in the rabbit_auth_backend_oauth2
%% credential. The former is the most common scenario for the first time authentication.

View File

@ -7,10 +7,13 @@
-module(rabbit_oauth2_scope).
-include("oauth2.hrl").
-export([vhost_access/2,
resource_access/3,
topic_access/4,
concat_scopes/2,
filter_matching_scope_prefix/2,
filter_matching_scope_prefix_and_drop_it/2]).
-include_lib("rabbit_common/include/rabbit.hrl").
@ -93,10 +96,18 @@ parse_resource_pattern(Pattern, Permission) ->
_Other -> ignore
end.
-spec filter_matching_scope_prefix(ResourceServer :: resource_server(),
Payload :: map()) -> map().
filter_matching_scope_prefix(
#resource_server{scope_prefix = ScopePrefix},
#{?SCOPE_JWT_FIELD := Scopes} = Payload) ->
Payload#{?SCOPE_JWT_FIELD :=
filter_matching_scope_prefix_and_drop_it(Scopes, ScopePrefix)};
filter_matching_scope_prefix(_, Payload) -> Payload.
-spec filter_matching_scope_prefix_and_drop_it(list(), binary()|list()) -> list().
filter_matching_scope_prefix_and_drop_it(Scopes, <<"">>) -> Scopes;
filter_matching_scope_prefix_and_drop_it(Scopes, PrefixPattern) ->
filter_matching_scope_prefix_and_drop_it(Scopes, <<>>) -> Scopes;
filter_matching_scope_prefix_and_drop_it(Scopes, PrefixPattern) ->
PatternLength = byte_size(PrefixPattern),
lists:filtermap(
fun(ScopeEl) ->

View File

@ -43,7 +43,7 @@ all() ->
test_invalid_signature,
test_incorrect_kid,
normalize_token_scope_using_multiple_scopes_key,
normalize_token_scope_with_keycloak_scopes,
normalize_token_scope_with_requesting_party_token_scopes,
normalize_token_scope_with_rich_auth_request,
normalize_token_scope_with_rich_auth_request_using_regular_expression_with_cluster,
test_unsuccessful_access_with_a_token_that_uses_missing_scope_alias_in_scope_field,
@ -125,7 +125,7 @@ normalize_token_scope_using_multiple_scopes_key(_) ->
Pairs = [
%% common case
{
"keycloak format 1",
"keycloak format 1, i.e. requesting party token",
#{<<"authorization">> =>
#{<<"permissions">> =>
[#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>,
@ -186,10 +186,10 @@ normalize_token_scope_using_multiple_scopes_key(_) ->
additional_scopes_key = <<"authorization.permissions.scopes realm_access.roles resource_access.account.roles">>
},
Token = normalize_token_scope(ResourceServer, Token0),
?assertEqual(ExpectedScope, uaa_jwt:get_scope(Token), Case)
?assertEqual(lists:sort(ExpectedScope), lists:sort(uaa_jwt:get_scope(Token)), Case)
end, Pairs).
normalize_token_scope_with_keycloak_scopes(_) ->
normalize_token_scope_with_requesting_party_token_scopes(_) ->
Pairs = [
%% common case
{
@ -241,12 +241,9 @@ normalize_token_scope_with_keycloak_scopes(_) ->
],
lists:foreach(fun({Case, Authorization, ExpectedScope}) ->
ResourceServer0 = new_resource_server(<<"rabbitmq-resource">>),
ResourceServer = ResourceServer0#resource_server{
additional_scopes_key = <<"authorization.permissions.scopes">>
},
ResourceServer0 = new_resource_server(<<"rabbitmq-resource">>),
Token0 = #{<<"authorization">> => Authorization},
Token = normalize_token_scope(ResourceServer, Token0),
Token = normalize_token_scope(ResourceServer0, Token0),
?assertEqual(ExpectedScope, uaa_jwt:get_scope(Token), Case)
end, Pairs).
@ -431,7 +428,7 @@ normalize_token_scope_with_rich_auth_request(_) ->
}
],
[<<"tag:management">>, <<"tag:policymaker">>,
<<"tag:management">>, <<"tag:monitoring">> ]
<<"tag:monitoring">> ]
},
{ "should produce a scope for every user tag action but only for the clusters that match {resource_server_id}",
[ #{<<"type">> => ?RESOURCE_SERVER_TYPE,