summaryrefslogtreecommitdiff
path: root/deps/rabbitmq_auth_backend_oauth2/src
diff options
context:
space:
mode:
Diffstat (limited to 'deps/rabbitmq_auth_backend_oauth2/src')
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl143
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl318
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2_app.erl26
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl90
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl122
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwk.erl84
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwt.erl48
-rw-r--r--deps/rabbitmq_auth_backend_oauth2/src/wildcard.erl58
8 files changed, 889 insertions, 0 deletions
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl b/deps/rabbitmq_auth_backend_oauth2/src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl
new file mode 100644
index 0000000000..6571ba9c59
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl
@@ -0,0 +1,143 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+-module('Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand').
+
+-behaviour('Elixir.RabbitMQ.CLI.CommandBehaviour').
+
+-export([
+ usage/0,
+ validate/2,
+ merge_defaults/2,
+ banner/2,
+ run/2,
+ switches/0,
+ aliases/0,
+ output/2,
+ formatter/0
+ ]).
+
+
+usage() ->
+ <<"add_uaa_key <name> [--json=<json_key>] [--pem=<public_key>] [--pem_file=<pem_file>]">>.
+
+switches() ->
+ [{json, string},
+ {pem, string},
+ {pem_file, string}].
+
+aliases() -> [].
+
+validate([], _Options) -> {validation_failure, not_enough_args};
+validate([_,_|_], _Options) -> {validation_failure, too_many_args};
+validate([_], Options) ->
+ Json = maps:get(json, Options, undefined),
+ Pem = maps:get(pem, Options, undefined),
+ PemFile = maps:get(pem_file, Options, undefined),
+ case {is_binary(Json), is_binary(Pem), is_binary(PemFile)} of
+ {false, false, false} ->
+ {validation_failure,
+ {bad_argument, <<"No key specified">>}};
+ {true, false, false} ->
+ validate_json(Json);
+ {false, true, false} ->
+ validate_pem(Pem);
+ {false, false, true} ->
+ validate_pem_file(PemFile);
+ {_, _, _} ->
+ {validation_failure,
+ {bad_argument, <<"There can be only one key type">>}}
+ end.
+
+validate_json(Json) ->
+ case rabbit_json:try_decode(Json) of
+ {ok, _} ->
+ case uaa_jwt:verify_signing_key(json, Json) of
+ ok -> ok;
+ {error, {fields_missing_for_kty, Kty}} ->
+ {validation_failure,
+ {bad_argument,
+ <<"Key fields are missing fot kty \"", Kty/binary, "\"">>}};
+ {error, unknown_kty} ->
+ {validation_failure,
+ {bad_argument, <<"\"kty\" field is invalid">>}};
+ {error, no_kty} ->
+ {validation_failure,
+ {bad_argument, <<"Json key should contain \"kty\" field">>}};
+ {error, Err} ->
+ {validation_failure, {bad_argument, Err}}
+ end;
+ {error, _} ->
+ {validation_failure, {bad_argument, <<"Invalid JSON">>}};
+ error ->
+ {validation_failure, {bad_argument, <<"Invalid JSON">>}}
+ end.
+
+validate_pem(Pem) ->
+ case uaa_jwt:verify_signing_key(pem, Pem) of
+ ok -> ok;
+ {error, invalid_pem_string} ->
+ {validation_failure, <<"Unable to read a key from the PEM string">>};
+ {error, Err} ->
+ {validation_failure, Err}
+ end.
+
+validate_pem_file(PemFile) ->
+ case uaa_jwt:verify_signing_key(pem_file, PemFile) of
+ ok -> ok;
+ {error, enoent} ->
+ {validation_failure, {bad_argument, <<"PEM file not found">>}};
+ {error, invalid_pem_file} ->
+ {validation_failure, <<"Unable to read a key from the PEM file">>};
+ {error, Err} ->
+ {validation_failure, Err}
+ end.
+
+merge_defaults(Args, #{pem_file := FileName} = Options) ->
+ AbsFileName = filename:absname(FileName),
+ {Args, Options#{pem_file := AbsFileName}};
+merge_defaults(Args, Options) -> {Args, Options}.
+
+banner([Name], #{json := Json}) ->
+ <<"Adding UAA signing key \"",
+ Name/binary,
+ "\" in JSON format: \"",
+ Json/binary, "\"">>;
+banner([Name], #{pem := Pem}) ->
+ <<"Adding UAA signing key \"",
+ Name/binary,
+ "\" public key: \"",
+ Pem/binary, "\"">>;
+banner([Name], #{pem_file := PemFile}) ->
+ <<"Adding UAA signing key \"",
+ Name/binary,
+ "\" filename: \"",
+ PemFile/binary, "\"">>.
+
+run([Name], #{node := Node} = Options) ->
+ {Type, Value} = case Options of
+ #{json := Json} -> {json, Json};
+ #{pem := Pem} -> {pem, Pem};
+ #{pem_file := PemFile} -> {pem_file, PemFile}
+ end,
+ case rabbit_misc:rpc_call(Node,
+ uaa_jwt, add_signing_key,
+ [Name, Type, Value]) of
+ {ok, _Keys} -> ok;
+ {error, Err} -> {error, Err}
+ end.
+
+output(E, _Opts) ->
+ 'Elixir.RabbitMQ.CLI.DefaultOutput':output(E).
+
+formatter() -> 'Elixir.RabbitMQ.CLI.Formatters.Erlang'.
+
+
+
+
+
+
+
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl
new file mode 100644
index 0000000000..e1a99ab7ea
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl
@@ -0,0 +1,318 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_auth_backend_oauth2).
+
+-include_lib("rabbit_common/include/rabbit.hrl").
+
+-behaviour(rabbit_authn_backend).
+-behaviour(rabbit_authz_backend).
+
+-export([description/0]).
+-export([user_login_authentication/2, user_login_authorization/2,
+ check_vhost_access/3, check_resource_access/4,
+ check_topic_access/4, check_token/1, state_can_expire/0, update_state/2]).
+
+% for testing
+-export([post_process_payload/1]).
+
+-import(rabbit_data_coercion, [to_map/1]).
+
+-ifdef(TEST).
+-compile(export_all).
+-endif.
+%%--------------------------------------------------------------------
+
+-define(APP, rabbitmq_auth_backend_oauth2).
+-define(RESOURCE_SERVER_ID, resource_server_id).
+%% a term used by the IdentityServer community
+-define(COMPLEX_CLAIM, extra_scopes_source).
+
+description() ->
+ [{name, <<"OAuth 2">>},
+ {description, <<"Performs authentication and authorisation using JWT tokens and OAuth 2 scopes">>}].
+
+%%--------------------------------------------------------------------
+
+user_login_authentication(Username, AuthProps) ->
+ case authenticate(Username, AuthProps) of
+ {refused, Msg, Args} = AuthResult ->
+ rabbit_log:debug(Msg ++ "~n", Args),
+ AuthResult;
+ _ = AuthResult ->
+ AuthResult
+ end.
+
+user_login_authorization(Username, AuthProps) ->
+ case authenticate(Username, AuthProps) of
+ {ok, #auth_user{impl = Impl}} -> {ok, Impl};
+ Else -> Else
+ end.
+
+check_vhost_access(#auth_user{impl = DecodedToken},
+ VHost, _AuthzData) ->
+ with_decoded_token(DecodedToken,
+ fun() ->
+ Scopes = get_scopes(DecodedToken),
+ ScopeString = rabbit_oauth2_scope:concat_scopes(Scopes, ","),
+ rabbit_log:debug("Matching virtual host '~s' against the following scopes: ~s", [VHost, ScopeString]),
+ rabbit_oauth2_scope:vhost_access(VHost, Scopes)
+ end).
+
+check_resource_access(#auth_user{impl = DecodedToken},
+ Resource, Permission, _AuthzContext) ->
+ with_decoded_token(DecodedToken,
+ fun() ->
+ Scopes = get_scopes(DecodedToken),
+ rabbit_oauth2_scope:resource_access(Resource, Permission, Scopes)
+ end).
+
+check_topic_access(#auth_user{impl = DecodedToken},
+ Resource, Permission, Context) ->
+ with_decoded_token(DecodedToken,
+ fun() ->
+ Scopes = get_scopes(DecodedToken),
+ rabbit_oauth2_scope:topic_access(Resource, Permission, Context, Scopes)
+ end).
+
+state_can_expire() -> true.
+
+update_state(AuthUser, NewToken) ->
+ case check_token(NewToken) of
+ %% avoid logging the token
+ {error, _} = E -> E;
+ {refused, {error, {invalid_token, error, _Err, _Stacktrace}}} ->
+ {refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid"};
+ {refused, Err} ->
+ {refused, rabbit_misc:format("Authentication using an OAuth 2/JWT token failed: ~p", [Err])};
+ {ok, DecodedToken} ->
+ Tags = tags_from(DecodedToken),
+
+ {ok, AuthUser#auth_user{tags = Tags,
+ impl = DecodedToken}}
+ end.
+
+%%--------------------------------------------------------------------
+
+authenticate(Username0, AuthProps0) ->
+ AuthProps = to_map(AuthProps0),
+ Token = token_from_context(AuthProps),
+ case check_token(Token) of
+ %% avoid logging the token
+ {error, _} = E -> E;
+ {refused, {error, {invalid_token, error, _Err, _Stacktrace}}} ->
+ {refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid", []};
+ {refused, Err} ->
+ {refused, "Authentication using an OAuth 2/JWT token failed: ~p", [Err]};
+ {ok, DecodedToken} ->
+ Func = fun() ->
+ Username = username_from(Username0, DecodedToken),
+ Tags = tags_from(DecodedToken),
+
+ {ok, #auth_user{username = Username,
+ tags = Tags,
+ impl = DecodedToken}}
+ end,
+ case with_decoded_token(DecodedToken, Func) of
+ {error, Err} ->
+ {refused, "Authentication using an OAuth 2/JWT token failed: ~p", [Err]};
+ Else ->
+ Else
+ end
+ end.
+
+with_decoded_token(DecodedToken, Fun) ->
+ case validate_token_expiry(DecodedToken) of
+ ok -> Fun();
+ {error, Msg} = Err ->
+ rabbit_log:error(Msg),
+ Err
+ end.
+
+validate_token_expiry(#{<<"exp">> := Exp}) when is_integer(Exp) ->
+ Now = os:system_time(seconds),
+ case Exp =< Now of
+ true -> {error, rabbit_misc:format("Provided JWT token has expired at timestamp ~p (validated at ~p)", [Exp, Now])};
+ false -> ok
+ end;
+validate_token_expiry(#{}) -> ok.
+
+-spec check_token(binary()) -> {ok, map()} | {error, term()}.
+check_token(Token) ->
+ case uaa_jwt:decode_and_verify(Token) of
+ {error, Reason} -> {refused, {error, Reason}};
+ {true, Payload} -> validate_payload(post_process_payload(Payload));
+ {false, _} -> {refused, signature_invalid}
+ end.
+
+post_process_payload(Payload) when is_map(Payload) ->
+ Payload0 = maps:map(fun(K, V) ->
+ case K of
+ <<"aud">> when is_binary(V) -> binary:split(V, <<" ">>, [global, trim_all]);
+ <<"scope">> when is_binary(V) -> binary:split(V, <<" ">>, [global, trim_all]);
+ _ -> V
+ end
+ end,
+ Payload
+ ),
+ Payload1 = case does_include_complex_claim_field(Payload0) of
+ true -> post_process_payload_complex_claim(Payload0);
+ false -> Payload0
+ end,
+
+ Payload2 = case maps:is_key(<<"authorization">>, Payload1) of
+ true -> post_process_payload_keycloak(Payload1);
+ false -> Payload1
+ end,
+
+ Payload2.
+
+does_include_complex_claim_field(Payload) when is_map(Payload) ->
+ maps:is_key(application:get_env(?APP, ?COMPLEX_CLAIM, undefined), Payload).
+
+post_process_payload_complex_claim(Payload) ->
+ ComplexClaim = maps:get(application:get_env(?APP, ?COMPLEX_CLAIM, undefined), Payload),
+ ResourceServerId = rabbit_data_coercion:to_binary(application:get_env(?APP, ?RESOURCE_SERVER_ID, <<>>)),
+
+ AdditionalScopes =
+ 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,
+
+ case AdditionalScopes of
+ [] -> Payload;
+ _ ->
+ ExistingScopes = maps:get(<<"scope">>, Payload, []),
+ maps:put(<<"scope">>, AdditionalScopes ++ ExistingScopes, Payload)
+ end.
+
+%% keycloak token format: https://github.com/rabbitmq/rabbitmq-auth-backend-oauth2/issues/36
+post_process_payload_keycloak(#{<<"authorization">> := Authorization} = Payload) ->
+ AdditionalScopes = case maps:get(<<"permissions">>, Authorization, undefined) of
+ undefined -> [];
+ Permissions -> extract_scopes_from_keycloak_permissions([], Permissions)
+ end,
+ ExistingScopes = maps:get(<<"scope">>, Payload),
+ maps:put(<<"scope">>, AdditionalScopes ++ ExistingScopes, 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">>, 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).
+
+validate_payload(#{<<"scope">> := _Scope, <<"aud">> := _Aud} = DecodedToken) ->
+ ResourceServerEnv = application:get_env(?APP, ?RESOURCE_SERVER_ID, <<>>),
+ ResourceServerId = rabbit_data_coercion:to_binary(ResourceServerEnv),
+ validate_payload(DecodedToken, ResourceServerId).
+
+validate_payload(#{<<"scope">> := Scope, <<"aud">> := Aud} = DecodedToken, ResourceServerId) ->
+ case check_aud(Aud, ResourceServerId) of
+ ok -> {ok, DecodedToken#{<<"scope">> => filter_scopes(Scope, ResourceServerId)}};
+ {error, Err} -> {refused, {invalid_aud, Err}}
+ end.
+
+filter_scopes(Scopes, <<"">>) -> Scopes;
+filter_scopes(Scopes, ResourceServerId) ->
+ PrefixPattern = <<ResourceServerId/binary, ".">>,
+ matching_scopes_without_prefix(Scopes, PrefixPattern).
+
+check_aud(_, <<>>) -> ok;
+check_aud(Aud, ResourceServerId) ->
+ case Aud of
+ List when is_list(List) ->
+ case lists:member(ResourceServerId, Aud) of
+ true -> ok;
+ false -> {error, {resource_id_not_found_in_aud, ResourceServerId, Aud}}
+ end;
+ _ -> {error, {badarg, {aud_is_not_a_list, Aud}}}
+ end.
+
+%%--------------------------------------------------------------------
+
+get_scopes(#{<<"scope">> := Scope}) -> Scope.
+
+-spec token_from_context(map()) -> binary() | undefined.
+token_from_context(AuthProps) ->
+ maps:get(password, AuthProps, undefined).
+
+%% Decoded tokens look like this:
+%%
+%% #{<<"aud">> => [<<"rabbitmq">>, <<"rabbit_client">>],
+%% <<"authorities">> => [<<"rabbitmq.read:*/*">>, <<"rabbitmq.write:*/*">>, <<"rabbitmq.configure:*/*">>],
+%% <<"azp">> => <<"rabbit_client">>,
+%% <<"cid">> => <<"rabbit_client">>,
+%% <<"client_id">> => <<"rabbit_client">>,
+%% <<"exp">> => 1530849387,
+%% <<"grant_type">> => <<"client_credentials">>,
+%% <<"iat">> => 1530806187,
+%% <<"iss">> => <<"http://localhost:8080/uaa/oauth/token">>,
+%% <<"jti">> => <<"df5d50a1cdcb4fa6bf32e7e03acfc74d">>,
+%% <<"rev_sig">> => <<"2f880d5b">>,
+%% <<"scope">> => [<<"rabbitmq.read:*/*">>, <<"rabbitmq.write:*/*">>, <<"rabbitmq.configure:*/*">>],
+%% <<"sub">> => <<"rabbit_client">>,
+%% <<"zid">> => <<"uaa">>}
+
+-spec username_from(binary(), map()) -> binary() | undefined.
+username_from(ClientProvidedUsername, DecodedToken) ->
+ ClientId = uaa_jwt:client_id(DecodedToken, undefined),
+ Sub = uaa_jwt:sub(DecodedToken, undefined),
+
+ rabbit_log:debug("Computing username from client's JWT token, client ID: '~s', sub: '~s'",
+ [ClientId, Sub]),
+
+ case uaa_jwt:client_id(DecodedToken, Sub) of
+ undefined ->
+ case ClientProvidedUsername of
+ undefined -> undefined;
+ <<>> -> undefined;
+ _Other -> ClientProvidedUsername
+ end;
+ Value ->
+ Value
+ end.
+
+-spec tags_from(map()) -> list(atom()).
+tags_from(DecodedToken) ->
+ Scopes = maps:get(<<"scope">>, DecodedToken, []),
+ TagScopes = matching_scopes_without_prefix(Scopes, <<"tag:">>),
+ lists:usort(lists:map(fun rabbit_data_coercion:to_atom/1, TagScopes)).
+
+matching_scopes_without_prefix(Scopes, PrefixPattern) ->
+ PatternLength = byte_size(PrefixPattern),
+ lists:filtermap(
+ fun(ScopeEl) ->
+ case binary:match(ScopeEl, PrefixPattern) of
+ {0, PatternLength} ->
+ ElLength = byte_size(ScopeEl),
+ {true,
+ binary:part(ScopeEl,
+ {PatternLength, ElLength - PatternLength})};
+ _ -> false
+ end
+ end,
+ Scopes).
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2_app.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2_app.erl
new file mode 100644
index 0000000000..e2b1d23131
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2_app.erl
@@ -0,0 +1,26 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_auth_backend_oauth2_app).
+
+-behaviour(application).
+-export([start/2, stop/1]).
+
+-behaviour(supervisor).
+-export([init/1]).
+
+start(_Type, _StartArgs) ->
+ supervisor:start_link({local,?MODULE},?MODULE,[]).
+
+stop(_State) ->
+ ok.
+
+%%----------------------------------------------------------------------------
+
+init([]) ->
+ {ok, {{one_for_one,3,10},[]}}.
+
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl
new file mode 100644
index 0000000000..2ebf6c3c52
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl
@@ -0,0 +1,90 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_oauth2_scope).
+
+-export([vhost_access/2, resource_access/3, topic_access/4, concat_scopes/2]).
+
+-include_lib("rabbit_common/include/rabbit.hrl").
+
+-type permission() :: read | write | configure.
+
+%% API functions --------------------------------------------------------------
+-spec vhost_access(binary(), [binary()]) -> boolean().
+vhost_access(VHost, Scopes) ->
+ PermissionScopes = get_scope_permissions(Scopes),
+ lists:any(
+ fun({VHostPattern, _, _, _}) ->
+ wildcard:match(VHost, VHostPattern)
+ end,
+ PermissionScopes).
+
+-spec resource_access(rabbit_types:r(atom()), permission(), [binary()]) -> boolean().
+resource_access(#resource{virtual_host = VHost, name = Name},
+ Permission, Scopes) ->
+ lists:any(
+ fun({VHostPattern, NamePattern, _, ScopeGrantedPermission}) ->
+ wildcard:match(VHost, VHostPattern) andalso
+ wildcard:match(Name, NamePattern) andalso
+ Permission =:= ScopeGrantedPermission
+ end,
+ get_scope_permissions(Scopes)).
+
+topic_access(#resource{virtual_host = VHost, name = ExchangeName},
+ Permission,
+ #{routing_key := RoutingKey},
+ Scopes) ->
+ lists:any(
+ fun({VHostPattern, ExchangeNamePattern, RoutingKeyPattern, ScopeGrantedPermission}) ->
+ is_binary(RoutingKeyPattern) andalso
+ wildcard:match(VHost, VHostPattern) andalso
+ wildcard:match(ExchangeName, ExchangeNamePattern) andalso
+ wildcard:match(RoutingKey, RoutingKeyPattern) andalso
+ Permission =:= ScopeGrantedPermission
+ end,
+ get_scope_permissions(Scopes)).
+
+%% Internal -------------------------------------------------------------------
+
+-spec get_scope_permissions([binary()]) -> [{rabbit_types:r(pattern), permission()}].
+get_scope_permissions(Scopes) when is_list(Scopes) ->
+ lists:filtermap(
+ fun(ScopeEl) ->
+ case parse_permission_pattern(ScopeEl) of
+ ignore -> false;
+ Perm -> {true, Perm}
+ end
+ end,
+ Scopes).
+
+-spec concat_scopes([binary()], string()) -> string().
+concat_scopes(Scopes, Separator) when is_list(Scopes) ->
+ lists:concat(lists:join(Separator, lists:map(fun rabbit_data_coercion:to_list/1, Scopes))).
+
+-spec parse_permission_pattern(binary()) -> {rabbit_types:r(pattern), permission()}.
+parse_permission_pattern(<<"read:", ResourcePatternBin/binary>>) ->
+ Permission = read,
+ parse_resource_pattern(ResourcePatternBin, Permission);
+parse_permission_pattern(<<"write:", ResourcePatternBin/binary>>) ->
+ Permission = write,
+ parse_resource_pattern(ResourcePatternBin, Permission);
+parse_permission_pattern(<<"configure:", ResourcePatternBin/binary>>) ->
+ Permission = configure,
+ parse_resource_pattern(ResourcePatternBin, Permission);
+parse_permission_pattern(_Other) ->
+ ignore.
+
+-spec parse_resource_pattern(binary(), permission()) ->
+ {rabbit_types:vhost(), binary(), binary() | none, permission()}.
+parse_resource_pattern(Pattern, Permission) ->
+ case binary:split(Pattern, <<"/">>, [global]) of
+ [VhostPattern, NamePattern] ->
+ {VhostPattern, NamePattern, none, Permission};
+ [VhostPattern, NamePattern, RoutingKeyPattern] ->
+ {VhostPattern, NamePattern, RoutingKeyPattern, Permission};
+ _Other -> ignore
+ end.
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl
new file mode 100644
index 0000000000..e8d59f5670
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl
@@ -0,0 +1,122 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+-module(uaa_jwt).
+
+-export([add_signing_key/3,
+ remove_signing_key/1,
+ decode_and_verify/1,
+ get_jwk/1,
+ verify_signing_key/2,
+ signing_keys/0]).
+
+-export([client_id/1, sub/1, client_id/2, sub/2]).
+
+-include_lib("jose/include/jose_jwk.hrl").
+
+-define(APP, rabbitmq_auth_backend_oauth2).
+
+-type key_type() :: json | pem | map.
+
+-spec add_signing_key(binary(), key_type(), binary() | map()) -> {ok, map()} | {error, term()}.
+
+add_signing_key(KeyId, Type, Value) ->
+ case verify_signing_key(Type, Value) of
+ ok ->
+ SigningKeys0 = signing_keys(),
+ SigningKeys1 = maps:put(KeyId, {Type, Value}, SigningKeys0),
+ ok = update_uaa_jwt_signing_keys(SigningKeys1),
+ {ok, SigningKeys1};
+ {error, _} = Err ->
+ Err
+ end.
+
+remove_signing_key(KeyId) ->
+ UaaEnv = application:get_env(?APP, key_config, []),
+ Keys0 = proplists:get_value(signing_keys, UaaEnv),
+ Keys1 = maps:remove(KeyId, Keys0),
+ update_uaa_jwt_signing_keys(UaaEnv, Keys1).
+
+-spec update_uaa_jwt_signing_keys(map()) -> ok.
+update_uaa_jwt_signing_keys(SigningKeys) ->
+ UaaEnv0 = application:get_env(?APP, key_config, []),
+ update_uaa_jwt_signing_keys(UaaEnv0, SigningKeys).
+
+-spec update_uaa_jwt_signing_keys([term()], map()) -> ok.
+update_uaa_jwt_signing_keys(UaaEnv0, SigningKeys) ->
+ UaaEnv1 = proplists:delete(signing_keys, UaaEnv0),
+ UaaEnv2 = [{signing_keys, SigningKeys} | UaaEnv1],
+ application:set_env(?APP, key_config, UaaEnv2).
+
+-spec decode_and_verify(binary()) -> {boolean(), map()} | {error, term()}.
+decode_and_verify(Token) ->
+ case uaa_jwt_jwt:get_key_id(Token) of
+ {ok, KeyId} ->
+ case get_jwk(KeyId) of
+ {ok, JWK} ->
+ uaa_jwt_jwt:decode_and_verify(JWK, Token);
+ {error, _} = Err ->
+ Err
+ end;
+ {error, _} = Err ->
+ Err
+ end.
+
+-spec get_jwk(binary()) -> {ok, map()} | {error, term()}.
+get_jwk(KeyId) ->
+ case signing_keys() of
+ undefined -> {error, signing_keys_not_configured};
+ Keys ->
+ case maps:get(KeyId, Keys, undefined) of
+ undefined ->
+ {error, key_not_found};
+ {Type, Value} ->
+ case Type of
+ json -> uaa_jwt_jwk:make_jwk(Value);
+ pem -> uaa_jwt_jwk:from_pem(Value);
+ pem_file -> uaa_jwt_jwk:from_pem_file(Value);
+ map -> uaa_jwt_jwk:make_jwk(Value);
+ _ -> {error, unknown_signing_key_type}
+ end
+ end
+ end.
+
+verify_signing_key(Type, Value) ->
+ Verified = case Type of
+ json -> uaa_jwt_jwk:make_jwk(Value);
+ pem -> uaa_jwt_jwk:from_pem(Value);
+ pem_file -> uaa_jwt_jwk:from_pem_file(Value);
+ map -> uaa_jwt_jwk:make_jwk(Value);
+ _ -> {error, unknown_signing_key_type}
+ end,
+ case Verified of
+ {ok, Key} ->
+ case jose_jwk:from(Key) of
+ #jose_jwk{} -> ok;
+ {error, Reason} -> {error, Reason}
+ end;
+ Err -> Err
+ end.
+
+signing_keys() ->
+ UaaEnv = application:get_env(?APP, key_config, []),
+ proplists:get_value(signing_keys, UaaEnv).
+
+-spec client_id(map()) -> binary() | undefined.
+client_id(DecodedToken) ->
+ maps:get(<<"client_id">>, DecodedToken, undefined).
+
+-spec client_id(map(), any()) -> binary() | undefined.
+client_id(DecodedToken, Default) ->
+ maps:get(<<"client_id">>, DecodedToken, Default).
+
+-spec sub(map()) -> binary() | undefined.
+sub(DecodedToken) ->
+ maps:get(<<"sub">>, DecodedToken, undefined).
+
+-spec sub(map(), any()) -> binary() | undefined.
+sub(DecodedToken, Default) ->
+ maps:get(<<"sub">>, DecodedToken, Default).
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwk.erl b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwk.erl
new file mode 100644
index 0000000000..11d2819fb5
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwk.erl
@@ -0,0 +1,84 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+-module(uaa_jwt_jwk).
+
+-export([make_jwk/1, from_pem/1, from_pem_file/1]).
+
+-include_lib("jose/include/jose_jwk.hrl").
+
+-spec make_jwk(binary() | map()) -> {ok, #{binary() => binary()}} | {error, term()}.
+make_jwk(Json) when is_binary(Json); is_list(Json) ->
+ JsonMap = jose:decode(iolist_to_binary(Json)),
+ make_jwk(JsonMap);
+
+make_jwk(JsonMap) when is_map(JsonMap) ->
+ case JsonMap of
+ #{<<"kty">> := <<"MAC">>, <<"value">> := _Value} ->
+ {ok, mac_to_oct(JsonMap)};
+ #{<<"kty">> := <<"RSA">>, <<"n">> := _N, <<"e">> := _E} ->
+ {ok, fix_alg(JsonMap)};
+ #{<<"kty">> := <<"oct">>, <<"k">> := _K} ->
+ {ok, fix_alg(JsonMap)};
+ #{<<"kty">> := <<"OKP">>, <<"crv">> := _Crv, <<"x">> := _X} ->
+ {ok, fix_alg(JsonMap)};
+ #{<<"kty">> := <<"EC">>} ->
+ {ok, fix_alg(JsonMap)};
+ #{<<"kty">> := Kty} when Kty == <<"oct">>;
+ Kty == <<"MAC">>;
+ Kty == <<"RSA">>;
+ Kty == <<"OKP">>;
+ Kty == <<"EC">> ->
+ {error, {fields_missing_for_kty, Kty}};
+ #{<<"kty">> := _Kty} ->
+ {error, unknown_kty};
+ #{} ->
+ {error, no_kty}
+ end.
+
+from_pem(Pem) ->
+ case jose_jwk:from_pem(Pem) of
+ #jose_jwk{} = Jwk -> {ok, Jwk};
+ Other ->
+ error_logger:warning_msg("Error parsing jwk from pem: ", [Other]),
+ {error, invalid_pem_string}
+ end.
+
+from_pem_file(FileName) ->
+ case filelib:is_file(FileName) of
+ false ->
+ {error, enoent};
+ true ->
+ case jose_jwk:from_pem_file(FileName) of
+ #jose_jwk{} = Jwk -> {ok, Jwk};
+ Other ->
+ error_logger:warning_msg("Error parsing jwk from pem file: ", [Other]),
+ {error, invalid_pem_file}
+ end
+ end.
+
+mac_to_oct(#{<<"kty">> := <<"MAC">>, <<"value">> := Value} = Key) ->
+ OktKey = maps:merge(Key,
+ #{<<"kty">> => <<"oct">>,
+ <<"k">> => base64url:encode(Value)}),
+ fix_alg(OktKey).
+
+fix_alg(#{<<"alg">> := Alg} = Key) ->
+ Algs = uaa_algs(),
+ case maps:get(Alg, Algs, undefined) of
+ undefined -> Key;
+ Val -> Key#{<<"alg">> := Val}
+ end;
+fix_alg(#{} = Key) -> Key.
+
+uaa_algs() ->
+ UaaEnv = application:get_env(rabbitmq_auth_backend_oauth2, uaa_jwt_decoder, []),
+ DefaultAlgs = #{<<"HMACSHA256">> => <<"HS256">>,
+ <<"HMACSHA384">> => <<"HS384">>,
+ <<"HMACSHA512">> => <<"HS512">>,
+ <<"SHA256withRSA">> => <<"RS256">>,
+ <<"SHA512withRSA">> => <<"RS512">>},
+ proplists:get_value(uaa_algs, UaaEnv, DefaultAlgs).
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwt.erl b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwt.erl
new file mode 100644
index 0000000000..c2e41c5d52
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt_jwt.erl
@@ -0,0 +1,48 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+-module(uaa_jwt_jwt).
+
+%% Transitional step until we can require Erlang/OTP 21 and
+%% use the now recommended try/catch syntax for obtaining the stack trace.
+-compile(nowarn_deprecated_function).
+
+-export([decode/1, decode_and_verify/2, get_key_id/1]).
+
+-include_lib("jose/include/jose_jwt.hrl").
+-include_lib("jose/include/jose_jws.hrl").
+
+decode(Token) ->
+ try
+ #jose_jwt{fields = Fields} = jose_jwt:peek_payload(Token),
+ Fields
+ catch Type:Err:Stacktrace ->
+ {error, {invalid_token, Type, Err, Stacktrace}}
+ end.
+
+decode_and_verify(Jwk, Token) ->
+ case jose_jwt:verify(Jwk, Token) of
+ {true, #jose_jwt{fields = Fields}, _} -> {true, Fields};
+ {false, #jose_jwt{fields = Fields}, _} -> {false, Fields}
+ end.
+
+get_key_id(Token) ->
+ try
+ case jose_jwt:peek_protected(Token) of
+ #jose_jws{fields = #{<<"kid">> := Kid}} -> {ok, Kid};
+ #jose_jws{} -> get_default_key()
+ end
+ catch Type:Err:Stacktrace ->
+ {error, {invalid_token, Type, Err, Stacktrace}}
+ end.
+
+
+get_default_key() ->
+ UaaEnv = application:get_env(rabbitmq_auth_backend_oauth2, key_config, []),
+ case proplists:get_value(default_key, UaaEnv, undefined) of
+ undefined -> {error, no_key};
+ Val -> {ok, Val}
+ end.
diff --git a/deps/rabbitmq_auth_backend_oauth2/src/wildcard.erl b/deps/rabbitmq_auth_backend_oauth2/src/wildcard.erl
new file mode 100644
index 0000000000..01212901c6
--- /dev/null
+++ b/deps/rabbitmq_auth_backend_oauth2/src/wildcard.erl
@@ -0,0 +1,58 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(wildcard).
+
+-export([match/2]).
+
+-spec match(Subject :: binary(), Pattern :: binary()) -> boolean().
+match(Subject, Pattern) ->
+ case parse_pattern(Pattern) of
+ [First | Rest] ->
+ FirstSize = byte_size(First),
+ case Subject of
+ % If a pattern does not start with a wildcard,
+ % do exact matching in the beginning of the subject
+ <<First:FirstSize/binary, _/binary>> ->
+ scan(Subject, Rest, FirstSize, byte_size(Subject));
+ _ -> false
+ end;
+ invalid -> false
+ end.
+
+-spec scan(Subject :: binary(), Pattern :: [binary()],
+ Pos :: integer(), Length :: integer()) -> boolean().
+% Pattern ends with a wildcard
+scan(_Subject, [<<>>], _Pos, _Length) -> true;
+% Pattern is complete. Subject scan is complete
+scan(_Subject, [], Length, Length) -> true;
+% No more pattern but subject scan is not complete
+scan(_Subject, [], Pos, Length) when Pos =/= Length -> false;
+% Subject scan is complete but there are more pattern elements
+scan(_Subject, _NonEmpty, Length, Length) -> false;
+% Skip duplicate wildcards
+scan(Subject, [<<>> | Rest], Pos, Length) ->
+ scan(Subject, Rest, Pos, Length);
+% Every other Part is after a wildcard
+scan(Subject, [Part | Rest], Pos, Length) ->
+ PartSize = byte_size(Part),
+ case binary:match(Subject, Part, [{scope, {Pos, Length - Pos}}]) of
+ nomatch -> false;
+ {PartPos, PartSize} ->
+ NewPos = PartPos + PartSize,
+ scan(Subject, Rest, NewPos, Length)
+ end.
+
+-spec parse_pattern(binary()) -> [binary()] | invalid.
+parse_pattern(Pattern) ->
+ Parts = binary:split(Pattern, <<"*">>, [global]),
+ try lists:map(fun(Part) -> cow_qs:urldecode(Part) end, Parts)
+ catch Type:Error ->
+ rabbit_log:warning("Invalid pattern ~p : ~p~n",
+ [Pattern, {Type, Error}]),
+ invalid
+ end.