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},
|
Properties = #{'Authentication-Method' => AuthMethod},
|
||||||
case emqx_access_control:authenticate(Credential) of
|
case emqx_access_control:authenticate(Credential) of
|
||||||
{ok, Result} ->
|
{ok, AuthResult} ->
|
||||||
{ok, Properties, Channel#channel{
|
{ok, Properties, Channel#channel{
|
||||||
clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)},
|
clientinfo = merge_auth_result(ClientInfo, AuthResult),
|
||||||
auth_cache = #{}
|
auth_cache = #{}
|
||||||
}};
|
}};
|
||||||
{ok, Result, AuthData} ->
|
{ok, AuthResult, AuthData} ->
|
||||||
{ok, Properties#{'Authentication-Data' => AuthData}, Channel#channel{
|
{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 = #{}
|
auth_cache = #{}
|
||||||
}};
|
}};
|
||||||
{continue, AuthCache} ->
|
{continue, AuthCache} ->
|
||||||
|
@ -1675,12 +1675,16 @@ do_authenticate(
|
||||||
end;
|
end;
|
||||||
do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
|
do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
|
||||||
case emqx_access_control:authenticate(Credential) of
|
case emqx_access_control:authenticate(Credential) of
|
||||||
{ok, #{is_superuser := IsSuperuser}} ->
|
{ok, AuthResult} ->
|
||||||
{ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}};
|
{ok, #{}, Channel#channel{clientinfo = merge_auth_result(ClientInfo, AuthResult)}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, emqx_reason_codes:connack_error(Reason)}
|
{error, emqx_reason_codes:connack_error(Reason)}
|
||||||
end.
|
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
|
%% Process Topic Alias
|
||||||
|
|
||||||
|
|
|
@ -376,7 +376,7 @@ do_verify(JWS, [JWK | More], VerifyClaims) ->
|
||||||
Claims = emqx_json:decode(Payload, [return_maps]),
|
Claims = emqx_json:decode(Payload, [return_maps]),
|
||||||
case verify_claims(Claims, VerifyClaims) of
|
case verify_claims(Claims, VerifyClaims) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Claims)};
|
{ok, maps:put(jwt, Claims, emqx_authn_utils:is_superuser(Claims))};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
|
@ -393,13 +393,13 @@ verify_claims(Claims, VerifyClaims0) ->
|
||||||
VerifyClaims =
|
VerifyClaims =
|
||||||
[
|
[
|
||||||
{<<"exp">>, fun(ExpireTime) ->
|
{<<"exp">>, fun(ExpireTime) ->
|
||||||
Now < ExpireTime
|
is_integer(ExpireTime) andalso Now < ExpireTime
|
||||||
end},
|
end},
|
||||||
{<<"iat">>, fun(IssueAt) ->
|
{<<"iat">>, fun(IssueAt) ->
|
||||||
IssueAt =< Now
|
is_integer(IssueAt) andalso IssueAt =< Now
|
||||||
end},
|
end},
|
||||||
{<<"nbf">>, fun(NotBefore) ->
|
{<<"nbf">>, fun(NotBefore) ->
|
||||||
NotBefore =< Now
|
is_integer(NotBefore) andalso NotBefore =< Now
|
||||||
end}
|
end}
|
||||||
] ++ VerifyClaims0,
|
] ++ VerifyClaims0,
|
||||||
do_verify_claims(Claims, VerifyClaims).
|
do_verify_claims(Claims, VerifyClaims).
|
||||||
|
|
|
@ -70,7 +70,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => JWS
|
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},
|
Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
|
||||||
JWS1 = generate_jws('hmac-based', Payload1, Secret),
|
JWS1 = generate_jws('hmac-based', Payload1, Secret),
|
||||||
|
@ -78,7 +78,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => JWS1
|
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">>),
|
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
||||||
Credential2 = Credential#{password => BadJWS},
|
Credential2 = Credential#{password => BadJWS},
|
||||||
|
@ -90,7 +90,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
secret_base64_encoded => true
|
secret_base64_encoded => true
|
||||||
},
|
},
|
||||||
{ok, State2} = emqx_authn_jwt:update(Config2, State),
|
{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
|
%% invalid secret
|
||||||
BadConfig = Config#{
|
BadConfig = Config#{
|
||||||
|
@ -101,7 +101,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
|
|
||||||
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
|
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
|
||||||
{ok, State3} = emqx_authn_jwt:update(Config3, State2),
|
{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(
|
?assertEqual(
|
||||||
{error, bad_username_or_password},
|
{error, bad_username_or_password},
|
||||||
emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)
|
emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)
|
||||||
|
@ -124,7 +124,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
},
|
},
|
||||||
JWS4 = generate_jws('hmac-based', Payload4, Secret),
|
JWS4 = generate_jws('hmac-based', Payload4, Secret),
|
||||||
Credential4 = Credential#{password => JWS4},
|
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
|
%% Issued At
|
||||||
Payload5 = #{
|
Payload5 = #{
|
||||||
|
@ -133,7 +133,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
},
|
},
|
||||||
JWS5 = generate_jws('hmac-based', Payload5, Secret),
|
JWS5 = generate_jws('hmac-based', Payload5, Secret),
|
||||||
Credential5 = Credential#{password => JWS5},
|
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 = #{
|
Payload6 = #{
|
||||||
<<"username">> => <<"myuser">>,
|
<<"username">> => <<"myuser">>,
|
||||||
|
@ -152,7 +152,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
},
|
},
|
||||||
JWS7 = generate_jws('hmac-based', Payload7, Secret),
|
JWS7 = generate_jws('hmac-based', Payload7, Secret),
|
||||||
Credential7 = Credential6#{password => JWS7},
|
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 = #{
|
Payload8 = #{
|
||||||
<<"username">> => <<"myuser">>,
|
<<"username">> => <<"myuser">>,
|
||||||
|
@ -185,7 +185,7 @@ t_jwt_authenticator_public_key(_) ->
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => JWS
|
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(
|
?assertEqual(
|
||||||
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)
|
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)
|
||||||
),
|
),
|
||||||
|
@ -280,7 +280,7 @@ t_jwks_renewal(_Config) ->
|
||||||
|
|
||||||
ok = snabbkaffe:stop(),
|
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(
|
?assertEqual(
|
||||||
{error, bad_username_or_password},
|
{error, bad_username_or_password},
|
||||||
emqx_authn_jwt:authenticate(Credential1#{password => JWS2}, State2)
|
emqx_authn_jwt:authenticate(Credential1#{password => JWS2}, State2)
|
||||||
|
@ -307,7 +307,7 @@ t_jwt_authenticator_verify_claims(_) ->
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => JWS0
|
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#{
|
Config1 = Config0#{
|
||||||
verify_claims => [{<<"foo">>, <<"${username}">>}]
|
verify_claims => [{<<"foo">>, <<"${username}">>}]
|
||||||
|
@ -340,7 +340,7 @@ t_jwt_authenticator_verify_claims(_) ->
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => JWS3
|
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
|
%% 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 {
|
cmd {
|
||||||
desc {
|
desc {
|
||||||
en: """Database query used to retrieve authorization data."""
|
en: """Database query used to retrieve authorization data."""
|
||||||
|
|
|
@ -384,6 +384,8 @@ type(postgresql) -> postgresql;
|
||||||
type(<<"postgresql">>) -> postgresql;
|
type(<<"postgresql">>) -> postgresql;
|
||||||
type(built_in_database) -> built_in_database;
|
type(built_in_database) -> built_in_database;
|
||||||
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
|
%% should never happen if the input is type-checked by hocon schema
|
||||||
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
|
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, postgresql),
|
||||||
hoconsc:ref(?MODULE, redis_single),
|
hoconsc:ref(?MODULE, redis_single),
|
||||||
hoconsc:ref(?MODULE, redis_sentinel),
|
hoconsc:ref(?MODULE, redis_sentinel),
|
||||||
hoconsc:ref(?MODULE, redis_cluster)
|
hoconsc:ref(?MODULE, redis_cluster),
|
||||||
|
hoconsc:ref(?MODULE, jwt)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
default => [],
|
default => [],
|
||||||
|
@ -124,7 +125,16 @@ fields(redis_sentinel) ->
|
||||||
fields(redis_cluster) ->
|
fields(redis_cluster) ->
|
||||||
authz_common_fields(redis) ++
|
authz_common_fields(redis) ++
|
||||||
connector_fields(redis, cluster) ++
|
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) ->
|
||||||
?DESC(?CONF_NS);
|
?DESC(?CONF_NS);
|
||||||
|
@ -152,6 +162,8 @@ desc(redis_sentinel) ->
|
||||||
?DESC(redis_sentinel);
|
?DESC(redis_sentinel);
|
||||||
desc(redis_cluster) ->
|
desc(redis_cluster) ->
|
||||||
?DESC(redis_cluster);
|
?DESC(redis_cluster);
|
||||||
|
desc(jwt) ->
|
||||||
|
?DESC(jwt);
|
||||||
desc(_) ->
|
desc(_) ->
|
||||||
undefined.
|
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