diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf
index 714a08704..e7c4890f7 100644
--- a/apps/emqx/i18n/emqx_schema_i18n.conf
+++ b/apps/emqx/i18n/emqx_schema_i18n.conf
@@ -2045,12 +2045,18 @@ Type of the rate limit.
base_listener_enable_authn {
desc {
en: """
-Set true
(default) to enable client authentication on this listener.
-When set to false
clients will be allowed to connect without authentication.
+Set true
(default) to enable client authentication on this listener, the authentication
+process goes through the configured authentication chain.
+When set to false
to allow any clients with or without authentication information such as username or password to log in.
+When set to quick_deny_anonymous, it behaves like when set to true
but clients will be
+denied immediately without going through any authenticators if username
is not provided. This is useful to fence off
+anonymous clients early.
"""
zh: """
-配置 true
(默认值)启用客户端进行身份认证。
-配置 false
时,将不对客户端做任何认证。
+配置 true
(默认值)启用客户端进行身份认证,通过检查认配置的认认证器链来决定是否允许接入。
+配置 false
时,将不对客户端做任何认证,任何客户端,不论是不是携带用户名等认证信息,都可以接入。
+配置 quick_deny_anonymous
时,行为跟 true
类似,但是会对匿名
+客户直接拒绝,不做使用任何认证器对客户端进行身份检查。
"""
}
label: {
diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl
index d99699a9a..30d56f257 100644
--- a/apps/emqx/src/emqx_access_control.erl
+++ b/apps/emqx/src/emqx_access_control.erl
@@ -38,11 +38,22 @@
| {ok, map(), binary()}
| {continue, map()}
| {continue, binary(), map()}
- | {error, term()}.
+ | {error, not_authorized}.
authenticate(Credential) ->
- case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of
+ %% pre-hook quick authentication or
+ %% if auth backend returning nothing but just 'ok'
+ %% it means it's not a superuser, or there is no way to tell.
+ NotSuperUser = #{is_superuser => false},
+ case emqx_authentication:pre_hook_authenticate(Credential) of
ok ->
- {ok, #{is_superuser => false}};
+ {ok, NotSuperUser};
+ continue ->
+ case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of
+ ok ->
+ {ok, NotSuperUser};
+ Other ->
+ Other
+ end;
Other ->
Other
end.
diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl
index 964a97dfb..749f5bfd7 100644
--- a/apps/emqx/src/emqx_authentication.erl
+++ b/apps/emqx/src/emqx_authentication.erl
@@ -29,9 +29,13 @@
-include_lib("stdlib/include/ms_transform.hrl").
-define(CONF_ROOT, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
+-define(IS_UNDEFINED(X), (X =:= undefined orelse X =:= <<>>)).
%% The authentication entrypoint.
--export([authenticate/2]).
+-export([
+ pre_hook_authenticate/1,
+ authenticate/2
+]).
%% Authenticator manager process start/stop
-export([
@@ -221,10 +225,23 @@ when
%%------------------------------------------------------------------------------
%% Authenticate
%%------------------------------------------------------------------------------
-
-authenticate(#{enable_authn := false}, _AuthResult) ->
+-spec pre_hook_authenticate(emqx_types:clientinfo()) ->
+ ok | continue | {error, not_authorized}.
+pre_hook_authenticate(#{enable_authn := false}) ->
inc_authenticate_metric('authentication.success.anonymous'),
- ?TRACE_RESULT("authentication_result", ignore, enable_authn_false);
+ ?TRACE_RESULT("authentication_result", ok, enable_authn_false);
+pre_hook_authenticate(#{enable_authn := quick_deny_anonymous} = Credential) ->
+ case maps:get(username, Credential, undefined) of
+ U when ?IS_UNDEFINED(U) ->
+ ?TRACE_RESULT(
+ "authentication_result", {error, not_authorized}, enable_authn_false
+ );
+ _ ->
+ continue
+ end;
+pre_hook_authenticate(_) ->
+ continue.
+
authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
case get_authenticators(Listener, global_chain(Protocol)) of
{ok, ChainName, Authenticators} ->
diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl
index 1024cef48..eb5238cfe 100644
--- a/apps/emqx/src/emqx_schema.erl
+++ b/apps/emqx/src/emqx_schema.erl
@@ -1668,7 +1668,7 @@ base_listener(Bind) ->
)},
{"enable_authn",
sc(
- boolean(),
+ hoconsc:enum([true, false, quick_deny_anonymous]),
#{
desc => ?DESC(base_listener_enable_authn),
default => true
diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl
index 7b6b4f463..c079ac125 100644
--- a/apps/emqx/test/emqx_access_control_SUITE.erl
+++ b/apps/emqx/test/emqx_access_control_SUITE.erl
@@ -37,7 +37,8 @@ init_per_testcase(_, Config) ->
Config.
end_per_testcase(_, _Config) ->
- ok = emqx_hooks:del('client.authorize', {?MODULE, authz_stub}).
+ ok = emqx_hooks:del('client.authorize', {?MODULE, authz_stub}),
+ ok = emqx_hooks:del('client.authenticate', {?MODULE, quick_deny_anonymous_authn}).
t_authenticate(_) ->
?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())).
@@ -60,6 +61,37 @@ t_delayed_authorize(_) ->
?assertEqual(deny, emqx_access_control:authorize(clientinfo(), Publish2, InvalidTopic)),
ok.
+t_quick_deny_anonymous(_) ->
+ ok = emqx_hooks:put(
+ 'client.authenticate',
+ {?MODULE, quick_deny_anonymous_authn, []},
+ ?HP_AUTHN
+ ),
+
+ RawClient0 = clientinfo(),
+ RawClient = RawClient0#{username => undefined},
+
+ %% No name, No authn
+ Client1 = RawClient#{enable_authn => false},
+ ?assertMatch({ok, _}, emqx_access_control:authenticate(Client1)),
+
+ %% No name, With quick_deny_anonymous
+ Client2 = RawClient#{enable_authn => quick_deny_anonymous},
+ ?assertMatch({error, _}, emqx_access_control:authenticate(Client2)),
+
+ %% Bad name, With quick_deny_anonymous
+ Client3 = RawClient#{enable_authn => quick_deny_anonymous, username => <<"badname">>},
+ ?assertMatch({error, _}, emqx_access_control:authenticate(Client3)),
+
+ %% Good name, With quick_deny_anonymous
+ Client4 = RawClient#{enable_authn => quick_deny_anonymous, username => <<"goodname">>},
+ ?assertMatch({ok, _}, emqx_access_control:authenticate(Client4)),
+
+ %% Name, With authn
+ Client5 = RawClient#{enable_authn => true, username => <<"badname">>},
+ ?assertMatch({error, _}, emqx_access_control:authenticate(Client5)),
+ ok.
+
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
@@ -67,6 +99,11 @@ t_delayed_authorize(_) ->
authz_stub(_Client, _PubSub, ValidTopic, _DefaultResult, ValidTopic) -> {stop, #{result => allow}};
authz_stub(_Client, _PubSub, _Topic, _DefaultResult, _ValidTopic) -> {stop, #{result => deny}}.
+quick_deny_anonymous_authn(#{username := <<"badname">>}, _AuthResult) ->
+ {stop, {error, not_authorized}};
+quick_deny_anonymous_authn(_ClientInfo, _AuthResult) ->
+ {stop, {ok, #{is_superuser => false}}}.
+
clientinfo() -> clientinfo(#{}).
clientinfo(InitProps) ->
maps:merge(
diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl
index b5e051193..df3b4018e 100644
--- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl
+++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl
@@ -2319,5 +2319,4 @@ returncode_name(?SN_RC2_EXCEED_LIMITATION) -> rejected_exceed_limitation;
returncode_name(?SN_RC2_REACHED_MAX_RETRY) -> reached_max_retry_times;
returncode_name(_) -> accepted.
-name_to_returncode(not_authorized) -> ?SN_RC2_NOT_AUTHORIZE;
-name_to_returncode(_) -> ?SN_RC2_NOT_AUTHORIZE.
+name_to_returncode(not_authorized) -> ?SN_RC2_NOT_AUTHORIZE.
diff --git a/changes/v5.0.11-en.md b/changes/v5.0.11-en.md
index e53c5785e..6ad40c7fe 100644
--- a/changes/v5.0.11-en.md
+++ b/changes/v5.0.11-en.md
@@ -23,6 +23,8 @@
- Keep MQTT v5 User-Property pairs from bridge ingested MQTT messsages to bridge target [#9398](https://github.com/emqx/emqx/pull/9398).
+- Add a new config `quick_deny_anonymous` to allow quick deny of anonymous clients (without username) so the auth backend checks can be skipped [#8516](https://github.com/emqx/emqx/pull/8516).
+
## Bug fixes
- Fix `ssl.existingName` option of helm chart not working [#9307](https://github.com/emqx/emqx/issues/9307).
diff --git a/changes/v5.0.11-zh.md b/changes/v5.0.11-zh.md
index 3ea516dad..b5fe7c4bd 100644
--- a/changes/v5.0.11-zh.md
+++ b/changes/v5.0.11-zh.md
@@ -21,6 +21,8 @@
- 为桥接收到的 MQTT v5 消息再转发时保留 User-Property 列表 [#9398](https://github.com/emqx/emqx/pull/9398)。
+- 添加了一个名为 `quick_deny_anonymous` 的新配置,用来在不调用认证链的情况下,快速的拒绝掉匿名用户,从而提高认证效率 [#8516](https://github.com/emqx/emqx/pull/8516)。
+
## 修复
- 修复 helm chart 的 `ssl.existingName` 选项不起作用 [#9307](https://github.com/emqx/emqx/issues/9307)。