Skip to content

Commit fdaf16b

Browse files
Merge pull request #13216 from rabbitmq/issue-12324
Support keycloak custom format via configuration
2 parents 82c81c8 + 8d0609e commit fdaf16b

File tree

9 files changed

+326
-144
lines changed

9 files changed

+326
-144
lines changed

deps/rabbitmq_auth_backend_oauth2/app.bzl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ def all_beam_files(name = "all_beam_files"):
1313
"src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl",
1414
"src/rabbit_auth_backend_oauth2.erl",
1515
"src/rabbit_auth_backend_oauth2_app.erl",
16-
"src/rabbit_oauth2_keycloak.erl",
1716
"src/rabbit_oauth2_provider.erl",
1817
"src/rabbit_oauth2_rar.erl",
1918
"src/rabbit_oauth2_resource_server.erl",
@@ -51,7 +50,6 @@ def all_test_beam_files(name = "all_test_beam_files"):
5150
"src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl",
5251
"src/rabbit_auth_backend_oauth2.erl",
5352
"src/rabbit_auth_backend_oauth2_app.erl",
54-
"src/rabbit_oauth2_keycloak.erl",
5553
"src/rabbit_oauth2_provider.erl",
5654
"src/rabbit_oauth2_rar.erl",
5755
"src/rabbit_oauth2_resource_server.erl",
@@ -101,7 +99,6 @@ def all_srcs(name = "all_srcs"):
10199
"src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl",
102100
"src/rabbit_auth_backend_oauth2.erl",
103101
"src/rabbit_auth_backend_oauth2_app.erl",
104-
"src/rabbit_oauth2_keycloak.erl",
105102
"src/rabbit_oauth2_provider.erl",
106103
"src/rabbit_oauth2_rar.erl",
107104
"src/rabbit_oauth2_resource_server.erl",

deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222

2323
%% End of Key JWT fields
2424

25+
%% UMA claim-type returns a RPT which is a token
26+
%% where scopes are located under a map of list of objects which have
27+
%% the scopes in the "scopes" attribute
28+
%% Used by Keycloak, WSO2 and others.
29+
%% https://en.wikipedia.org/wiki/User-Managed_Access#cite_note-docs.wso2.com-19
30+
-define(SCOPES_LOCATION_IN_REQUESTING_PARTY_TOKEN, <<"authorization.permissions.scopes">>).
31+
32+
2533
-type raw_jwt_token() :: binary() | #{binary() => any()}.
2634
-type decoded_jwt_token() :: #{binary() => any()}.
2735

deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl

Lines changed: 140 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
get_scope/1, set_scope/2,
2929
resolve_resource_server/1]).
3030

31-
-import(rabbit_oauth2_keycloak, [has_keycloak_scopes/1, extract_scopes_from_keycloak_format/1]).
32-
-import(rabbit_oauth2_rar, [extract_scopes_from_rich_auth_request/2, has_rich_auth_request_scopes/1]).
31+
-import(rabbit_oauth2_rar, [extract_scopes_from_rich_auth_request/2]).
3332

34-
-import(rabbit_oauth2_scope, [filter_matching_scope_prefix_and_drop_it/2]).
33+
-import(rabbit_oauth2_scope, [
34+
filter_matching_scope_prefix/2,
35+
filter_matching_scope_prefix_and_drop_it/2]).
3536

3637
-ifdef(TEST).
3738
-compile(export_all).
@@ -229,98 +230,152 @@ check_token(Token, {ResourceServer, InternalOAuthProvider}) ->
229230
{false, _} -> {refused, signature_invalid}
230231
end.
231232

233+
extract_scopes_from_scope_claim(Payload) ->
234+
case maps:find(?SCOPE_JWT_FIELD, Payload) of
235+
{ok, Bin} when is_binary(Bin) ->
236+
maps:put(?SCOPE_JWT_FIELD,
237+
binary:split(Bin, <<" ">>, [global, trim_all]),
238+
Payload);
239+
_ -> Payload
240+
end.
241+
232242
-spec normalize_token_scope(
233243
ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map().
234244
normalize_token_scope(ResourceServer, Payload) ->
235-
Payload0 = maps:map(fun(K, V) ->
236-
case K of
237-
?SCOPE_JWT_FIELD when is_binary(V) ->
238-
binary:split(V, <<" ">>, [global, trim_all]);
239-
_ -> V
240-
end
241-
end, Payload),
242-
243-
Payload1 = case has_additional_scopes_key(ResourceServer, Payload0) of
244-
true -> extract_scopes_from_additional_scopes_key(ResourceServer, Payload0);
245-
false -> Payload0
246-
end,
247-
248-
Payload2 = case has_keycloak_scopes(Payload1) of
249-
true -> extract_scopes_from_keycloak_format(Payload1);
250-
false -> Payload1
251-
end,
252-
253-
Payload3 = case ResourceServer#resource_server.scope_aliases of
254-
undefined -> Payload2;
255-
ScopeAliases -> extract_scopes_using_scope_aliases(ScopeAliases, Payload2)
256-
end,
257-
258-
Payload4 = case has_rich_auth_request_scopes(Payload3) of
259-
true -> extract_scopes_from_rich_auth_request(ResourceServer, Payload3);
260-
false -> Payload3
261-
end,
262-
263-
FilteredScopes = filter_matching_scope_prefix_and_drop_it(
264-
get_scope(Payload4), ResourceServer#resource_server.scope_prefix),
265-
set_scope(FilteredScopes, Payload4).
266245

246+
filter_duplicates(
247+
filter_matching_scope_prefix(ResourceServer,
248+
extract_scopes_from_rich_auth_request(ResourceServer,
249+
extract_scopes_using_scope_aliases(ResourceServer,
250+
extract_scopes_from_additional_scopes_key(ResourceServer,
251+
extract_scopes_from_requesting_party_token(ResourceServer,
252+
extract_scopes_from_scope_claim(Payload))))))).
253+
254+
filter_duplicates(#{?SCOPE_JWT_FIELD := Scopes} = Payload) ->
255+
set_scope(lists:usort(Scopes), Payload);
256+
filter_duplicates(Payload) -> Payload.
257+
258+
-spec extract_scopes_from_requesting_party_token(
259+
ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map().
260+
extract_scopes_from_requesting_party_token(ResourceServer, Payload) ->
261+
Path = ?SCOPES_LOCATION_IN_REQUESTING_PARTY_TOKEN,
262+
case extract_token_value(ResourceServer, Payload, Path,
263+
fun extract_scope_list_from_token_value/2) of
264+
[] ->
265+
Payload;
266+
AdditionalScopes ->
267+
set_scope(lists:flatten(AdditionalScopes) ++ get_scope(Payload), Payload)
268+
end.
267269

268270
-spec extract_scopes_using_scope_aliases(
269-
ScopeAliasMapping :: map(), Payload :: map()) -> map().
270-
extract_scopes_using_scope_aliases(ScopeAliasMapping, Payload) ->
271-
Scopes0 = get_scope(Payload),
272-
Scopes = rabbit_data_coercion:to_list_of_binaries(Scopes0),
273-
%% for all scopes, look them up in the scope alias map, and if they are
274-
%% present, add the alias to the final scope list. Note that we also preserve
275-
%% the original scopes, it should not hurt.
276-
ExpandedScopes =
277-
lists:foldl(fun(ScopeListItem, Acc) ->
278-
case maps:get(ScopeListItem, ScopeAliasMapping, undefined) of
279-
undefined ->
280-
Acc;
281-
MappedList when is_list(MappedList) ->
282-
Binaries = rabbit_data_coercion:to_list_of_binaries(MappedList),
283-
Acc ++ Binaries;
284-
Value ->
285-
Binaries = rabbit_data_coercion:to_list_of_binaries(Value),
286-
Acc ++ Binaries
287-
end
288-
end, Scopes, Scopes),
289-
set_scope(ExpandedScopes, Payload).
290-
291-
-spec has_additional_scopes_key(
292-
ResourceServer :: resource_server(), Payload :: map()) -> boolean().
293-
has_additional_scopes_key(ResourceServer, Payload) when is_map(Payload) ->
294-
case ResourceServer#resource_server.additional_scopes_key of
295-
undefined -> false;
296-
ScopeKey -> maps:is_key(ScopeKey, Payload)
271+
ResourceServer :: resource_server(), Payload :: map()) -> map().
272+
extract_scopes_using_scope_aliases(
273+
#resource_server{scope_aliases = ScopeAliasMapping}, Payload)
274+
when is_map(ScopeAliasMapping) ->
275+
Scopes0 = get_scope(Payload),
276+
Scopes = rabbit_data_coercion:to_list_of_binaries(Scopes0),
277+
%% for all scopes, look them up in the scope alias map, and if they are
278+
%% present, add the alias to the final scope list. Note that we also preserve
279+
%% the original scopes, it should not hurt.
280+
ExpandedScopes =
281+
lists:foldl(fun(ScopeListItem, Acc) ->
282+
case maps:get(ScopeListItem, ScopeAliasMapping, undefined) of
283+
undefined ->
284+
Acc;
285+
MappedList when is_list(MappedList) ->
286+
Binaries = rabbit_data_coercion:to_list_of_binaries(MappedList),
287+
Acc ++ Binaries;
288+
Value ->
289+
Binaries = rabbit_data_coercion:to_list_of_binaries(Value),
290+
Acc ++ Binaries
291+
end
292+
end, Scopes, Scopes),
293+
set_scope(ExpandedScopes, Payload);
294+
extract_scopes_using_scope_aliases(_, Payload) -> Payload.
295+
296+
%% Path is a binary expression which is a plain word like <<"roles">>
297+
%% or +1 word separated by . like <<"authorization.permissions.scopes">>
298+
%% The Payload is a map.
299+
%% Using the path <<"authorization.permissions.scopes">> as an example
300+
%% 1. lookup the key <<"authorization">> in the Payload
301+
%% 2. if it is found, the next map to use as payload is the value found from the key <<"authorization">>
302+
%% 3. lookup the key <<"permissions">> in the previous map
303+
%% 4. if it is found, it may be a map or a list of maps.
304+
%% 5. if it is a list of maps, iterate each element in the list
305+
%% 6. for each element in the list, which should be a map, find the key <<"scopes">>
306+
%% 7. because there are no more words/keys, return a list of all the values found
307+
%% associated to the word <<"scopes">>
308+
extract_token_value(R, Payload, Path, ValueMapperFun)
309+
when is_map(Payload), is_binary(Path), is_function(ValueMapperFun) ->
310+
extract_token_value_from_map(R, Payload, [], split_path(Path), ValueMapperFun);
311+
extract_token_value(_, _, _, _) ->
312+
[].
313+
314+
extract_scope_list_from_token_value(_R, List) when is_list(List) -> List;
315+
extract_scope_list_from_token_value(_R, Binary) when is_binary(Binary) ->
316+
binary:split(Binary, <<" ">>, [global, trim_all]);
317+
extract_scope_list_from_token_value(#resource_server{id = ResourceServerId}, Map) when is_map(Map) ->
318+
case maps:get(ResourceServerId, Map, undefined) of
319+
undefined -> [];
320+
Ks when is_list(Ks) ->
321+
[erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- Ks];
322+
ClaimBin when is_binary(ClaimBin) ->
323+
UnprefixedClaims = binary:split(ClaimBin, <<" ">>, [global, trim_all]),
324+
[erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- UnprefixedClaims];
325+
_ -> []
326+
end;
327+
extract_scope_list_from_token_value(_, _) -> [].
328+
329+
extract_token_value_from_map(_, _Map, Acc, [], _Mapper) ->
330+
Acc;
331+
extract_token_value_from_map(R, Map, Acc, [KeyStr], Mapper) when is_map(Map) ->
332+
case maps:find(KeyStr, Map) of
333+
{ok, Value} -> Acc ++ Mapper(R, Value);
334+
error -> Acc
335+
end;
336+
extract_token_value_from_map(R, Map, Acc, [KeyStr | Rest], Mapper) when is_map(Map) ->
337+
case maps:find(KeyStr, Map) of
338+
{ok, M} when is_map(M) -> extract_token_value_from_map(R, M, Acc, Rest, Mapper);
339+
{ok, L} when is_list(L) -> extract_token_value_from_list(R, L, Acc, Rest, Mapper);
340+
{ok, Value} when Rest =:= [] -> Acc ++ Mapper(R, Value);
341+
_ -> Acc
297342
end.
298343

344+
extract_token_value_from_list(_, [], Acc, [], _Mapper) ->
345+
Acc;
346+
extract_token_value_from_list(_, [], Acc, [_KeyStr | _Rest], _Mapper) ->
347+
Acc;
348+
extract_token_value_from_list(R, [H | T], Acc, [KeyStr | Rest] = KeyList, Mapper) when is_map(H) ->
349+
NewAcc = case maps:find(KeyStr, H) of
350+
{ok, Map} when is_map(Map) -> extract_token_value_from_map(R, Map, Acc, Rest, Mapper);
351+
{ok, List} when is_list(List) -> extract_token_value_from_list(R, List, Acc, Rest, Mapper);
352+
{ok, Value} -> Acc++Mapper(R, Value);
353+
_ -> Acc
354+
end,
355+
extract_token_value_from_list(R, T, NewAcc, KeyList, Mapper);
356+
357+
extract_token_value_from_list(R, [E | T], Acc, [], Mapper) ->
358+
extract_token_value_from_list(R, T, Acc++Mapper(R, E), [], Mapper);
359+
extract_token_value_from_list(R, [E | _T] = L, Acc, KeyList, Mapper) when is_map(E) ->
360+
extract_token_value_from_list(R, L, Acc, KeyList, Mapper);
361+
extract_token_value_from_list(R, [_ | T], Acc, KeyList, Mapper) ->
362+
extract_token_value_from_list(R, T, Acc, KeyList, Mapper).
363+
364+
365+
split_path(Path) when is_binary(Path) ->
366+
binary:split(Path, <<".">>, [global, trim_all]).
367+
368+
299369
-spec extract_scopes_from_additional_scopes_key(
300370
ResourceServer :: resource_server(), Payload :: map()) -> map().
301-
extract_scopes_from_additional_scopes_key(ResourceServer, Payload) ->
302-
Claim = maps:get(ResourceServer#resource_server.additional_scopes_key, Payload),
303-
AdditionalScopes = extract_additional_scopes(ResourceServer, Claim),
304-
set_scope(AdditionalScopes ++ get_scope(Payload), Payload).
305-
306-
extract_additional_scopes(ResourceServer, ComplexClaim) ->
307-
ResourceServerId = ResourceServer#resource_server.id,
308-
case ComplexClaim of
309-
L when is_list(L) -> L;
310-
M when is_map(M) ->
311-
case maps:get(ResourceServerId, M, undefined) of
312-
undefined -> [];
313-
Ks when is_list(Ks) ->
314-
[erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- Ks];
315-
ClaimBin when is_binary(ClaimBin) ->
316-
UnprefixedClaims = binary:split(ClaimBin, <<" ">>, [global, trim_all]),
317-
[erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- UnprefixedClaims];
318-
_ -> []
319-
end;
320-
Bin when is_binary(Bin) ->
321-
binary:split(Bin, <<" ">>, [global, trim_all]);
322-
_ -> []
323-
end.
371+
extract_scopes_from_additional_scopes_key(
372+
#resource_server{additional_scopes_key = Key} = ResourceServer, Payload)
373+
when is_binary(Key) ->
374+
Paths = binary:split(Key, <<" ">>, [global, trim_all]),
375+
AdditionalScopes = [ extract_token_value(ResourceServer,
376+
Payload, Path, fun extract_scope_list_from_token_value/2) || Path <- Paths],
377+
set_scope(lists:flatten(AdditionalScopes) ++ get_scope(Payload), Payload);
378+
extract_scopes_from_additional_scopes_key(_, Payload) -> Payload.
324379

325380

326381
%% A token may be present in the password credential or in the rabbit_auth_backend_oauth2

deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_keycloak.erl

Lines changed: 0 additions & 41 deletions
This file was deleted.

deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_rar.erl

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
-include("oauth2.hrl").
1212
-import(uaa_jwt, [get_scope/1, set_scope/2]).
1313

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

1616
-define(AUTHORIZATION_DETAILS_CLAIM, <<"authorization_details">>).
1717
-define(RAR_ACTIONS_FIELD, <<"actions">>).
@@ -44,15 +44,12 @@
4444
<<"management">>,
4545
<<"policymaker">> ]).
4646

47-
-spec has_rich_auth_request_scopes(Payload::map()) -> boolean().
48-
has_rich_auth_request_scopes(Payload) ->
49-
maps:is_key(?AUTHORIZATION_DETAILS_CLAIM, Payload).
50-
5147
-spec extract_scopes_from_rich_auth_request(ResourceServer :: resource_server(),
5248
Payload :: map()) -> map().
5349
%% https://oauth.net/2/rich-authorization-requests/
5450
extract_scopes_from_rich_auth_request(ResourceServer,
55-
#{?AUTHORIZATION_DETAILS_CLAIM := Permissions} = Payload) ->
51+
#{?AUTHORIZATION_DETAILS_CLAIM := Permissions} = Payload)
52+
when is_list(Permissions) ->
5653
ResourceServerType = ResourceServer#resource_server.resource_server_type,
5754

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

6360
ExistingScopes = get_scope(Payload),
64-
set_scope(AdditionalScopes ++ ExistingScopes, Payload).
61+
set_scope(AdditionalScopes ++ ExistingScopes, Payload);
62+
extract_scopes_from_rich_auth_request(_, Payload) -> Payload.
6563

6664
put_location_attribute(Attribute, Map) ->
6765
put_attribute(binary:split(Attribute, <<":">>, [global, trim_all]), Map).

0 commit comments

Comments
 (0)