Skip to content

Commit 96a560c

Browse files
Merge pull request #16258 from amazon-mq/fix/rabbitmq-server-16255
Fix `auth_backend_cache` cache miss on reconnect (#16255)
2 parents 28a847c + 067e41f commit 96a560c

7 files changed

Lines changed: 159 additions & 46 deletions

File tree

deps/rabbit/src/rabbit_auth_mechanism_plain.erl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ handle_response(Response, _State) ->
5252

5353

5454
build_auth_props(Pass, Socket) ->
55-
[{password, Pass}, {sockOrAddr, Socket}].
55+
%% Pass a precomputed boolean, not the raw socket, so that AuthProps is
56+
%% stable across reconnections of the same client. See issue #16255.
57+
[{password, Pass}, {is_loopback, rabbit_net:is_loopback(Socket)}].
5658

5759
extract_user_pass(Response) ->
5860
case extract_elem(Response) of

deps/rabbitmq_auth_backend_cache/test/rabbit_auth_backend_cache_SUITE.erl

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
%%
77
-module(rabbit_auth_backend_cache_SUITE).
88

9-
-include_lib("rabbit_common/include/rabbit.hrl").
9+
-include_lib("amqp_client/include/amqp_client.hrl").
10+
-include_lib("common_test/include/ct.hrl").
1011

1112
-compile(export_all).
1213

@@ -17,6 +18,7 @@ all() ->
1718
access_response,
1819
cache_expiration,
1920
cache_expiration_topic,
21+
cache_hit_across_reconnections,
2022
expiry_timestamp_delegation
2123
].
2224

@@ -40,6 +42,19 @@ init_per_testcase(access_response, Config) ->
4042
<<"guest">>, <<"/">>, <<"amq.topic">>, <<"^a">>, <<"^b">>, <<"acting-user">>
4143
]),
4244
Config;
45+
init_per_testcase(cache_hit_across_reconnections, Config) ->
46+
ok = rabbit_ct_broker_helpers:add_code_path_to_all_nodes(
47+
Config, rabbit_auth_backend_cache_counting_mock),
48+
ok = rpc(Config, rabbit_auth_backend_cache_counting_mock, init, []),
49+
ok = rpc(
50+
Config, application, set_env,
51+
[rabbitmq_auth_backend_cache, cached_backend,
52+
rabbit_auth_backend_cache_counting_mock]),
53+
ok = rpc(
54+
Config, application, set_env,
55+
[rabbit, auth_backends, [rabbit_auth_backend_cache]]),
56+
ok = rpc(Config, rabbit_auth_backend_cache, clear_cache, []),
57+
Config;
4358
init_per_testcase(_TestCase, Config) ->
4459
Config.
4560

@@ -53,6 +68,18 @@ end_per_testcase(cache_expiration, Config) ->
5368
rabbit_ct_broker_helpers:add_user(Config, <<"guest">>),
5469
rabbit_ct_broker_helpers:set_full_permissions(Config, <<"/">>),
5570
Config;
71+
end_per_testcase(cache_hit_across_reconnections, Config) ->
72+
%% Restore the default cached_backend defined in the application's
73+
%% PROJECT_ENV so that subsequent test cases are unaffected.
74+
ok = rpc(
75+
Config, application, set_env,
76+
[rabbitmq_auth_backend_cache, cached_backend,
77+
rabbit_auth_backend_internal]),
78+
ok = rpc(
79+
Config, application, set_env,
80+
[rabbit, auth_backends, [rabbit_auth_backend_internal]]),
81+
ok = rpc(Config, rabbit_auth_backend_cache, clear_cache, []),
82+
Config;
5683
end_per_testcase(_TestCase, Config) ->
5784
Config.
5885

@@ -163,6 +190,27 @@ cache_expiration_topic(Config) ->
163190
false = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
164191
false = rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]).
165192

193+
cache_hit_across_reconnections(Config) ->
194+
%% Regression test for issue #16255: the cache backend used to miss on
195+
%% every reconnection because the underlying socket, which varies per
196+
%% connection, was part of AuthProps and therefore part of the cache
197+
%% key. Two sequential AMQP 0-9-1 connections with the same credentials
198+
%% should result in exactly one call to the underlying authentication
199+
%% backend.
200+
ok = rpc(Config, rabbit_auth_backend_cache_counting_mock, reset, []),
201+
Host = ?config(rmq_hostname, Config),
202+
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
203+
Params = #amqp_params_network{host = Host,
204+
port = Port,
205+
username = <<"guest">>,
206+
password = <<"guest">>},
207+
{ok, Conn1} = amqp_connection:start(Params),
208+
ok = amqp_connection:close(Conn1),
209+
{ok, Conn2} = amqp_connection:start(Params),
210+
ok = amqp_connection:close(Conn2),
211+
1 = rpc(Config, rabbit_auth_backend_cache_counting_mock,
212+
authentication_call_count, []).
213+
166214
expiry_timestamp_delegation(Config) ->
167215
{ok, Auth} = rpc(Config,rabbit_auth_backend_internal, user_login_authentication, [<<"guest">>, [{password, <<"guest">>}]]),
168216
%% rabbit_auth_backend_internal returns `never` for expiry_timestamp
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
%% Counting authentication and authorization backend used by
9+
%% rabbit_auth_backend_cache_SUITE to verify that the cache backend hits
10+
%% across reconnections that share the same credentials.
11+
12+
-module(rabbit_auth_backend_cache_counting_mock).
13+
-include_lib("rabbit_common/include/rabbit.hrl").
14+
15+
-behaviour(rabbit_authn_backend).
16+
-behaviour(rabbit_authz_backend).
17+
18+
-export([user_login_authentication/2, user_login_authorization/2,
19+
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
20+
expiry_timestamp/1]).
21+
22+
-export([init/0, reset/0, authentication_call_count/0]).
23+
24+
-define(KEY, {?MODULE, authentication_calls}).
25+
26+
init() ->
27+
reset().
28+
29+
reset() ->
30+
persistent_term:put(?KEY, counters:new(1, [])),
31+
ok.
32+
33+
authentication_call_count() ->
34+
counters:get(persistent_term:get(?KEY), 1).
35+
36+
user_login_authentication(Username, _AuthProps) ->
37+
counters:add(persistent_term:get(?KEY), 1, 1),
38+
{ok, #auth_user{username = Username,
39+
tags = [],
40+
impl = fun() -> none end}}.
41+
42+
user_login_authorization(_Username, _AuthProps) ->
43+
{ok, fun() -> none end, []}.
44+
45+
check_vhost_access(#auth_user{}, _VHostPath, _AuthzData) ->
46+
true.
47+
48+
check_resource_access(#auth_user{}, #resource{}, _Permission, _AuthzContext) ->
49+
true.
50+
51+
check_topic_access(#auth_user{}, #resource{}, _Permission, _TopicContext) ->
52+
true.
53+
54+
expiry_timestamp(_) ->
55+
never.

deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,17 @@ is_internal_none_password(_, _) -> false.
9898
is_sockOrAddr(sockOrAddr) -> true;
9999
is_sockOrAddr(_) -> false.
100100

101+
%% is_loopback is derived from the connection's peer address and is
102+
%% internal context, not a public credential, so it must not be sent to
103+
%% the external HTTP auth server.
104+
is_connection_context(is_loopback) -> true;
105+
is_connection_context(_) -> false.
106+
101107
extract_other_credentials(AuthProps) ->
102108
PublicAuthProps = [{K,V} || {K,V} <-AuthProps, not is_internal_property(K) and
103109
not is_internal_none_password(K, V) and
104-
not is_sockOrAddr(K)],
110+
not is_sockOrAddr(K) and
111+
not is_connection_context(K)],
105112
case PublicAuthProps of
106113
[] -> resolve_using_persisted_credentials(AuthProps);
107114
_ -> PublicAuthProps

deps/rabbitmq_auth_backend_internal_loopback/src/rabbit_auth_backend_internal_loopback.erl

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -78,42 +78,41 @@ user_login_authentication(Username, []) ->
7878
%% For cases when we do have a set of credentials. rabbit_auth_mechanism_plain:handle_response/2
7979
%% performs initial validation.
8080
user_login_authentication(Username, AuthProps) ->
81-
case proplists:lookup(sockOrAddr, AuthProps) of
82-
none -> {refused, ?NO_SOCKET_OR_ADDRESS_REJECTION_MESSAGE, [Username]}; % sockOrAddr doesn't exist
83-
{sockOrAddr, SockOrAddr} ->
84-
case rabbit_net:is_loopback(SockOrAddr) of
85-
true ->
86-
case lists:keyfind(password, 1, AuthProps) of
87-
{password, <<"">>} ->
88-
{refused, ?BLANK_PASSWORD_REJECTION_MESSAGE,
89-
[Username]};
90-
{password, ""} ->
91-
{refused, ?BLANK_PASSWORD_REJECTION_MESSAGE,
92-
[Username]};
93-
{password, none} -> %% For cases when authenticating using an x.509 certificate
94-
internal_check_user_login(Username, fun(_) -> true end);
95-
{password, Cleartext} ->
96-
internal_check_user_login(
97-
Username,
98-
fun(User) ->
99-
case internal_user:get_password_hash(User) of
100-
<<Salt:4/binary, Hash/binary>> ->
101-
Hash =:= rabbit_password:salted_hash(
102-
hashing_module_for_user(User), Salt, Cleartext);
103-
_ ->
104-
false
105-
end
106-
end);
107-
false ->
108-
case proplists:get_value(rabbit_auth_backend_internal, AuthProps, undefined) of
109-
undefined -> {refused, ?BLANK_PASSWORD_REJECTION_MESSAGE, [Username]};
110-
_ -> internal_check_user_login(Username, fun(_) -> true end)
111-
end
112-
end;
81+
case proplists:lookup(is_loopback, AuthProps) of
82+
none ->
83+
%% is_loopback is not set, so we cannot tell whether the
84+
%% connection is from localhost. Refuse.
85+
{refused, ?NO_SOCKET_OR_ADDRESS_REJECTION_MESSAGE, [Username]};
86+
{is_loopback, true} ->
87+
case lists:keyfind(password, 1, AuthProps) of
88+
{password, <<"">>} ->
89+
{refused, ?BLANK_PASSWORD_REJECTION_MESSAGE,
90+
[Username]};
91+
{password, ""} ->
92+
{refused, ?BLANK_PASSWORD_REJECTION_MESSAGE,
93+
[Username]};
94+
{password, none} -> %% For cases when authenticating using an x.509 certificate
95+
internal_check_user_login(Username, fun(_) -> true end);
96+
{password, Cleartext} ->
97+
internal_check_user_login(
98+
Username,
99+
fun(User) ->
100+
case internal_user:get_password_hash(User) of
101+
<<Salt:4/binary, Hash/binary>> ->
102+
Hash =:= rabbit_password:salted_hash(
103+
hashing_module_for_user(User), Salt, Cleartext);
104+
_ ->
105+
false
106+
end
107+
end);
113108
false ->
114-
{refused, ?NOT_LOOPBACK_REJECTION_MESSAGE, [Username]}
115-
end
116-
109+
case proplists:get_value(rabbit_auth_backend_internal, AuthProps, undefined) of
110+
undefined -> {refused, ?BLANK_PASSWORD_REJECTION_MESSAGE, [Username]};
111+
_ -> internal_check_user_login(Username, fun(_) -> true end)
112+
end
113+
end;
114+
{is_loopback, false} ->
115+
{refused, ?NOT_LOOPBACK_REJECTION_MESSAGE, [Username]}
117116
end.
118117

119118

deps/rabbitmq_auth_backend_internal_loopback/test/rabbit_auth_backend_internal_loopback_SUITE.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ end_per_suite(Config) ->
6565
init_per_group(localhost_connection, Config) ->
6666
ok = rabbit_ct_broker_helpers:add_user(Config, maps:get(username, ?LOOPBACK_USER)),
6767
ok = rabbit_ct_broker_helpers:add_user(Config, maps:get(username, ?NONLOOPBACK_USER)),
68-
[{sockOrAddr, ?LOCALHOST_ADDR} | Config];
68+
[{peer_addr, ?LOCALHOST_ADDR} | Config];
6969
init_per_group(nonlocalhost_connection, Config) ->
70-
[{sockOrAddr, ?NONLOCALHOST_ADDR} | Config];
70+
[{peer_addr, ?NONLOCALHOST_ADDR} | Config];
7171
init_per_group(_, Config) ->
7272
Config.
7373

@@ -99,5 +99,5 @@ login_from_nonlocalhost_with_nonloopback_user(Config) ->
9999
rpc(Config, M, F, A) ->
100100
rabbit_ct_broker_helpers:rpc(Config, 0, M, F, A).
101101

102-
build_auth_props(Pass, Socket) ->
103-
[{password, Pass}, {sockOrAddr, Socket}].
102+
build_auth_props(Pass, Addr) ->
103+
[{password, Pass}, {is_loopback, rabbit_net:is_loopback(Addr)}].

deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_access_control.erl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,12 @@ is_authorized(ReqData, Context, Username, Password, ErrorMsg, Fun, AuthConfig, R
138138
end,
139139
{IP, _} = cowboy_req:peer(ReqData),
140140

141-
AuthProps = [{password, Password}, {sockOrAddr, IP}] ++ case vhost(ReqData) of
142-
VHost when is_binary(VHost) -> [{vhost, VHost}];
143-
_ -> []
144-
end,
141+
AuthProps = [{password, Password},
142+
{is_loopback, rabbit_net:is_loopback(IP)}] ++
143+
case vhost(ReqData) of
144+
VHost when is_binary(VHost) -> [{vhost, VHost}];
145+
_ -> []
146+
end,
145147

146148
{ok, AuthBackends} = get_auth_backends(),
147149

0 commit comments

Comments
 (0)