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:
Michael Klishin 2023-01-31 12:17:06 -05:00 committed by GitHub
commit 9ba8052ea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 83 additions and 11 deletions

View File

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

View File

@ -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} ->

View File

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

View File

@ -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}]}.

View File

@ -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:
%%

View File

@ -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}}}],