Skip to content

Commit 5b90d1a

Browse files
Merge pull request #12831 from rabbitmq/mergify/bp/v4.0.x/pr-12818
OAuth 2: JWT token refresh should preserve user identity displayed in the UI (backport #12818)
2 parents 1edb047 + c46a22c commit 5b90d1a

File tree

4 files changed

+109
-26
lines changed

4 files changed

+109
-26
lines changed

deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl

+39-21
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020

2121
% for testing
2222
-export([post_process_payload/2, get_expanded_scopes/2]).
23-
23+
-import(uaa_jwt, [resolve_resource_server_id/1]).
2424
-import(rabbit_data_coercion, [to_map/1]).
25+
-import(rabbit_oauth2_config, [get_preferred_username_claims/1]).
2526

2627
-ifdef(TEST).
2728
-compile(export_all).
@@ -98,19 +99,28 @@ check_topic_access(#auth_user{impl = DecodedTokenFun},
9899
end).
99100

100101
update_state(AuthUser, NewToken) ->
101-
case check_token(NewToken) of
102-
%% avoid logging the token
103-
{error, _} = E -> E;
104-
{refused, {error, {invalid_token, error, _Err, _Stacktrace}}} ->
105-
{refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid"};
106-
{refused, Err} ->
107-
{refused, rabbit_misc:format("Authentication using an OAuth 2/JWT token failed: ~tp", [Err])};
108-
{ok, DecodedToken} ->
109-
Tags = tags_from(DecodedToken),
110-
111-
{ok, AuthUser#auth_user{tags = Tags,
112-
impl = fun() -> DecodedToken end}}
113-
end.
102+
case check_token(NewToken) of
103+
%% avoid logging the token
104+
{error, _} = E -> E;
105+
{refused, {error, {invalid_token, error, _Err, _Stacktrace}}} ->
106+
{refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid"};
107+
{refused, Err} ->
108+
{refused, rabbit_misc:format("Authentication using an OAuth 2/JWT token failed: ~tp", [Err])};
109+
{ok, DecodedToken} ->
110+
ResourceServerId = resolve_resource_server_id(DecodedToken),
111+
CurToken = AuthUser#auth_user.impl,
112+
case ensure_same_username(
113+
get_preferred_username_claims(ResourceServerId),
114+
CurToken(), DecodedToken) of
115+
ok ->
116+
Tags = tags_from(DecodedToken),
117+
{ok, AuthUser#auth_user{tags = Tags,
118+
impl = fun() -> DecodedToken end}};
119+
{error, mismatch_username_after_token_refresh} ->
120+
{refused,
121+
"Not allowed to change username on refreshed token"}
122+
end
123+
end.
114124

115125
expiry_timestamp(#auth_user{impl = DecodedTokenFun}) ->
116126
case DecodedTokenFun() of
@@ -135,13 +145,15 @@ authenticate(_, AuthProps0) ->
135145
{refused, "Authentication using an OAuth 2/JWT token failed: ~tp", [Err]};
136146
{ok, DecodedToken} ->
137147
Func = fun(Token0) ->
138-
Username = username_from(rabbit_oauth2_config:get_preferred_username_claims(), Token0),
139-
Tags = tags_from(Token0),
140-
141-
{ok, #auth_user{username = Username,
142-
tags = Tags,
143-
impl = fun() -> Token0 end}}
144-
end,
148+
ResourceServerId = resolve_resource_server_id(Token0),
149+
Username = username_from(
150+
get_preferred_username_claims(ResourceServerId),
151+
Token0),
152+
Tags = tags_from(Token0),
153+
{ok, #auth_user{username = Username,
154+
tags = Tags,
155+
impl = fun() -> Token0 end}}
156+
end,
145157
case with_decoded_token(DecodedToken, Func) of
146158
{error, Err} ->
147159
{refused, "Authentication using an OAuth 2/JWT token failed: ~tp", [Err]};
@@ -157,6 +169,12 @@ with_decoded_token(DecodedToken, Fun) ->
157169
rabbit_log:error(Msg),
158170
Err
159171
end.
172+
ensure_same_username(PreferredUsernameClaims, CurrentDecodedToken, NewDecodedToken) ->
173+
CurUsername = username_from(PreferredUsernameClaims, CurrentDecodedToken),
174+
case {CurUsername, username_from(PreferredUsernameClaims, NewDecodedToken)} of
175+
{CurUsername, CurUsername} -> ok;
176+
_ -> {error, mismatch_username_after_token_refresh}
177+
end.
160178

161179
validate_token_expiry(#{<<"exp">> := Exp}) when is_integer(Exp) ->
162180
Now = os:system_time(seconds),

deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
-export([add_signing_key/3,
1010
decode_and_verify/1,
1111
get_jwk/2,
12+
resolve_resource_server_id/1,
1213
verify_signing_key/2]).
1314

1415
-export([client_id/1, sub/1, client_id/2, sub/2]).
@@ -79,6 +80,14 @@ decode_and_verify(Token) ->
7980
end
8081
end.
8182

83+
-spec resolve_resource_server_id(binary()|map()) -> binary() | {error, term()}.
84+
resolve_resource_server_id(Token) when is_map(Token) ->
85+
case maps:get(<<"aud">>, Token, undefined) of
86+
undefined ->
87+
{error, audience_not_found_in_token};
88+
Audience ->
89+
rabbit_oauth2_config:get_resource_server_id_for_audience(Audience)
90+
end;
8291
resolve_resource_server_id(Token) ->
8392
case uaa_jwt_jwt:get_aud(Token) of
8493
{error, _} = Error ->
@@ -87,6 +96,7 @@ resolve_resource_server_id(Token) ->
8796
rabbit_oauth2_config:get_resource_server_id_for_audience(Audience)
8897
end.
8998

99+
90100
-spec get_jwk(binary(), oauth_provider_id()) -> {ok, map()} | {error, term()}.
91101
get_jwk(KeyId, OAuthProviderId) ->
92102
get_jwk(KeyId, OAuthProviderId, true).

deps/rabbitmq_auth_backend_oauth2/test/jwks_SUITE.erl

+34-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
open_unmanaged_connection/4, open_unmanaged_connection/5,
1818
close_connection_and_channel/2]).
1919
-import(rabbit_mgmt_test_util, [amqp_port/1]).
20+
-import(rabbit_ct_helpers, [
21+
set_config/2,
22+
get_config/2, get_config/3
23+
]).
2024

2125
all() ->
2226
[
@@ -45,7 +49,8 @@ groups() ->
4549
test_failed_connection_with_a_token_with_insufficient_resource_permission,
4650
test_failed_connection_with_algorithm_restriction,
4751
test_failed_token_refresh_case1,
48-
test_failed_token_refresh_case2
52+
test_failed_token_refresh_case2,
53+
cannot_change_username_on_refreshed_token
4954
]},
5055
{no_peer_verification, [], [
5156
{group, happy_path},
@@ -531,6 +536,11 @@ generate_valid_token(Config, Jwk, Scopes, Audience) ->
531536
IncludeKid = rabbit_ct_helpers:get_config(Config, include_kid, true),
532537
?UTIL_MOD:sign_token_hs(Token, Jwk, IncludeKid).
533538

539+
generate_valid_token_with_sub(Config, Jwk, Scopes, Sub) ->
540+
Token = ?UTIL_MOD:token_with_sub(?UTIL_MOD:fixture_token_with_scopes(Scopes), Sub),
541+
IncludeKid = rabbit_ct_helpers:get_config(Config, include_kid, true),
542+
?UTIL_MOD:sign_token_hs(Token, Jwk, IncludeKid).
543+
534544
generate_valid_token_with_extra_fields(Config, ExtraFields) ->
535545
Jwk = case rabbit_ct_helpers:get_config(Config, fixture_jwk) of
536546
undefined -> ?UTIL_MOD:fixture_jwk();
@@ -912,6 +922,29 @@ test_failed_token_refresh_case2(Config) ->
912922

913923
close_connection(Conn).
914924

925+
cannot_change_username_on_refreshed_token(Config) ->
926+
Jwk =
927+
case get_config(Config, fixture_jwk) of
928+
undefined -> ?UTIL_MOD:fixture_jwk();
929+
Value -> Value
930+
end,
931+
{_, CurToken} = generate_valid_token(Config, Jwk, <<"oldUsername">>, [
932+
<<"rabbitmq.configure:vhost4/*">>,
933+
<<"rabbitmq.write:vhost4/*">>,
934+
<<"rabbitmq.read:vhost4/*">>]),
935+
Conn = open_unmanaged_connection(Config, 0, <<"vhost4">>,
936+
<<"oldUsername">>, CurToken),
937+
938+
{_, RefreshToken} = generate_valid_token_with_sub(Config, Jwk, <<"newUsername">>,
939+
[<<"rabbitmq.configure:vhost4/*">>,
940+
<<"rabbitmq.write:vhost4/*">>,
941+
<<"rabbitmq.read:vhost4/*">>]),
942+
943+
%% the error is communicated asynchronously via a connection-level error
944+
?assertException(exit, _, amqp_connection:update_secret(Conn, RefreshToken,
945+
<<"token refresh">>)).
946+
947+
915948
test_failed_connection_with_algorithm_restriction(Config) ->
916949
{_Algo, Token} = rabbit_ct_helpers:get_config(Config, fixture_jwt),
917950
?assertMatch({error, {auth_failure, _}},

deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl

+26-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ groups() ->
5252

5353
{token_refresh, [], [
5454
test_failed_token_refresh_case1,
55-
test_failed_token_refresh_case2
55+
test_failed_token_refresh_case2,
56+
refreshed_token_cannot_change_username
5657
]},
5758

5859
{extra_scopes_source, [], [
@@ -312,21 +313,33 @@ preconfigure_node(Config) ->
312313

313314
rabbit_ct_helpers:set_config(Config, {fixture_jwk, Jwk}).
314315

316+
generate_valid_token_with_sub(Config, Sub) ->
317+
generate_valid_token(Config,
318+
?UTIL_MOD:full_permission_scopes(), undefined, Sub).
319+
315320
generate_valid_token(Config) ->
316321
generate_valid_token(Config, ?UTIL_MOD:full_permission_scopes()).
317322

318323
generate_valid_token(Config, Scopes) ->
319-
generate_valid_token(Config, Scopes, undefined).
324+
generate_valid_token(Config, Scopes, undefined, undefined).
320325

321326
generate_valid_token(Config, Scopes, Audience) ->
327+
generate_valid_token(Config, Scopes, Audience, undefined).
328+
329+
generate_valid_token(Config, Scopes, Audience, Sub) ->
322330
Jwk = case rabbit_ct_helpers:get_config(Config, fixture_jwk) of
323331
undefined -> ?UTIL_MOD:fixture_jwk();
324332
Value -> Value
325333
end,
326-
Token = case Audience of
334+
Token0 = case Audience of
327335
undefined -> ?UTIL_MOD:fixture_token_with_scopes(Scopes);
328-
DefinedAudience -> maps:put(<<"aud">>, DefinedAudience, ?UTIL_MOD:fixture_token_with_scopes(Scopes))
336+
DefinedAudience -> maps:put(<<"aud">>, DefinedAudience,
337+
?UTIL_MOD:fixture_token_with_scopes(Scopes))
329338
end,
339+
Token = case Sub of
340+
undefined -> Token0;
341+
_ -> maps:put(<<"sub">>, Sub, Token0)
342+
end,
330343
?UTIL_MOD:sign_token_hs(Token, Jwk).
331344

332345
generate_valid_token_with_extra_fields(Config, ExtraFields) ->
@@ -693,6 +706,15 @@ test_failed_token_refresh_case1(Config) ->
693706

694707
close_connection(Conn).
695708

709+
refreshed_token_cannot_change_username(Config) ->
710+
{_, Token} = generate_valid_token_with_sub(Config, <<"username">>),
711+
Conn = open_unmanaged_connection(Config, 0, <<"vhost4">>, <<"username">>, Token),
712+
{_, RefreshedToken} = generate_valid_token_with_sub(Config, <<"username2">>),
713+
714+
%% the error is communicated asynchronously via a connection-level error
715+
?assertException(exit, {{nodedown,not_allowed},_}, amqp_connection:update_secret(Conn, RefreshedToken, <<"token refresh">>)).
716+
717+
696718
test_failed_token_refresh_case2(Config) ->
697719
{_Algo, Token} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost4/*">>,
698720
<<"rabbitmq.write:vhost4/*">>,

0 commit comments

Comments
 (0)