Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 0 additions & 3 deletions deps/rabbitmq_auth_backend_oauth2/app.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ def all_beam_files(name = "all_beam_files"):
"src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl",
"src/rabbit_auth_backend_oauth2.erl",
"src/rabbit_auth_backend_oauth2_app.erl",
"src/rabbit_oauth2_keycloak.erl",
"src/rabbit_oauth2_provider.erl",
"src/rabbit_oauth2_rar.erl",
"src/rabbit_oauth2_resource_server.erl",
Expand Down Expand Up @@ -51,7 +50,6 @@ def all_test_beam_files(name = "all_test_beam_files"):
"src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl",
"src/rabbit_auth_backend_oauth2.erl",
"src/rabbit_auth_backend_oauth2_app.erl",
"src/rabbit_oauth2_keycloak.erl",
"src/rabbit_oauth2_provider.erl",
"src/rabbit_oauth2_rar.erl",
"src/rabbit_oauth2_resource_server.erl",
Expand Down Expand Up @@ -101,7 +99,6 @@ def all_srcs(name = "all_srcs"):
"src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl",
"src/rabbit_auth_backend_oauth2.erl",
"src/rabbit_auth_backend_oauth2_app.erl",
"src/rabbit_oauth2_keycloak.erl",
"src/rabbit_oauth2_provider.erl",
"src/rabbit_oauth2_rar.erl",
"src/rabbit_oauth2_resource_server.erl",
Expand Down
8 changes: 8 additions & 0 deletions deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -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()}.

Expand Down
225 changes: 140 additions & 85 deletions deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
get_scope/1, set_scope/2,
resolve_resource_server/1]).

-import(rabbit_oauth2_keycloak, [has_keycloak_scopes/1, extract_scopes_from_keycloak_format/1]).
-import(rabbit_oauth2_rar, [extract_scopes_from_rich_auth_request/2, has_rich_auth_request_scopes/1]).
-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).
Expand Down Expand Up @@ -229,98 +230,152 @@ check_token(Token, {ResourceServer, InternalOAuthProvider}) ->
{false, _} -> {refused, signature_invalid}
end.

extract_scopes_from_scope_claim(Payload) ->
case maps:find(?SCOPE_JWT_FIELD, Payload) of
{ok, Bin} when is_binary(Bin) ->
maps:put(?SCOPE_JWT_FIELD,
binary:split(Bin, <<" ">>, [global, trim_all]),
Payload);
_ -> Payload
end.

-spec normalize_token_scope(
ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map().
normalize_token_scope(ResourceServer, Payload) ->
Payload0 = maps:map(fun(K, V) ->
case K of
?SCOPE_JWT_FIELD when is_binary(V) ->
binary:split(V, <<" ">>, [global, trim_all]);
_ -> V
end
end, Payload),

Payload1 = case has_additional_scopes_key(ResourceServer, Payload0) of
true -> extract_scopes_from_additional_scopes_key(ResourceServer, Payload0);
false -> Payload0
end,

Payload2 = case has_keycloak_scopes(Payload1) of
true -> extract_scopes_from_keycloak_format(Payload1);
false -> Payload1
end,

Payload3 = case ResourceServer#resource_server.scope_aliases of
undefined -> Payload2;
ScopeAliases -> extract_scopes_using_scope_aliases(ScopeAliases, Payload2)
end,

Payload4 = case has_rich_auth_request_scopes(Payload3) of
true -> extract_scopes_from_rich_auth_request(ResourceServer, Payload3);
false -> Payload3
end,

FilteredScopes = filter_matching_scope_prefix_and_drop_it(
get_scope(Payload4), ResourceServer#resource_server.scope_prefix),
set_scope(FilteredScopes, Payload4).

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(
ScopeAliasMapping :: map(), Payload :: map()) -> map().
extract_scopes_using_scope_aliases(ScopeAliasMapping, Payload) ->
Scopes0 = get_scope(Payload),
Scopes = rabbit_data_coercion:to_list_of_binaries(Scopes0),
%% for all scopes, look them up in the scope alias map, and if they are
%% present, add the alias to the final scope list. Note that we also preserve
%% the original scopes, it should not hurt.
ExpandedScopes =
lists:foldl(fun(ScopeListItem, Acc) ->
case maps:get(ScopeListItem, ScopeAliasMapping, undefined) of
undefined ->
Acc;
MappedList when is_list(MappedList) ->
Binaries = rabbit_data_coercion:to_list_of_binaries(MappedList),
Acc ++ Binaries;
Value ->
Binaries = rabbit_data_coercion:to_list_of_binaries(Value),
Acc ++ Binaries
end
end, Scopes, Scopes),
set_scope(ExpandedScopes, Payload).

-spec has_additional_scopes_key(
ResourceServer :: resource_server(), Payload :: map()) -> boolean().
has_additional_scopes_key(ResourceServer, Payload) when is_map(Payload) ->
case ResourceServer#resource_server.additional_scopes_key of
undefined -> false;
ScopeKey -> maps:is_key(ScopeKey, Payload)
ResourceServer :: resource_server(), Payload :: map()) -> map().
extract_scopes_using_scope_aliases(
#resource_server{scope_aliases = ScopeAliasMapping}, Payload)
when is_map(ScopeAliasMapping) ->
Scopes0 = get_scope(Payload),
Scopes = rabbit_data_coercion:to_list_of_binaries(Scopes0),
%% for all scopes, look them up in the scope alias map, and if they are
%% present, add the alias to the final scope list. Note that we also preserve
%% the original scopes, it should not hurt.
ExpandedScopes =
lists:foldl(fun(ScopeListItem, Acc) ->
case maps:get(ScopeListItem, ScopeAliasMapping, undefined) of
undefined ->
Acc;
MappedList when is_list(MappedList) ->
Binaries = rabbit_data_coercion:to_list_of_binaries(MappedList),
Acc ++ Binaries;
Value ->
Binaries = rabbit_data_coercion:to_list_of_binaries(Value),
Acc ++ Binaries
end
end, Scopes, Scopes),
set_scope(ExpandedScopes, Payload);
extract_scopes_using_scope_aliases(_, Payload) -> Payload.

%% Path is a binary expression which is a plain word like <<"roles">>
%% or +1 word separated by . like <<"authorization.permissions.scopes">>
%% The Payload is a map.
%% Using the path <<"authorization.permissions.scopes">> as an example
%% 1. lookup the key <<"authorization">> in the Payload
%% 2. if it is found, the next map to use as payload is the value found from the key <<"authorization">>
%% 3. lookup the key <<"permissions">> in the previous map
%% 4. if it is found, it may be a map or a list of maps.
%% 5. if it is a list of maps, iterate each element in the list
%% 6. for each element in the list, which should be a map, find the key <<"scopes">>
%% 7. because there are no more words/keys, return a list of all the values found
%% associated to the word <<"scopes">>
extract_token_value(R, Payload, Path, ValueMapperFun)
when is_map(Payload), is_binary(Path), is_function(ValueMapperFun) ->
extract_token_value_from_map(R, Payload, [], split_path(Path), ValueMapperFun);
extract_token_value(_, _, _, _) ->
[].

extract_scope_list_from_token_value(_R, List) when is_list(List) -> List;
extract_scope_list_from_token_value(_R, Binary) when is_binary(Binary) ->
binary:split(Binary, <<" ">>, [global, trim_all]);
extract_scope_list_from_token_value(#resource_server{id = ResourceServerId}, Map) when is_map(Map) ->
case maps:get(ResourceServerId, Map, 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;
extract_scope_list_from_token_value(_, _) -> [].

extract_token_value_from_map(_, _Map, Acc, [], _Mapper) ->
Acc;
extract_token_value_from_map(R, Map, Acc, [KeyStr], Mapper) when is_map(Map) ->
case maps:find(KeyStr, Map) of
{ok, Value} -> Acc ++ Mapper(R, Value);
error -> Acc
end;
extract_token_value_from_map(R, Map, Acc, [KeyStr | Rest], Mapper) when is_map(Map) ->
case maps:find(KeyStr, Map) of
{ok, M} when is_map(M) -> extract_token_value_from_map(R, M, Acc, Rest, Mapper);
{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_list(_, [], Acc, [], _Mapper) ->
Acc;
extract_token_value_from_list(_, [], Acc, [_KeyStr | _Rest], _Mapper) ->
Acc;
extract_token_value_from_list(R, [H | T], Acc, [KeyStr | Rest] = KeyList, Mapper) when is_map(H) ->
NewAcc = case maps:find(KeyStr, H) of
{ok, Map} when is_map(Map) -> extract_token_value_from_map(R, Map, Acc, Rest, Mapper);
{ok, List} when is_list(List) -> extract_token_value_from_list(R, List, Acc, Rest, Mapper);
{ok, Value} -> Acc++Mapper(R, Value);
_ -> Acc
end,
extract_token_value_from_list(R, T, NewAcc, KeyList, Mapper);

extract_token_value_from_list(R, [E | T], Acc, [], Mapper) ->
extract_token_value_from_list(R, T, Acc++Mapper(R, E), [], Mapper);
extract_token_value_from_list(R, [E | _T] = L, Acc, KeyList, Mapper) when is_map(E) ->
extract_token_value_from_list(R, L, Acc, KeyList, Mapper);
extract_token_value_from_list(R, [_ | T], Acc, KeyList, Mapper) ->
extract_token_value_from_list(R, T, Acc, KeyList, Mapper).


split_path(Path) when is_binary(Path) ->
binary:split(Path, <<".">>, [global, trim_all]).


-spec extract_scopes_from_additional_scopes_key(
ResourceServer :: resource_server(), Payload :: map()) -> map().
extract_scopes_from_additional_scopes_key(ResourceServer, Payload) ->
Claim = maps:get(ResourceServer#resource_server.additional_scopes_key, Payload),
AdditionalScopes = extract_additional_scopes(ResourceServer, Claim),
set_scope(AdditionalScopes ++ get_scope(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.
extract_scopes_from_additional_scopes_key(
#resource_server{additional_scopes_key = Key} = ResourceServer, Payload)
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.


%% A token may be present in the password credential or in the rabbit_auth_backend_oauth2
Expand Down
41 changes: 0 additions & 41 deletions deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_keycloak.erl

This file was deleted.

12 changes: 5 additions & 7 deletions deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_rar.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
-include("oauth2.hrl").
-import(uaa_jwt, [get_scope/1, set_scope/2]).

-export([extract_scopes_from_rich_auth_request/2, has_rich_auth_request_scopes/1]).
-export([extract_scopes_from_rich_auth_request/2]).

-define(AUTHORIZATION_DETAILS_CLAIM, <<"authorization_details">>).
-define(RAR_ACTIONS_FIELD, <<"actions">>).
Expand Down Expand Up @@ -44,15 +44,12 @@
<<"management">>,
<<"policymaker">> ]).

-spec has_rich_auth_request_scopes(Payload::map()) -> boolean().
has_rich_auth_request_scopes(Payload) ->
maps:is_key(?AUTHORIZATION_DETAILS_CLAIM, Payload).

-spec extract_scopes_from_rich_auth_request(ResourceServer :: resource_server(),
Payload :: map()) -> map().
%% https://oauth.net/2/rich-authorization-requests/
extract_scopes_from_rich_auth_request(ResourceServer,
#{?AUTHORIZATION_DETAILS_CLAIM := Permissions} = Payload) ->
#{?AUTHORIZATION_DETAILS_CLAIM := Permissions} = Payload)
when is_list(Permissions) ->
ResourceServerType = ResourceServer#resource_server.resource_server_type,

FilteredPermissionsByType = lists:filter(fun(P) ->
Expand All @@ -61,7 +58,8 @@ extract_scopes_from_rich_auth_request(ResourceServer,
ResourceServer#resource_server.id, FilteredPermissionsByType),

ExistingScopes = get_scope(Payload),
set_scope(AdditionalScopes ++ ExistingScopes, Payload).
set_scope(AdditionalScopes ++ ExistingScopes, Payload);
extract_scopes_from_rich_auth_request(_, Payload) -> Payload.

put_location_attribute(Attribute, Map) ->
put_attribute(binary:split(Attribute, <<":">>, [global, trim_all]), Map).
Expand Down
Loading
Loading