Skip to content

Commit 174d7af

Browse files
Merge pull request #11175 from rabbitmq/mergify/bp/v3.13.x/pr-10645
Add token helper functions to oauth2-client module (backport #10645)
2 parents 94a6737 + 6435d40 commit 174d7af

File tree

10 files changed

+744
-471
lines changed

10 files changed

+744
-471
lines changed

deps/oauth2_client/BUILD.bazel

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ rabbitmq_app(
4747
],
4848
license_files = [":license_files"],
4949
priv = [":priv"],
50-
deps = ["//deps/rabbit_common:erlang_app"],
50+
deps = [
51+
"//deps/rabbit_common:erlang_app",
52+
"@jose//:erlang_app",
53+
],
5154
)
5255

5356
xref(
@@ -77,7 +80,9 @@ dialyze(
7780

7881
eunit(
7982
name = "eunit",
80-
compiled_suites = [":test_oauth_http_mock_beam"],
83+
compiled_suites = [
84+
":test_oauth_http_mock_beam",
85+
":test_oauth2_client_test_util_beam"],
8186
target = ":test_erlang_app",
8287
)
8388

@@ -112,6 +117,9 @@ rabbitmq_integration_suite(
112117
rabbitmq_suite(
113118
name = "unit_SUITE",
114119
size = "small",
120+
additional_beam = [
121+
"test/oauth2_client_test_util.beam",
122+
],
115123
)
116124

117125
assert_suites()

deps/oauth2_client/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ PROJECT_DESCRIPTION = OAuth2 client from the RabbitMQ Project
33
PROJECT_MOD = oauth2_client_app
44

55
BUILD_DEPS = rabbit
6-
DEPS = rabbit_common
6+
DEPS = rabbit_common jose
77
TEST_DEPS = rabbitmq_ct_helpers cowboy
88
LOCAL_DEPS = ssl inets crypto public_key
99

deps/oauth2_client/app.bzl

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ def all_beam_files(name = "all_beam_files"):
88
)
99
erlang_bytecode(
1010
name = "other_beam",
11-
srcs = ["src/oauth2_client.erl"],
11+
srcs = ["src/oauth2_client.erl",
12+
"src/jwt_helper.erl"],
1213
hdrs = [":public_and_private_hdrs"],
1314
app_name = "oauth2_client",
1415
dest = "ebin",
1516
erlc_opts = "//:erlc_opts",
17+
deps = [
18+
"@jose//:erlang_app"
19+
],
1620
)
1721

1822
def all_test_beam_files(name = "all_test_beam_files"):
@@ -24,11 +28,15 @@ def all_test_beam_files(name = "all_test_beam_files"):
2428
erlang_bytecode(
2529
name = "test_other_beam",
2630
testonly = True,
27-
srcs = ["src/oauth2_client.erl"],
31+
srcs = ["src/oauth2_client.erl",
32+
"src/jwt_helper.erl"],
2833
hdrs = [":public_and_private_hdrs"],
2934
app_name = "oauth2_client",
3035
dest = "test",
3136
erlc_opts = "//:test_erlc_opts",
37+
deps = [
38+
"@jose//:erlang_app"
39+
],
3240
)
3341

3442
def all_srcs(name = "all_srcs"):
@@ -46,7 +54,8 @@ def all_srcs(name = "all_srcs"):
4654

4755
filegroup(
4856
name = "srcs",
49-
srcs = ["src/oauth2_client.erl"],
57+
srcs = ["src/oauth2_client.erl",
58+
"src/jwt_helper.erl"],
5059
)
5160
filegroup(
5261
name = "private_hdrs",
@@ -90,3 +99,11 @@ def test_suite_beam_files(name = "test_suite_beam_files"):
9099
app_name = "oauth2_client",
91100
erlc_opts = "//:test_erlc_opts",
92101
)
102+
erlang_bytecode(
103+
name = "test_oauth2_client_test_util_beam",
104+
testonly = True,
105+
srcs = ["test/oauth2_client_test_util.erl"],
106+
outs = ["test/oauth2_client_test_util.beam"],
107+
app_name = "oauth2_client",
108+
erlc_opts = "//:test_erlc_opts",
109+
)

deps/oauth2_client/include/oauth2_client.hrl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
% define access token request common constants
1010

1111
-define(DEFAULT_HTTP_TIMEOUT, 60000).
12+
13+
% Refresh tome this number of seconds before expires_in token's attribute
14+
-define(REFRESH_IN_BEFORE_EXPIRES_IN, 5).
15+
1216
-define(DEFAULT_OPENID_CONFIGURATION_PATH, "/.well-known/openid-configuration").
1317

1418
% define access token request constants
@@ -66,7 +70,9 @@
6670
-record(successful_access_token_response, {
6771
access_token :: binary(),
6872
token_type :: binary(),
69-
refresh_token :: option(binary()),
73+
refresh_token :: option(binary()), % A refresh token SHOULD NOT be included
74+
% .. for client-credentials flow.
75+
% https://www.rfc-editor.org/rfc/rfc6749#section-4.4.3
7076
expires_in :: option(integer())
7177
}).
7278

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
-module(jwt_helper).
8+
9+
-export([decode/1, get_expiration_time/1]).
10+
11+
-include_lib("jose/include/jose_jwt.hrl").
12+
13+
decode(Token) ->
14+
try
15+
#jose_jwt{fields = Fields} = jose_jwt:peek_payload(Token),
16+
Fields
17+
catch Type:Err:Stacktrace ->
18+
{error, {invalid_token, Type, Err, Stacktrace}}
19+
end.
20+
21+
get_expiration_time(#{<<"exp">> := Exp}) when is_integer(Exp) -> {ok, Exp};
22+
get_expiration_time(#{}) -> {error, missing_exp_field}.

deps/oauth2_client/src/oauth2_client.erl

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
66
%%
77
-module(oauth2_client).
8-
-export([get_access_token/2,
8+
-export([get_access_token/2, get_expiration_time/1,
99
refresh_access_token/2,
1010
get_oauth_provider/1, get_oauth_provider/2,
1111
extract_ssl_options_as_list/1
@@ -71,6 +71,19 @@ get_openid_configuration(IssuerURI, OpenIdConfigurationPath, TLSOptions) ->
7171
get_openid_configuration(IssuerURI, TLSOptions) ->
7272
get_openid_configuration(IssuerURI, ?DEFAULT_OPENID_CONFIGURATION_PATH, TLSOptions).
7373

74+
-spec get_expiration_time(successful_access_token_response()) ->
75+
{ok, [{expires_in, integer() }| {exp, integer() }]} | {error, missing_exp_field}.
76+
get_expiration_time(#successful_access_token_response{expires_in = ExpiresInSec,
77+
access_token = AccessToken}) ->
78+
case ExpiresInSec of
79+
undefined ->
80+
case jwt_helper:get_expiration_time(jwt_helper:decode(AccessToken)) of
81+
{ok, Exp} -> {ok, [{exp, Exp}]};
82+
{error, _} = Error -> Error
83+
end;
84+
_ -> {ok, [{expires_in, ExpiresInSec}]}
85+
end.
86+
7487
update_oauth_provider_endpoints_configuration(OAuthProvider) ->
7588
LockId = lock(),
7689
try do_update_oauth_provider_endpoints_configuration(OAuthProvider) of
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
-module(oauth2_client_test_util).
8+
9+
-compile(export_all).
10+
11+
-define(DEFAULT_EXPIRATION_IN_SECONDS, 2).
12+
13+
%%
14+
%% API
15+
%%
16+
17+
sign_token_hs(Token, #{<<"kid">> := TokenKey} = Jwk) ->
18+
sign_token_hs(Token, Jwk, TokenKey).
19+
20+
sign_token_hs(Token, Jwk, TokenKey) ->
21+
Jws = #{
22+
<<"alg">> => <<"HS256">>,
23+
<<"kid">> => TokenKey
24+
},
25+
sign_token(Token, Jwk, Jws).
26+
27+
sign_token_rsa(Token, Jwk, TokenKey) ->
28+
Jws = #{
29+
<<"alg">> => <<"RS256">>,
30+
<<"kid">> => TokenKey
31+
},
32+
sign_token(Token, Jwk, Jws).
33+
34+
sign_token_no_kid(Token, Jwk) ->
35+
Signed = jose_jwt:sign(Jwk, Token),
36+
jose_jws:compact(Signed).
37+
38+
sign_token(Token, Jwk, Jws) ->
39+
Signed = jose_jwt:sign(Jwk, Jws, Token),
40+
jose_jws:compact(Signed).
41+
42+
fixture_jwk() ->
43+
fixture_jwk(<<"token-key">>).
44+
45+
fixture_jwk(TokenKey) ->
46+
fixture_jwk(TokenKey, <<"dG9rZW5rZXk">>).
47+
48+
fixture_jwk(TokenKey, K) ->
49+
#{<<"alg">> => <<"HS256">>,
50+
<<"k">> => K,
51+
<<"kid">> => TokenKey,
52+
<<"kty">> => <<"oct">>,
53+
<<"use">> => <<"sig">>,
54+
<<"value">> => TokenKey}.
55+
56+
full_permission_scopes() ->
57+
[<<"rabbitmq.configure:*/*">>,
58+
<<"rabbitmq.write:*/*">>,
59+
<<"rabbitmq.read:*/*">>].
60+
61+
expirable_token() ->
62+
expirable_token(?DEFAULT_EXPIRATION_IN_SECONDS).
63+
64+
expirable_token(Seconds) ->
65+
TokenPayload = fixture_token(),
66+
%% expiration is a timestamp with precision in seconds
67+
TokenPayload#{<<"exp">> := os:system_time(seconds) + Seconds}.
68+
69+
expirable_token_with_expiration_time(ExpiresIn) ->
70+
TokenPayload = fixture_token(),
71+
%% expiration is a timestamp with precision in seconds
72+
TokenPayload#{<<"exp">> := ExpiresIn}.
73+
74+
expired_token() ->
75+
expired_token_with_scopes(full_permission_scopes()).
76+
77+
expired_token_with_scopes(Scopes) ->
78+
token_with_scopes_and_expiration(Scopes, seconds_in_the_past(10)).
79+
80+
fixture_token_with_scopes(Scopes) ->
81+
token_with_scopes_and_expiration(Scopes, default_expiration_moment()).
82+
83+
token_with_scopes_and_expiration(Scopes, Expiration) ->
84+
%% expiration is a timestamp with precision in seconds
85+
#{<<"exp">> => Expiration,
86+
<<"iss">> => <<"unit_test">>,
87+
<<"foo">> => <<"bar">>,
88+
<<"aud">> => [<<"rabbitmq">>],
89+
<<"scope">> => Scopes}.
90+
91+
token_without_scopes() ->
92+
%% expiration is a timestamp with precision in seconds
93+
#{
94+
<<"iss">> => <<"unit_test">>,
95+
<<"foo">> => <<"bar">>,
96+
<<"aud">> => [<<"rabbitmq">>]
97+
}.
98+
99+
fixture_token() ->
100+
fixture_token([]).
101+
102+
token_with_sub(TokenFixture, Sub) ->
103+
maps:put(<<"sub">>, Sub, TokenFixture).
104+
token_with_scopes(TokenFixture, Scopes) ->
105+
maps:put(<<"scope">>, Scopes, TokenFixture).
106+
107+
fixture_token(ExtraScopes) ->
108+
Scopes = [<<"rabbitmq.configure:vhost/foo">>,
109+
<<"rabbitmq.write:vhost/foo">>,
110+
<<"rabbitmq.read:vhost/foo">>,
111+
<<"rabbitmq.read:vhost/bar">>,
112+
<<"rabbitmq.read:vhost/bar/%23%2Ffoo">>] ++ ExtraScopes,
113+
fixture_token_with_scopes(Scopes).
114+
115+
fixture_token_with_full_permissions() ->
116+
fixture_token_with_scopes(full_permission_scopes()).
117+
118+
plain_token_without_scopes_and_aud() ->
119+
%% expiration is a timestamp with precision in seconds
120+
#{<<"exp">> => default_expiration_moment(),
121+
<<"iss">> => <<"unit_test">>,
122+
<<"foo">> => <<"bar">>}.
123+
124+
token_with_scope_alias_in_scope_field(Value) ->
125+
%% expiration is a timestamp with precision in seconds
126+
#{<<"exp">> => default_expiration_moment(),
127+
<<"iss">> => <<"unit_test">>,
128+
<<"foo">> => <<"bar">>,
129+
<<"aud">> => [<<"rabbitmq">>],
130+
<<"scope">> => Value}.
131+
132+
token_with_scope_alias_in_claim_field(Claims, Scopes) ->
133+
%% expiration is a timestamp with precision in seconds
134+
#{<<"exp">> => default_expiration_moment(),
135+
<<"iss">> => <<"unit_test">>,
136+
<<"foo">> => <<"bar">>,
137+
<<"aud">> => [<<"rabbitmq">>],
138+
<<"scope">> => Scopes,
139+
<<"claims">> => Claims}.
140+
141+
seconds_in_the_future() ->
142+
seconds_in_the_future(30).
143+
144+
seconds_in_the_future(N) ->
145+
os:system_time(seconds) + N.
146+
147+
seconds_in_the_past() ->
148+
seconds_in_the_past(10).
149+
150+
seconds_in_the_past(N) ->
151+
os:system_time(seconds) - N.
152+
153+
default_expiration_moment() ->
154+
seconds_in_the_future(30).

0 commit comments

Comments
 (0)