Merge pull request #6931 from rabbitmq/support-oauth2-in-amqp1_0
Support OAuth 2.0 authentication in AMQP 1.0 plugin
This commit is contained in:
commit
9ba8052ea1
|
@ -92,7 +92,11 @@ user_login_authentication(Username, AuthProps) ->
|
|||
false
|
||||
end
|
||||
end);
|
||||
false -> exit({unknown_auth_props, Username, AuthProps})
|
||||
false ->
|
||||
case proplists:get_value(rabbit_auth_backend_internal, AuthProps, undefined) of
|
||||
undefined -> exit({unknown_auth_props, Username, AuthProps});
|
||||
_ -> internal_check_user_login(Username, fun(_) -> true end)
|
||||
end
|
||||
end.
|
||||
|
||||
state_can_expire() -> false.
|
||||
|
|
|
@ -52,8 +52,8 @@ list() ->
|
|||
auth_fun({none, _}, _VHost, _ExtraAuthProps) ->
|
||||
fun () -> {ok, rabbit_auth_backend_dummy:user()} end;
|
||||
|
||||
auth_fun({Username, none}, _VHost, _ExtraAuthProps) ->
|
||||
fun () -> rabbit_access_control:check_user_login(Username, []) end;
|
||||
auth_fun({Username, none}, _VHost, ExtraAuthProps) ->
|
||||
fun () -> rabbit_access_control:check_user_login(Username, [] ++ ExtraAuthProps) end;
|
||||
|
||||
auth_fun({Username, Password}, VHost, ExtraAuthProps) ->
|
||||
fun () ->
|
||||
|
@ -72,8 +72,19 @@ auth_fun({Username, Password}, VHost, ExtraAuthProps) ->
|
|||
'broker_not_found_on_node' |
|
||||
{'auth_failure', string()} | 'access_refused').
|
||||
|
||||
%% Infos is a PropList which contains the content of the Proplist #amqp_adapter_info.additional_info
|
||||
%% among other credentials such as protocol, ssl information, etc.
|
||||
%% #amqp_adapter_info.additional_info may carry a credential called `authz_bakends` which has the
|
||||
%% content of the #user.authz_backends attribute. This means that we are propagating the outcome
|
||||
%% from the first successful authentication for the current user when opening an internal
|
||||
%% amqp connection. This is particularly relevant for protocol plugins such as AMQP 1.0 where
|
||||
%% users are authenticated in one context and later on an internal amqp connection is opened
|
||||
%% on a different context. In other words, we do not have anymore the initial credentials presented
|
||||
%% during the first authentication. However, we do have the outcome from such successful authentication.
|
||||
|
||||
connect(Creds, VHost, Protocol, Pid, Infos) ->
|
||||
ExtraAuthProps = extract_extra_auth_props(Creds, VHost, Pid, Infos),
|
||||
ExtraAuthProps = append_authz_backends(extract_extra_auth_props(Creds, VHost, Pid, Infos), Infos),
|
||||
|
||||
AuthFun = auth_fun(Creds, VHost, ExtraAuthProps),
|
||||
case rabbit_boot_state:has_reached_and_is_active(core_started) of
|
||||
true ->
|
||||
|
@ -114,6 +125,13 @@ extract_extra_auth_props(Creds, VHost, Pid, Infos) ->
|
|||
maybe_call_connection_info_module(Protocol, Creds, VHost, Pid, Infos)
|
||||
end.
|
||||
|
||||
|
||||
append_authz_backends(AuthProps, Infos) ->
|
||||
case proplists:get_value(authz_backends, Infos, undefined) of
|
||||
undefined -> AuthProps;
|
||||
AuthzBackends -> AuthProps ++ AuthzBackends
|
||||
end.
|
||||
|
||||
extract_protocol(Infos) ->
|
||||
case proplists:get_value(protocol, Infos, undefined) of
|
||||
{Protocol, _Version} ->
|
||||
|
|
|
@ -710,6 +710,7 @@ send_to_new_1_0_session(Channel, Frame, State) ->
|
|||
user = User},
|
||||
proxy_socket = ProxySocket} = State,
|
||||
%% Note: the equivalent, start_channel is in channel_sup_sup
|
||||
|
||||
case rabbit_amqp1_0_session_sup_sup:start_session(
|
||||
%% NB subtract fixed frame header size
|
||||
ChanSupSup, {amqp10_framing, Sock, Channel,
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
%%----------------------------------------------------------------------------
|
||||
start_link({amqp10_framing, Sock, Channel, FrameMax, ReaderPid,
|
||||
Username, VHost, Collector, ProxySocket}) ->
|
||||
User, VHost, Collector, ProxySocket}) ->
|
||||
{ok, SupPid} = supervisor:start_link(?MODULE, []),
|
||||
{ok, WriterPid} =
|
||||
supervisor:start_child(
|
||||
|
@ -61,8 +61,8 @@ start_link({amqp10_framing, Sock, Channel, FrameMax, ReaderPid,
|
|||
id => channel,
|
||||
start =>
|
||||
{rabbit_amqp1_0_session_process, start_link, [
|
||||
{Channel, ReaderPid, WriterPid, Username, VHost, FrameMax,
|
||||
adapter_info(SocketForAdapterInfo), Collector}
|
||||
{Channel, ReaderPid, WriterPid, User, VHost, FrameMax,
|
||||
adapter_info(User, SocketForAdapterInfo), Collector}
|
||||
]},
|
||||
restart => transient,
|
||||
significant => true,
|
||||
|
@ -86,5 +86,19 @@ init([]) ->
|
|||
auto_shutdown => any_significant},
|
||||
{ok, {SupFlags, []}}.
|
||||
|
||||
adapter_info(Sock) ->
|
||||
amqp_connection:socket_adapter_info(Sock, {'AMQP', "1.0"}).
|
||||
|
||||
%% For each AMQP 1.0 session opened, an internal direct AMQP 0-9-1 connection is opened too.
|
||||
%% This direct connection will authenticate the user again. Again because at this point
|
||||
%% the SASL handshake has already taken place and this user has already been authenticated.
|
||||
%% However, we do not have the credentials the user presented. For that reason, the
|
||||
%% #amqp_adapter_info.additional_info carries an extra property called authz_backends
|
||||
%% which is initialized from the #user.authz_backends attribute. In other words, we
|
||||
%% propagate the outcome from the first authentication attempt to the subsequent attempts.
|
||||
|
||||
%% See rabbit_direct.erl to see how `authz_bakends` is propagated from
|
||||
% amqp_adapter_info.additional_info to the rabbit_access_control module
|
||||
|
||||
adapter_info(User, Sock) ->
|
||||
AdapterInfo = amqp_connection:socket_adapter_info(Sock, {'AMQP', "1.0"}),
|
||||
AdapterInfo#amqp_adapter_info{additional_info =
|
||||
AdapterInfo#amqp_adapter_info.additional_info ++ [{authz_backends, User#user.authz_backends}]}.
|
||||
|
|
|
@ -167,7 +167,7 @@ validate_token_expiry(#{<<"exp">> := Exp}) when is_integer(Exp) ->
|
|||
end;
|
||||
validate_token_expiry(#{}) -> ok.
|
||||
|
||||
-spec check_token(binary()) ->
|
||||
-spec check_token(binary() | map()) ->
|
||||
{'ok', map()} |
|
||||
{'error', term() }|
|
||||
{'refused',
|
||||
|
@ -175,6 +175,9 @@ validate_token_expiry(#{}) -> ok.
|
|||
{'error', term()} |
|
||||
{'invalid_aud', term()}}.
|
||||
|
||||
check_token(DecodedToken) when is_map(DecodedToken) ->
|
||||
{ok, DecodedToken};
|
||||
|
||||
check_token(Token) ->
|
||||
Settings = application:get_all_env(?APP),
|
||||
case uaa_jwt:decode_and_verify(Token) of
|
||||
|
@ -533,9 +536,23 @@ check_aud(Aud, ResourceServerId) ->
|
|||
|
||||
get_scopes(#{?SCOPE_JWT_FIELD := Scope}) -> Scope.
|
||||
|
||||
%% 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.
|
||||
%% However, there are scenarios where the same user (on the same connection) is authenticated
|
||||
%% more than once. When this scenario occurs, we extract the token from the credential
|
||||
%% called rabbit_auth_backend_oauth2 whose value is the Decoded token returned during the
|
||||
%% first authentication.
|
||||
|
||||
-spec token_from_context(map()) -> binary() | undefined.
|
||||
token_from_context(AuthProps) ->
|
||||
maps:get(password, AuthProps, undefined).
|
||||
case maps:get(password, AuthProps, undefined) of
|
||||
undefined ->
|
||||
case maps:get(rabbit_auth_backend_oauth2, AuthProps, undefined) of
|
||||
undefined -> undefined;
|
||||
Impl -> Impl()
|
||||
end;
|
||||
Token -> Token
|
||||
end.
|
||||
|
||||
%% Decoded tokens look like this:
|
||||
%%
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
|
||||
all() ->
|
||||
[
|
||||
test_own_scope,
|
||||
|
@ -19,6 +20,7 @@ all() ->
|
|||
test_validate_payload,
|
||||
test_validate_payload_when_verify_aud_false,
|
||||
test_successful_access_with_a_token,
|
||||
test_successful_access_with_a_parsed_token,
|
||||
test_successful_access_with_a_token_that_has_tag_scopes,
|
||||
test_unsuccessful_access_with_a_bogus_token,
|
||||
test_restricted_vhost_access_with_a_valid_token,
|
||||
|
@ -629,6 +631,22 @@ test_successful_access_with_a_token(_) ->
|
|||
|
||||
assert_topic_access_granted(User, VHost, <<"bar">>, read, #{routing_key => <<"#/foo">>}).
|
||||
|
||||
test_successful_access_with_a_parsed_token(_) ->
|
||||
Jwk = ?UTIL_MOD:fixture_jwk(),
|
||||
UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}],
|
||||
application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv),
|
||||
application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>),
|
||||
|
||||
VHost = <<"vhost">>,
|
||||
Username = <<"username">>,
|
||||
Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:token_with_sub(?UTIL_MOD:fixture_token(), Username), Jwk),
|
||||
{ok, #auth_user{impl = Impl} } =
|
||||
rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]),
|
||||
|
||||
{ok, _ } =
|
||||
rabbit_auth_backend_oauth2:user_login_authentication(Username, [{rabbit_auth_backend_oauth2, Impl}]).
|
||||
|
||||
|
||||
test_successful_access_with_a_token_that_has_tag_scopes(_) ->
|
||||
Jwk = ?UTIL_MOD:fixture_jwk(),
|
||||
UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}],
|
||||
|
|
Loading…
Reference in New Issue