Support keycloak custom format via configuration

This commit is contained in:
Marcial Rosales 2025-02-10 08:19:20 +01:00
parent 55ae918094
commit 1179d3a3ec
7 changed files with 290 additions and 117 deletions

View File

@ -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",
@ -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",
@ -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",

View File

@ -28,8 +28,7 @@
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]).
@ -229,79 +228,142 @@ 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,
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(Payload4), ResourceServer#resource_server.scope_prefix),
set_scope(FilteredScopes, Payload4).
get_scope(Payload1), ResourceServer#resource_server.scope_prefix),
set_scope(FilteredScopes, Payload1).
-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).
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_map(_, _, Acc, _, _Mapper) ->
Acc.
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 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)
end.
-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_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,
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,

View File

@ -1,41 +0,0 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(rabbit_oauth2_keycloak).
-include("oauth2.hrl").
-export([extract_scopes_from_keycloak_format/1, has_keycloak_scopes/1]).
-import(uaa_jwt, [get_scope/1, set_scope/2]).
-define(AUTHORIZATION_CLAIM, <<"authorization">>).
-define(PERMISSIONS_CLAIM, <<"permissions">>).
-define(SCOPES_CLAIM, <<"scopes">>).
-spec has_keycloak_scopes(Payload::map()) -> boolean().
has_keycloak_scopes(Payload) ->
maps:is_key(?AUTHORIZATION_CLAIM, Payload).
-spec extract_scopes_from_keycloak_format(Payload :: map()) -> map().
%% keycloak token format: https://github.com/rabbitmq/rabbitmq-auth-backend-oauth2/issues/36
extract_scopes_from_keycloak_format(#{?AUTHORIZATION_CLAIM := Authorization} = Payload) ->
AdditionalScopes = extract_scopes_from_keycloak_permissions([],
maps:get(?PERMISSIONS_CLAIM, Authorization, [])),
set_scope(AdditionalScopes ++ get_scope(Payload), Payload).
extract_scopes_from_keycloak_permissions(Acc, []) ->
Acc;
extract_scopes_from_keycloak_permissions(Acc, [H | T]) when is_map(H) ->
Scopes = case maps:get(?SCOPES_CLAIM, H, []) of
ScopesAsList when is_list(ScopesAsList) ->
ScopesAsList;
ScopesAsBinary when is_binary(ScopesAsBinary) ->
[ScopesAsBinary]
end,
extract_scopes_from_keycloak_permissions(Acc ++ Scopes, T);
extract_scopes_from_keycloak_permissions(Acc, [_ | T]) ->
extract_scopes_from_keycloak_permissions(Acc, T).

View File

@ -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">>).
@ -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) ->
@ -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).

View File

@ -96,6 +96,7 @@ parse_resource_pattern(Pattern, Permission) ->
-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) ->
PatternLength = byte_size(PrefixPattern),
lists:filtermap(
fun(ScopeEl) ->

View File

@ -316,5 +316,15 @@
}
]}
], []
},
{additional_scopes_key,
"auth_oauth2.resource_server_id = new_resource_server_id
auth_oauth2.additional_scopes_key = roles realm.roles",
[
{rabbitmq_auth_backend_oauth2, [
{resource_server_id,<<"new_resource_server_id">>},
{extra_scopes_source, <<"roles realm.roles">> }
]}
], []
}
].

View File

@ -17,13 +17,16 @@
user_login_authentication/2,
user_login_authorization/2,
normalize_token_scope/2,
check_vhost_access/3]).
check_vhost_access/3,
extract_token_value/4,
extract_scope_list_from_token_value/2]).
-import(rabbit_oauth2_resource_server, [
new_resource_server/1
]).
all() ->
[
test_extract_scope_from_path_expression,
filter_matching_scope_prefix_and_drop_it,
normalize_token_scopes_with_scope_prefix,
normalize_token_scope_from_space_separated_list_in_scope_claim,
@ -39,6 +42,7 @@ all() ->
test_token_expiration,
test_invalid_signature,
test_incorrect_kid,
normalize_token_scope_using_multiple_scopes_key,
normalize_token_scope_with_keycloak_scopes,
normalize_token_scope_with_rich_auth_request,
normalize_token_scope_with_rich_auth_request_using_regular_expression_with_cluster,
@ -46,6 +50,7 @@ all() ->
test_unsuccessful_access_with_a_token_that_uses_missing_scope_alias_in_extra_scope_source_field,
test_username_from,
{group, with_rabbitmq_node}
].
groups() ->
[
@ -116,6 +121,73 @@ end_per_group(_, Config) ->
-define(RESOURCE_SERVER_TYPE, <<"rabbitmq-type">>).
-define(DEFAULT_SCOPE_PREFIX, <<"rabbitmq.">>).
normalize_token_scope_using_multiple_scopes_key(_) ->
Pairs = [
%% common case
{
"keycloak format 1",
#{<<"authorization">> =>
#{<<"permissions">> =>
[#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>,
<<"rsname">> => <<"allvhost">>,
<<"scopes">> => [<<"rabbitmq-resource.read:*/*">>]},
#{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>,
<<"rsname">> => <<"vhost1">>,
<<"scopes">> => [<<"rabbitmq-resource.write:vhost1/*">>]},
#{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>,
<<"rsname">> => <<"Default Resource">>,
<<"scopes">> => [<<"unknown-resource.write:vhost1/*">>]}
]
}
},
[<<"read:*/*">>, <<"write:vhost1/*">>]
},
{
"keycloak format 2 using realm_access",
#{<<"realm_access">> =>
#{<<"roles">> => [<<"rabbitmq-resource.read:format2/*">>]}
},
[<<"read:format2/*">>]
},
{
"keycloak format 2 using resource_access",
#{<<"resource_access">> =>
#{<<"account">> => #{<<"roles">> => [<<"rabbitmq-resource.read:format2bis/*">>]} }
},
[<<"read:format2bis/*">>]
},
{
"both formats",
#{<<"authorization">> =>
#{<<"permissions">> =>
[#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>,
<<"rsname">> => <<"allvhost">>,
<<"scopes">> => [<<"rabbitmq-resource.read:*/*">>]},
#{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>,
<<"rsname">> => <<"vhost1">>,
<<"scopes">> => [<<"rabbitmq-resource.write:vhost1/*">>]},
#{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>,
<<"rsname">> => <<"Default Resource">>,
<<"scopes">> => [<<"unknown-resource.write:vhost1/*">>]}
]
},
<<"realm_access">> =>
#{<<"roles">> => [<<"rabbitmq-resource.read:format2/*">>]},
<<"resource_access">> =>
#{<<"account">> => #{<<"roles">> => [<<"rabbitmq-resource.read:format2bis/*">>]} }
},
[<<"read:*/*">>, <<"write:vhost1/*">>, <<"read:format2/*">>, <<"read:format2bis/*">>]
}
],
lists:foreach(fun({Case, Token0, ExpectedScope}) ->
ResourceServer0 = new_resource_server(<<"rabbitmq-resource">>),
ResourceServer = ResourceServer0#resource_server{
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)
end, Pairs).
normalize_token_scope_with_keycloak_scopes(_) ->
Pairs = [
@ -169,7 +241,10 @@ normalize_token_scope_with_keycloak_scopes(_) ->
],
lists:foreach(fun({Case, Authorization, ExpectedScope}) ->
ResourceServer = new_resource_server(<<"rabbitmq-resource">>),
ResourceServer0 = new_resource_server(<<"rabbitmq-resource">>),
ResourceServer = ResourceServer0#resource_server{
additional_scopes_key = <<"authorization.permissions.scopes">>
},
Token0 = #{<<"authorization">> => Authorization},
Token = normalize_token_scope(ResourceServer, Token0),
?assertEqual(ExpectedScope, uaa_jwt:get_scope(Token), Case)
@ -1286,6 +1361,77 @@ normalize_token_scope_without_scope_claim(_) ->
Token0 = #{ },
?assertEqual([], uaa_jwt:get_scope(normalize_token_scope(ResourceServer, Token0))).
test_extract_scope_from_path_expression(_) ->
M = fun rabbit_auth_backend_oauth2:extract_scope_list_from_token_value/2,
R = #resource_server{id = <<"rabbitmq">>},
[<<"role1">>] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => <<"role1">> }},
<<"auth.permission">>, M),
[<<"role1">>,<<"role2">>] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => [<<"role1">>,<<"role2">>] }},
<<"auth.permission">>, M),
[<<"role1">>,<<"role2">>] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => <<"role1 role2">> }},
<<"auth.permission">>, M),
[<<"rabbitmq.role1">>,<<"rabbitmq.role2">>] = extract_token_value(R,
#{ <<"auth">> => #{
<<"rabbitmq">> => [<<"role1">>,<<"role2">>]
}},
<<"auth">>, M),
[<<"rabbitmq.role1">>,<<"rabbitmq.role2">>] = extract_token_value(R,
#{ <<"auth">> => #{
<<"rabbitmq">> => <<"role1 role2">>
}},
<<"auth">>, M),
%% this is the old keycloak format
[<<"role1">>,<<"role2">>] = extract_token_value(R,
#{ <<"auth">> => #{
<<"permission">> => [
#{ <<"scopes">> => <<"role1">>},
#{ <<"scopes">> => <<"role2">>}
]
}},
<<"auth.permission.scopes">>, M),
[<<"role1">>,<<"role2">>] = extract_token_value(R,
#{ <<"auth">> => #{
<<"permission">> => [
#{ <<"scopes">> => [<<"role1">>]},
#{ <<"scopes">> => [<<"role2">>]}
]
}},
<<"auth.permission.scopes">>, M),
[<<"role1">>,<<"role2">>] = extract_token_value(R,
#{ <<"auth">> => [
#{ <<"permission">> => [
#{ <<"scopes">> => [<<"role1">>]}
]},
#{ <<"permission">> => [
#{ <<"scopes">> => [<<"role2">>]}
]}
]},
<<"auth.permission.scopes">>, M),
[<<"role1">>] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }},
<<"auth.permission">>, M),
[] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }},
<<"auth.permission2">>, M),
[] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }},
<<"auth2.permission">>, M),
[] = extract_token_value(R,
#{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }},
<<"auth.permission2">>, M).
%%
%% Helpers
%%