emqx/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl

515 lines
19 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-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_auth_jwt_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
all() -> emqx_ct:all(?MODULE).
init_per_testcase(TestCase, Config) ->
?MODULE:TestCase(init, Config),
emqx_ct_helpers:start_apps([emqx_auth_jwt], fun set_special_configs/1),
Config.
end_per_testcase(TestCase, Config) ->
try ?MODULE:TestCase('end', Config) catch _:_ -> ok end,
emqx_ct_helpers:stop_apps([emqx_auth_jwt]).
set_special_configs(emqx) ->
application:set_env(emqx, allow_anonymous, false),
application:set_env(emqx, acl_nomatch, deny),
application:set_env(emqx, enable_acl_cache, false),
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
application:set_env(emqx, plugins_loaded_file,
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)),
application:set_env(emqx, acl_file,
emqx_ct_helpers:deps_path(emqx, AclFilePath));
set_special_configs(emqx_auth_jwt) ->
application:set_env(emqx_auth_jwt, secret, "emqxsecret"),
application:set_env(emqx_auth_jwt, from, password);
set_special_configs(_) ->
ok.
sign(Payload, Header, Key) when is_map(Header) ->
Jwk = jose_jwk:from_oct(Key),
Jwt = emqx_json:encode(Payload),
{_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, Header, Jwt)),
Token;
sign(Payload, Alg, Key) ->
Jwk = jose_jwk:from_oct(Key),
Jwt = emqx_json:encode(Payload),
{_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, #{<<"alg">> => Alg}, Jwt)),
Token.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_check_auth(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_auth(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, erlang:system_time(seconds) + 2}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt]),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := #{<<"clientid">> := <<"client1">>}}}, Result0),
ct:sleep(3100),
Result1 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result after 3100ms: ~p~n", [Result1]),
?assertMatch({error, _}, Result1),
Jwt_Error = sign([{client_id, <<"client1">>},
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
ct:pal("invalid jwt: ~p~n", [Jwt_Error]),
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
?assertEqual({error, invalid_signature}, Result2),
?assertMatch({error, _}, emqx_access_control:authenticate(Plain#{password => <<"asd">>})).
t_check_nbf(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_nbf(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{nbf, erlang:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt]),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertEqual({error, {invalid_signature, not_valid_yet}}, Result0).
t_check_iat(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_iat(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{iat, erlang:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt]),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertEqual({error, {invalid_signature, issued_in_future}}, Result0).
t_check_auth_invalid_exp(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_auth_invalid_exp(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt0 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, [{foo, bar}]}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt0]),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt0}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({error, _}, Result0),
Jwt1 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, <<"foobar">>}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt1]),
Result1 = emqx_access_control:authenticate(Plain#{password => Jwt1}),
ct:pal("Auth result: ~p~n", [Result1]),
?assertMatch({error, _}, Result1).
t_check_auth_str_exp(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_auth_str_exp(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Exp = integer_to_binary(erlang:system_time(seconds) + 3),
Jwt0 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, Exp}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt0]),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt0}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
Jwt1 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, <<"0">>}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt1]),
Result1 = emqx_access_control:authenticate(Plain#{password => Jwt1}),
ct:pal("Auth result: ~p~n", [Result1]),
?assertMatch({error, _}, Result1),
Exp2 = float_to_binary(erlang:system_time(seconds) + 3.5),
Jwt2 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, Exp2}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt2]),
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt2}),
ct:pal("Auth result: ~p~n", [Result2]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result2).
t_check_auth_float_exp(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_auth_float_exp(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Exp = erlang:system_time(seconds) + 3.5,
Jwt0 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, Exp}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt0]),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt0}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
Jwt1 = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
{exp, 1.5}], <<"HS256">>, <<"emqxsecret">>),
ct:pal("Jwt: ~p~n", [Jwt1]),
Result1 = emqx_access_control:authenticate(Plain#{password => Jwt1}),
ct:pal("Auth result: ~p~n", [Result1]),
?assertMatch({error, _}, Result1).
t_check_claims(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_claims(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{exp, erlang:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
Jwt_Error = sign([{clientid, <<"client1">>},
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
?assertEqual({error, invalid_signature}, Result2).
t_check_claims_clientid(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]).
t_check_claims_clientid(_Config) ->
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client23">>},
{username, <<"plain">>},
{exp, erlang:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
Jwt_Error = sign([{clientid, <<"client1">>},
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
?assertEqual({error, invalid_signature}, Result2).
t_check_claims_username(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]).
t_check_claims_username(_Config) ->
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
Jwt = sign([{client_id, <<"client23">>},
{username, <<"plain">>},
{exp, erlang:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
Jwt_Error = sign([{clientid, <<"client1">>},
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
Result3 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]),
?assertEqual({error, invalid_signature}, Result3).
t_check_claims_kid_in_header(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, []).
t_check_claims_kid_in_header(_Config) ->
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client23">>},
{username, <<"plain">>},
{exp, erlang:system_time(seconds) + 3}],
#{<<"alg">> => <<"HS256">>,
<<"kid">> => <<"a_kid_str">>}, <<"emqxsecret">>),
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0).
t_keys_update(init, _Config) ->
ok = meck:new(httpc, [passthrough, no_history]),
ok = meck:expect(
httpc,
request,
fun(get, _, _, _) ->
{ok,
{200,
[],
jiffy:encode(#{<<"keys">> => []})}}
end),
application:set_env(emqx_auth_jwt, verify_claims, []),
application:set_env(emqx_auth_jwt, refresh_interval, 100),
application:set_env(emqx_auth_jwt, jwks, "http://localhost:4001/keys.json").
t_keys_update(_Config) ->
?check_trace(
snabbkaffe:block_until(
?match_n_events(2, #{?snk_kind := emqx_auth_jwt_svr_jwks_updated}),
_Timeout = infinity,
_BackInTIme = 0),
fun(_, Trace) ->
?assertMatch([#{pid := Pid}, #{pid := Pid} | _],
?of_kind(emqx_auth_jwt_svr_jwks_updated, Trace))
end).
t_check_jwt_acl(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, [{sub, [<<"a/b">>]},
{pub, [<<"c/d">>]},
{all, [<<"all">>]}]},
{exp, erlang:system_time(seconds) + 10}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [0]},
emqtt:subscribe(C, <<"a/b">>, 0)),
?assertMatch(
ok,
emqtt:publish(C, <<"c/d">>, <<"hi">>, 0)),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"c/d">>, 0)),
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,
%% can pub/sub to all rules
?assertMatch(
{ok, #{}, [0]},
emqtt:subscribe(C, <<"all">>, 0)),
?assertMatch(
ok,
emqtt:publish(C, <<"all">>, <<"hi">>, 0)),
receive
{publish, #{topic := <<"all">>}} -> ok
after 2000 ->
?assert(false, "Publish to `all` should be allowed")
end,
ok = emqtt:disconnect(C).
t_check_jwt_acl_no_recs(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_no_recs(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, []},
{exp, erlang:system_time(seconds) + 10}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0)),
ok = emqtt:disconnect(C).
t_check_jwt_acl_no_acl_claim(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_no_acl_claim(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{exp, erlang:system_time(seconds) + 10}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0)),
ok = emqtt:disconnect(C).
t_check_jwt_acl_no_jwt_claims_helper(_ClientInfo, _LastAuthResult) ->
{stop, #{auth_result => success, anonymous => false}}.
t_check_jwt_acl_no_jwt_claims(init, _Config) ->
ok;
t_check_jwt_acl_no_jwt_claims('end', _Config) ->
ok = emqx_hooks:del(
'client.authenticate',
{?MODULE, t_check_jwt_acl_no_jwt_claims_helper, []}
).
t_check_jwt_acl_no_jwt_claims(_Config) ->
%% bypass the jwt authentication checking
ok = emqx_hooks:add(
'client.authenticate',
{?MODULE, t_check_jwt_acl_no_jwt_claims_helper, []},
_Priority = 99999
),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{username, <<"client1">>},
{password, <<"password">>}]),
{ok, _} = emqtt:connect(C),
ok = snabbkaffe:start_trace(),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0)),
{ok, _} = ?block_until(#{?snk_kind := no_jwt_claim}, 1000),
Trace = snabbkaffe:collect_trace(),
?assertEqual(1, length(?of_kind(no_jwt_claim, Trace))),
snabbkaffe:stop(),
ok = emqtt:disconnect(C).
t_check_jwt_acl_expire(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_expire(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, [{sub, [<<"a/b">>]}]},
{exp, erlang:system_time(seconds) + 1}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{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)),
Default = emqx_zone:get_env(external, acl_nomatch, deny),
emqx_zone:set_env(external, acl_nomatch, allow),
try
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0))
after
emqx_zone:set_env(external, acl_nomatch, Default)
end,
ok = emqtt:disconnect(C).
t_check_jwt_acl_no_exp(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_no_exp(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, [{sub, [<<"a/b">>]}]}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [0]},
emqtt:subscribe(C, <<"a/b">>, 0)),
ok = emqtt:disconnect(C).
t_check_compatibility(init, _Config) -> ok.
t_check_compatibility(_Config) ->
%% We literary want emqx_auth_jwt:check call emqx_auth_jwt:check_auth, so check with meck
ok = meck:new(emqx_auth_jwt, [passthrough, no_history]),
ok = meck:expect(emqx_auth_jwt, check_auth, fun(a, b, c) -> ok end),
?assertEqual(
ok,
emqx_auth_jwt:check(a, b, c)
),
meck:validate(emqx_auth_jwt),
meck:unload(emqx_auth_jwt).