Merge pull request #7730 from savonarola/jwt-authz
feat(emqx_auth_jwt): use JWT for ACL checks
This commit is contained in:
commit
9f35dd7f80
|
@ -1654,14 +1654,14 @@ do_authenticate(
|
|||
) ->
|
||||
Properties = #{'Authentication-Method' => AuthMethod},
|
||||
case emqx_access_control:authenticate(Credential) of
|
||||
{ok, Result} ->
|
||||
{ok, AuthResult} ->
|
||||
{ok, Properties, Channel#channel{
|
||||
clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)},
|
||||
clientinfo = merge_auth_result(ClientInfo, AuthResult),
|
||||
auth_cache = #{}
|
||||
}};
|
||||
{ok, Result, AuthData} ->
|
||||
{ok, AuthResult, AuthData} ->
|
||||
{ok, Properties#{'Authentication-Data' => AuthData}, Channel#channel{
|
||||
clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)},
|
||||
clientinfo = merge_auth_result(ClientInfo, AuthResult),
|
||||
auth_cache = #{}
|
||||
}};
|
||||
{continue, AuthCache} ->
|
||||
|
@ -1675,12 +1675,16 @@ do_authenticate(
|
|||
end;
|
||||
do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
|
||||
case emqx_access_control:authenticate(Credential) of
|
||||
{ok, #{is_superuser := IsSuperuser}} ->
|
||||
{ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}};
|
||||
{ok, AuthResult} ->
|
||||
{ok, #{}, Channel#channel{clientinfo = merge_auth_result(ClientInfo, AuthResult)}};
|
||||
{error, Reason} ->
|
||||
{error, emqx_reason_codes:connack_error(Reason)}
|
||||
end.
|
||||
|
||||
merge_auth_result(ClientInfo, AuthResult) when is_map(ClientInfo) andalso is_map(AuthResult) ->
|
||||
IsSuperuser = maps:get(is_superuser, AuthResult, false),
|
||||
maps:merge(ClientInfo, AuthResult#{is_superuser => IsSuperuser}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Process Topic Alias
|
||||
|
||||
|
|
|
@ -376,7 +376,7 @@ do_verify(JWS, [JWK | More], VerifyClaims) ->
|
|||
Claims = emqx_json:decode(Payload, [return_maps]),
|
||||
case verify_claims(Claims, VerifyClaims) of
|
||||
ok ->
|
||||
{ok, emqx_authn_utils:is_superuser(Claims)};
|
||||
{ok, maps:put(jwt, Claims, emqx_authn_utils:is_superuser(Claims))};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
|
@ -393,13 +393,13 @@ verify_claims(Claims, VerifyClaims0) ->
|
|||
VerifyClaims =
|
||||
[
|
||||
{<<"exp">>, fun(ExpireTime) ->
|
||||
Now < ExpireTime
|
||||
is_integer(ExpireTime) andalso Now < ExpireTime
|
||||
end},
|
||||
{<<"iat">>, fun(IssueAt) ->
|
||||
IssueAt =< Now
|
||||
is_integer(IssueAt) andalso IssueAt =< Now
|
||||
end},
|
||||
{<<"nbf">>, fun(NotBefore) ->
|
||||
NotBefore =< Now
|
||||
is_integer(NotBefore) andalso NotBefore =< Now
|
||||
end}
|
||||
] ++ VerifyClaims0,
|
||||
do_verify_claims(Claims, VerifyClaims).
|
||||
|
|
|
@ -70,7 +70,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
username => <<"myuser">>,
|
||||
password => JWS
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||
|
||||
Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
|
||||
JWS1 = generate_jws('hmac-based', Payload1, Secret),
|
||||
|
@ -78,7 +78,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
username => <<"myuser">>,
|
||||
password => JWS1
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)),
|
||||
?assertMatch({ok, #{is_superuser := true}}, emqx_authn_jwt:authenticate(Credential1, State)),
|
||||
|
||||
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
||||
Credential2 = Credential#{password => BadJWS},
|
||||
|
@ -90,7 +90,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
secret_base64_encoded => true
|
||||
},
|
||||
{ok, State2} = emqx_authn_jwt:update(Config2, State),
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
||||
|
||||
%% invalid secret
|
||||
BadConfig = Config#{
|
||||
|
@ -101,7 +101,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
|
||||
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
|
||||
{ok, State3} = emqx_authn_jwt:update(Config3, State2),
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State3)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State3)),
|
||||
?assertEqual(
|
||||
{error, bad_username_or_password},
|
||||
emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)
|
||||
|
@ -124,7 +124,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
},
|
||||
JWS4 = generate_jws('hmac-based', Payload4, Secret),
|
||||
Credential4 = Credential#{password => JWS4},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential4, State3)),
|
||||
|
||||
%% Issued At
|
||||
Payload5 = #{
|
||||
|
@ -133,7 +133,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
},
|
||||
JWS5 = generate_jws('hmac-based', Payload5, Secret),
|
||||
Credential5 = Credential#{password => JWS5},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential5, State3)),
|
||||
|
||||
Payload6 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
|
@ -152,7 +152,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
},
|
||||
JWS7 = generate_jws('hmac-based', Payload7, Secret),
|
||||
Credential7 = Credential6#{password => JWS7},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential7, State3)),
|
||||
|
||||
Payload8 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
|
@ -185,7 +185,7 @@ t_jwt_authenticator_public_key(_) ->
|
|||
username => <<"myuser">>,
|
||||
password => JWS
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||
?assertEqual(
|
||||
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)
|
||||
),
|
||||
|
@ -280,7 +280,7 @@ t_jwks_renewal(_Config) ->
|
|||
|
||||
ok = snabbkaffe:stop(),
|
||||
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential1, State2)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential1, State2)),
|
||||
?assertEqual(
|
||||
{error, bad_username_or_password},
|
||||
emqx_authn_jwt:authenticate(Credential1#{password => JWS2}, State2)
|
||||
|
@ -307,7 +307,7 @@ t_jwt_authenticator_verify_claims(_) ->
|
|||
username => <<"myuser">>,
|
||||
password => JWS0
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential0, State0)),
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential0, State0)),
|
||||
|
||||
Config1 = Config0#{
|
||||
verify_claims => [{<<"foo">>, <<"${username}">>}]
|
||||
|
@ -340,7 +340,7 @@ t_jwt_authenticator_verify_claims(_) ->
|
|||
username => <<"myuser">>,
|
||||
password => JWS3
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential3, State1)).
|
||||
?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential3, State1)).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
|
|
@ -340,6 +340,28 @@ Commands can support following wildcards:\n
|
|||
}
|
||||
}
|
||||
|
||||
jwt {
|
||||
desc {
|
||||
en: """Authorization using ACL rules from authentication JWT."""
|
||||
zh: """Authorization using ACL rules from authentication JWT."""
|
||||
}
|
||||
label {
|
||||
en: """jwt"""
|
||||
zh: """jwt"""
|
||||
}
|
||||
}
|
||||
|
||||
acl_claim_name {
|
||||
desc {
|
||||
en: """JWT claim name to use for getting ACL rules."""
|
||||
zh: """JWT claim name to use for getting ACL rules."""
|
||||
}
|
||||
label {
|
||||
en: """acl_claim_name"""
|
||||
zh: """acl_claim_name"""
|
||||
}
|
||||
}
|
||||
|
||||
cmd {
|
||||
desc {
|
||||
en: """Database query used to retrieve authorization data."""
|
||||
|
|
|
@ -384,6 +384,8 @@ type(postgresql) -> postgresql;
|
|||
type(<<"postgresql">>) -> postgresql;
|
||||
type(built_in_database) -> built_in_database;
|
||||
type(<<"built_in_database">>) -> built_in_database;
|
||||
type(jwt) -> jwt;
|
||||
type(<<"jwt">>) -> jwt;
|
||||
%% should never happen if the input is type-checked by hocon schema
|
||||
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
|
||||
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authz_jwt).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-behaviour(emqx_authz).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
%% APIs
|
||||
-export([
|
||||
description/0,
|
||||
init/1,
|
||||
destroy/1,
|
||||
authorize/4
|
||||
]).
|
||||
|
||||
-define(JWT_RULE_NAMES, [
|
||||
{<<"pub">>, publish},
|
||||
{<<"sub">>, subscribe},
|
||||
{<<"all">>, all}
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% emqx_authz callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
description() ->
|
||||
"AuthZ with JWT".
|
||||
|
||||
init(#{acl_claim_name := _AclClaimName} = Source) ->
|
||||
Source.
|
||||
|
||||
destroy(_Source) -> ok.
|
||||
|
||||
authorize(#{jwt := JWT} = Client, PubSub, Topic, #{acl_claim_name := AclClaimName}) ->
|
||||
case verify(JWT) of
|
||||
{ok, #{AclClaimName := Rules}} when is_map(Rules) ->
|
||||
do_authorize(Client, PubSub, Topic, Rules);
|
||||
_ ->
|
||||
{matched, deny}
|
||||
end;
|
||||
authorize(_Client, _PubSub, _Topic, _Source) ->
|
||||
nomatch.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
verify(JWT) ->
|
||||
Now = erlang:system_time(second),
|
||||
VerifyClaims =
|
||||
[
|
||||
{<<"exp">>, fun(ExpireTime) ->
|
||||
is_integer(ExpireTime) andalso Now < ExpireTime
|
||||
end},
|
||||
{<<"iat">>, fun(IssueAt) ->
|
||||
is_integer(IssueAt) andalso IssueAt =< Now
|
||||
end},
|
||||
{<<"nbf">>, fun(NotBefore) ->
|
||||
is_integer(NotBefore) andalso NotBefore =< Now
|
||||
end}
|
||||
],
|
||||
IsValid = lists:all(
|
||||
fun({ClaimName, Validator}) ->
|
||||
(not maps:is_key(ClaimName, JWT)) orelse
|
||||
Validator(maps:get(ClaimName, JWT))
|
||||
end,
|
||||
VerifyClaims
|
||||
),
|
||||
case IsValid of
|
||||
true -> {ok, JWT};
|
||||
false -> error
|
||||
end.
|
||||
|
||||
do_authorize(Client, PubSub, Topic, AclRules) ->
|
||||
do_authorize(Client, PubSub, Topic, AclRules, ?JWT_RULE_NAMES).
|
||||
|
||||
do_authorize(_Client, _PubSub, _Topic, _AclRules, []) ->
|
||||
{matched, deny};
|
||||
do_authorize(Client, PubSub, Topic, AclRules, [{Key, Action} | JWTRuleNames]) ->
|
||||
TopicFilters = maps:get(Key, AclRules, []),
|
||||
case
|
||||
emqx_authz_rule:match(
|
||||
Client,
|
||||
PubSub,
|
||||
Topic,
|
||||
emqx_authz_rule:compile({allow, all, Action, TopicFilters})
|
||||
)
|
||||
of
|
||||
{matched, Permission} -> {matched, Permission};
|
||||
nomatch -> do_authorize(Client, PubSub, Topic, AclRules, JWTRuleNames)
|
||||
end.
|
|
@ -67,7 +67,8 @@ fields("authorization") ->
|
|||
hoconsc:ref(?MODULE, postgresql),
|
||||
hoconsc:ref(?MODULE, redis_single),
|
||||
hoconsc:ref(?MODULE, redis_sentinel),
|
||||
hoconsc:ref(?MODULE, redis_cluster)
|
||||
hoconsc:ref(?MODULE, redis_cluster),
|
||||
hoconsc:ref(?MODULE, jwt)
|
||||
]
|
||||
),
|
||||
default => [],
|
||||
|
@ -124,7 +125,16 @@ fields(redis_sentinel) ->
|
|||
fields(redis_cluster) ->
|
||||
authz_common_fields(redis) ++
|
||||
connector_fields(redis, cluster) ++
|
||||
[{cmd, cmd()}].
|
||||
[{cmd, cmd()}];
|
||||
fields(jwt) ->
|
||||
authz_common_fields(jwt) ++
|
||||
[
|
||||
{acl_claim_name, #{
|
||||
type => binary(),
|
||||
default => <<"acl">>,
|
||||
desc => ?DESC(acl_claim_name)
|
||||
}}
|
||||
].
|
||||
|
||||
desc(?CONF_NS) ->
|
||||
?DESC(?CONF_NS);
|
||||
|
@ -152,6 +162,8 @@ desc(redis_sentinel) ->
|
|||
?DESC(redis_sentinel);
|
||||
desc(redis_cluster) ->
|
||||
?DESC(redis_cluster);
|
||||
desc(jwt) ->
|
||||
?DESC(jwt);
|
||||
desc(_) ->
|
||||
undefined.
|
||||
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authz_jwt_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||
-include_lib("emqx_authn/include/emqx_authn.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-define(SECRET, <<"some_secret">>).
|
||||
-define(AUTHN_PATH, [authentication]).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_common_test_helpers:start_apps(
|
||||
[emqx_conf, emqx_authn, emqx_authz],
|
||||
fun set_special_configs/1
|
||||
),
|
||||
ok = emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_authz, emqx_conf]).
|
||||
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
?AUTHN_PATH,
|
||||
?GLOBAL
|
||||
),
|
||||
AuthConfig = authn_config(),
|
||||
{ok, _} = emqx:update_config(
|
||||
?AUTHN_PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
ok = emqx_authz_test_lib:reset_authorizers(),
|
||||
{ok, _} = emqx_authz:update(replace, [authz_config()]),
|
||||
Config.
|
||||
|
||||
end_per_testcase(_TestCase, _Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
?AUTHN_PATH,
|
||||
?GLOBAL
|
||||
),
|
||||
ok = emqx_authz_test_lib:restore_authorizers().
|
||||
|
||||
set_special_configs(emqx_authz) ->
|
||||
ok = emqx_authz_test_lib:reset_authorizers();
|
||||
set_special_configs(_) ->
|
||||
ok.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Tests
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_topic_rules(_Config) ->
|
||||
Payload = #{
|
||||
<<"exp">> => erlang:system_time(second) + 60,
|
||||
<<"acl">> => #{
|
||||
<<"pub">> => [
|
||||
<<"eq testpub1/${username}">>,
|
||||
<<"testpub2/${clientid}">>,
|
||||
<<"testpub3/#">>
|
||||
],
|
||||
<<"sub">> => [
|
||||
<<"eq testsub1/${username}">>,
|
||||
<<"testsub2/${clientid}">>,
|
||||
<<"testsub3/#">>
|
||||
],
|
||||
<<"all">> => [
|
||||
<<"eq testall1/${username}">>,
|
||||
<<"testall2/${clientid}">>,
|
||||
<<"testall3/#">>
|
||||
]
|
||||
},
|
||||
<<"username">> => <<"username">>
|
||||
},
|
||||
JWT = generate_jws(Payload),
|
||||
|
||||
{ok, C} = emqtt:start_link(
|
||||
[
|
||||
{clean_start, true},
|
||||
{proto_ver, v5},
|
||||
{clientid, <<"clientid">>},
|
||||
{username, <<"username">>},
|
||||
{password, JWT}
|
||||
]
|
||||
),
|
||||
{ok, _} = emqtt:connect(C),
|
||||
|
||||
Cases = [
|
||||
{deny, <<"testpub1/username">>},
|
||||
{deny, <<"testpub2/clientid">>},
|
||||
{deny, <<"testpub3/foobar">>},
|
||||
|
||||
{deny, <<"testsub1/username">>},
|
||||
{allow, <<"testsub1/${username}">>},
|
||||
{allow, <<"testsub2/clientid">>},
|
||||
{allow, <<"testsub3/foobar">>},
|
||||
{allow, <<"testsub3/+/foobar">>},
|
||||
{allow, <<"testsub3/#">>},
|
||||
|
||||
{deny, <<"testsub2/username">>},
|
||||
{deny, <<"testsub1/clientid">>},
|
||||
{deny, <<"testsub4/foobar">>},
|
||||
|
||||
{deny, <<"testall1/username">>},
|
||||
{allow, <<"testall1/${username}">>},
|
||||
{allow, <<"testall2/clientid">>},
|
||||
{allow, <<"testall3/foobar">>},
|
||||
{allow, <<"testall3/+/foobar">>},
|
||||
{allow, <<"testall3/#">>},
|
||||
|
||||
{deny, <<"testall2/username">>},
|
||||
{deny, <<"testall1/clientid">>},
|
||||
{deny, <<"testall4/foobar">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun
|
||||
({allow, Topic}) ->
|
||||
?assertMatch(
|
||||
{ok, #{}, [0]},
|
||||
emqtt:subscribe(C, Topic, 0)
|
||||
);
|
||||
({deny, Topic}) ->
|
||||
?assertMatch(
|
||||
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
||||
emqtt:subscribe(C, Topic, 0)
|
||||
)
|
||||
end,
|
||||
Cases
|
||||
),
|
||||
|
||||
ok = emqtt:disconnect(C).
|
||||
|
||||
t_check_pub(_Config) ->
|
||||
Payload = #{
|
||||
<<"username">> => <<"username">>,
|
||||
<<"acl">> => #{<<"sub">> => [<<"a/b">>]},
|
||||
<<"exp">> => erlang:system_time(second) + 10
|
||||
},
|
||||
|
||||
JWT = generate_jws(Payload),
|
||||
|
||||
{ok, C} = emqtt:start_link(
|
||||
[
|
||||
{clean_start, true},
|
||||
{proto_ver, v5},
|
||||
{clientid, <<"clientid">>},
|
||||
{username, <<"username">>},
|
||||
{password, JWT}
|
||||
]
|
||||
),
|
||||
{ok, _} = emqtt:connect(C),
|
||||
|
||||
ok = emqtt:publish(C, <<"a/b">>, <<"hi">>, 0),
|
||||
|
||||
receive
|
||||
{publish, #{topic := <<"a/b">>}} ->
|
||||
?assert(false, "Publish to `a/b` should not be allowed")
|
||||
after 100 -> ok
|
||||
end,
|
||||
|
||||
ok = emqtt:disconnect(C).
|
||||
|
||||
t_check_no_recs(_Config) ->
|
||||
Payload = #{
|
||||
<<"username">> => <<"username">>,
|
||||
<<"acl">> => #{},
|
||||
<<"exp">> => erlang:system_time(second) + 10
|
||||
},
|
||||
|
||||
JWT = generate_jws(Payload),
|
||||
|
||||
{ok, C} = emqtt:start_link(
|
||||
[
|
||||
{clean_start, true},
|
||||
{proto_ver, v5},
|
||||
{clientid, <<"clientid">>},
|
||||
{username, <<"username">>},
|
||||
{password, JWT}
|
||||
]
|
||||
),
|
||||
{ok, _} = emqtt:connect(C),
|
||||
|
||||
?assertMatch(
|
||||
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
||||
emqtt:subscribe(C, <<"a/b">>, 0)
|
||||
),
|
||||
|
||||
ok = emqtt:disconnect(C).
|
||||
|
||||
t_check_no_acl_claim(_Config) ->
|
||||
Payload = #{
|
||||
<<"username">> => <<"username">>,
|
||||
<<"exp">> => erlang:system_time(second) + 10
|
||||
},
|
||||
|
||||
JWT = generate_jws(Payload),
|
||||
|
||||
{ok, C} = emqtt:start_link(
|
||||
[
|
||||
{clean_start, true},
|
||||
{proto_ver, v5},
|
||||
{clientid, <<"clientid">>},
|
||||
{username, <<"username">>},
|
||||
{password, JWT}
|
||||
]
|
||||
),
|
||||
{ok, _} = emqtt:connect(C),
|
||||
|
||||
?assertMatch(
|
||||
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
||||
emqtt:subscribe(C, <<"a/b">>, 0)
|
||||
),
|
||||
|
||||
ok = emqtt:disconnect(C).
|
||||
|
||||
t_check_expire(_Config) ->
|
||||
Payload = #{
|
||||
<<"username">> => <<"username">>,
|
||||
<<"acl">> => #{<<"sub">> => [<<"a/b">>]},
|
||||
<<"exp">> => erlang:system_time(second) + 1
|
||||
},
|
||||
|
||||
JWT = generate_jws(Payload),
|
||||
|
||||
{ok, C} = emqtt:start_link(
|
||||
[
|
||||
{clean_start, true},
|
||||
{proto_ver, v5},
|
||||
{clientid, <<"clientid">>},
|
||||
{username, <<"username">>},
|
||||
{password, JWT}
|
||||
]
|
||||
),
|
||||
{ok, _} = emqtt:connect(C),
|
||||
?assertMatch(
|
||||
{ok, #{}, [0]},
|
||||
emqtt:subscribe(C, <<"a/b">>, 0)
|
||||
),
|
||||
|
||||
?assertMatch(
|
||||
{ok, #{}, [0]},
|
||||
emqtt:unsubscribe(C, <<"a/b">>)
|
||||
),
|
||||
|
||||
timer:sleep(2000),
|
||||
|
||||
?assertMatch(
|
||||
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
||||
emqtt:subscribe(C, <<"a/b">>, 0)
|
||||
),
|
||||
|
||||
ok = emqtt:disconnect(C).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
authn_config() ->
|
||||
#{
|
||||
<<"mechanism">> => <<"jwt">>,
|
||||
<<"use_jwks">> => <<"false">>,
|
||||
<<"algorithm">> => <<"hmac-based">>,
|
||||
<<"secret">> => ?SECRET,
|
||||
<<"secret_base64_encoded">> => <<"false">>,
|
||||
<<"verify_claims">> => #{
|
||||
<<"username">> => ?PH_USERNAME
|
||||
}
|
||||
}.
|
||||
|
||||
authz_config() ->
|
||||
#{
|
||||
<<"type">> => <<"jwt">>,
|
||||
<<"acl_claim_name">> => <<"acl">>
|
||||
}.
|
||||
|
||||
generate_jws(Payload) ->
|
||||
JWK = jose_jwk:from_oct(?SECRET),
|
||||
Header = #{
|
||||
<<"alg">> => <<"HS256">>,
|
||||
<<"typ">> => <<"JWT">>
|
||||
},
|
||||
Signed = jose_jwt:sign(JWK, Header, Payload),
|
||||
{_, JWS} = jose_jws:compact(Signed),
|
||||
JWS.
|
Loading…
Reference in New Issue