Parcourir la source

refactor(authn): refactor to support global and listener authentication

zhouzb il y a 4 ans
Parent
commit
e998770f2e
33 fichiers modifiés avec 2051 ajouts et 1797 suppressions
  1. 16 0
      apps/emqx/include/emqx.hrl
  2. 731 0
      apps/emqx/src/emqx_authentication.erl
  3. 9 1
      apps/emqx/src/emqx_broker_sup.erl
  4. 2 0
      apps/emqx/src/emqx_config_handler.erl
  5. 4 0
      apps/emqx/src/emqx_listeners.erl
  6. 0 2
      apps/emqx/src/emqx_metrics.erl
  7. 6 1
      apps/emqx/src/emqx_schema.erl
  8. 6 37
      apps/emqx_authn/etc/emqx_authn.conf
  9. 3 16
      apps/emqx_authn/include/emqx_authn.hrl
  10. 1 3
      apps/emqx_authn/rebar.config
  11. 0 637
      apps/emqx_authn/src/emqx_authn.erl
  12. 787 520
      apps/emqx_authn/src/emqx_authn_api.erl
  13. 31 20
      apps/emqx_authn/src/emqx_authn_app.erl
  14. 4 45
      apps/emqx_authn/src/emqx_authn_schema.erl
  15. 1 7
      apps/emqx_authn/src/emqx_authn_sup.erl
  16. 10 10
      apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl
  17. 33 30
      apps/emqx_authn/src/simple_authn/emqx_authn_http.erl
  18. 12 5
      apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl
  19. 14 7
      apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl
  20. 13 6
      apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl
  21. 11 6
      apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl
  22. 15 7
      apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl
  23. 14 6
      apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl
  24. 0 98
      apps/emqx_authn/test/emqx_authn_SUITE.erl
  25. 140 140
      apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl
  26. 146 146
      apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl
  27. 2 9
      apps/emqx_connector/src/emqx_connector_mongo.erl
  28. 12 2
      apps/emqx_connector/src/emqx_connector_redis.erl
  29. 7 13
      apps/emqx_gateway/etc/emqx_gateway.conf
  30. 20 20
      apps/emqx_gateway/src/emqx_gateway_schema.erl
  31. 0 1
      apps/emqx_machine/src/emqx_machine_schema.erl
  32. 0 2
      apps/emqx_retainer/src/emqx_retainer.erl
  33. 1 0
      rebar.config

+ 16 - 0
apps/emqx/include/emqx.hrl

@@ -134,3 +134,19 @@
         }).
 
 -endif.
+
+%%--------------------------------------------------------------------
+%% Authentication
+%%--------------------------------------------------------------------
+
+-record(authenticator,
+        { id :: binary()
+        , provider :: module()
+        , enable :: boolean()
+        , state :: map()
+        }).
+
+-record(chain,
+        { name :: binary()
+        , authenticators :: [#authenticator{}]
+        }).

+ 731 - 0
apps/emqx/src/emqx_authentication.erl

@@ -0,0 +1,731 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2021 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_authentication).
+
+-behaviour(gen_server).
+-behaviour(hocon_schema).
+-behaviour(emqx_config_handler).
+
+-include("emqx.hrl").
+-include("logger.hrl").
+
+-export([ roots/0
+        , fields/1
+        ]).
+
+-export([ pre_config_update/2
+        , post_config_update/4
+        ]).
+
+-export([ authenticate/2
+        ]).
+
+-export([ initialize_authentication/2 ]).
+
+-export([ start_link/0
+        , stop/0
+        ]).
+
+-export([ add_provider/2
+        , remove_provider/1
+        , create_chain/1
+        , delete_chain/1
+        , lookup_chain/1
+        , list_chains/0
+        , create_authenticator/2
+        , delete_authenticator/2
+        , update_authenticator/3
+        , lookup_authenticator/2
+        , list_authenticators/1
+        , move_authenticator/3
+        ]).
+
+-export([ import_users/3
+        , add_user/3
+        , delete_user/3
+        , update_user/4
+        , lookup_user/3
+        , list_users/2
+        ]).
+
+-export([ generate_id/1 ]).
+
+%% gen_server callbacks
+-export([ init/1
+        , handle_call/3
+        , handle_cast/2
+        , handle_info/2
+        , terminate/2
+        , code_change/3
+        ]).
+
+-define(CHAINS_TAB, emqx_authn_chains).
+
+-define(VER_1, <<"1">>).
+-define(VER_2, <<"2">>).
+
+-type config() :: #{atom() => term()}.
+-type state() :: #{atom() => term()}.
+-type extra() :: #{superuser := boolean(),
+                   atom() => term()}.
+-type user_info() :: #{user_id := binary(),
+                       atom() => term()}.
+
+-callback refs() -> [{ref, Module, Name}] when Module::module(), Name::atom().
+
+-callback create(Config)
+    -> {ok, State}
+     | {error, term()}
+    when Config::config(), State::state().
+
+-callback update(Config, State)
+    -> {ok, NewState}
+     | {error, term()}
+    when Config::config(), State::state(), NewState::state().
+
+-callback authenticate(Credential, State)
+    -> ignore
+     | {ok, Extra}
+     | {ok, Extra, AuthData}
+     | {continue, AuthCache}
+     | {continue, AuthData, AuthCache}
+     | {error, term()}
+  when Credential::map(), State::state(), Extra::extra(), AuthData::binary(), AuthCache::map().
+
+-callback destroy(State)
+    -> ok
+    when State::state().
+
+-callback import_users(Filename, State)
+    -> ok
+     | {error, term()}
+    when Filename::binary(), State::state().
+
+-callback add_user(UserInfo, State)
+    -> {ok, User}
+     | {error, term()}
+    when UserInfo::user_info(), State::state(), User::user_info().
+
+-callback delete_user(UserID, State)
+    -> ok
+     | {error, term()}
+    when UserID::binary(), State::state().
+
+-callback update_user(UserID, UserInfo, State)
+    -> {ok, User}
+     | {error, term()}
+    when UserID::binary, UserInfo::map(), State::state(), User::user_info().
+
+-callback list_users(State)
+    -> {ok, Users}
+    when State::state(), Users::[user_info()].
+
+-optional_callbacks([ import_users/2
+                    , add_user/2
+                    , delete_user/2
+                    , update_user/3
+                    , list_users/1
+                    ]).
+
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+roots() -> [{authentication, fun authentication/1}].
+
+fields(_) -> [].
+
+authentication(type) ->
+    {ok, Refs} = get_refs(),
+    hoconsc:union([hoconsc:array(hoconsc:union(Refs)) | Refs]);
+authentication(default) -> [];
+authentication(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% Callbacks of config handler
+%%------------------------------------------------------------------------------
+
+pre_config_update(UpdateReq, OldConfig) ->
+    case do_pre_config_update(UpdateReq, to_list(OldConfig)) of
+        {error, Reason} -> {error, Reason};
+        {ok, NewConfig} -> {ok, may_to_map(NewConfig)}
+    end.
+
+do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) ->
+    {ok, OldConfig ++ [Config]};
+do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
+    NewConfig = lists:filter(fun(OldConfig0) ->
+                                AuthenticatorID =/= generate_id(OldConfig0)
+                             end, OldConfig),
+    {ok, NewConfig};
+do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) ->
+    NewConfig = lists:map(fun(OldConfig0) ->
+                              case AuthenticatorID =:= generate_id(OldConfig0) of
+                                  true -> maps:merge(OldConfig0, Config);
+                                  false -> OldConfig0
+                              end
+                          end, OldConfig),
+    {ok, NewConfig};
+do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
+    case split_by_id(AuthenticatorID, OldConfig) of
+        {error, Reason} -> {error, Reason};
+        {ok, Part1, [Found | Part2]} ->
+            case Position of
+                <<"top">> ->
+                    {ok, [Found | Part1] ++ Part2};
+                <<"bottom">> ->
+                    {ok, Part1 ++ Part2 ++ [Found]};
+                <<"before:", Before/binary>> ->
+                    case split_by_id(Before, Part1 ++ Part2) of
+                        {error, Reason} ->
+                            {error, Reason};
+                        {ok, NPart1, [NFound | NPart2]} ->
+                            {ok, NPart1 ++ [Found, NFound | NPart2]}
+                    end;
+                _ ->
+                    {error, {invalid_parameter, position}}
+            end
+    end.
+
+post_config_update(UpdateReq, NewConfig, OldConfig, AppEnvs) ->
+    do_post_config_update(UpdateReq, check_config(to_list(NewConfig)), OldConfig, AppEnvs).
+
+do_post_config_update({create_authenticator, ChainName, Config}, _NewConfig, _OldConfig, _AppEnvs) ->
+    NConfig = check_config(Config),
+    _ = create_chain(ChainName),
+    create_authenticator(ChainName, NConfig);
+
+do_post_config_update({delete_authenticator, ChainName, AuthenticatorID}, _NewConfig, _OldConfig, _AppEnvs) ->
+    delete_authenticator(ChainName, AuthenticatorID);
+
+do_post_config_update({update_authenticator, ChainName, AuthenticatorID, _Config}, NewConfig, _OldConfig, _AppEnvs) ->
+    [Config] = lists:filter(fun(NewConfig0) ->
+                                AuthenticatorID =:= generate_id(NewConfig0)
+                            end, NewConfig),
+    NConfig = check_config(Config),
+    update_authenticator(ChainName, AuthenticatorID, NConfig);
+
+do_post_config_update({move_authenticator, ChainName, AuthenticatorID, Position}, _NewConfig, _OldConfig, _AppEnvs) ->
+    NPosition = case Position of
+                    <<"top">> -> top;
+                    <<"bottom">> -> bottom;
+                    <<"before:", Before/binary>> ->
+                        {before, Before}
+                end,
+    move_authenticator(ChainName, AuthenticatorID, NPosition).
+
+check_config(Config) ->
+    #{authentication := CheckedConfig} = hocon_schema:check_plain(emqx_authentication,
+        #{<<"authentication">> => Config}, #{nullable => true, atom_key => true}),
+    CheckedConfig.
+
+%%------------------------------------------------------------------------------
+%% Authenticate
+%%------------------------------------------------------------------------------
+
+authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
+    case ets:lookup(?CHAINS_TAB, Listener) of
+        [#chain{authenticators = Authenticators}] when Authenticators =/= [] ->
+            do_authenticate(Authenticators, Credential);
+        _ ->
+            case ets:lookup(?CHAINS_TAB, global_chain(Protocol)) of
+                [#chain{authenticators = Authenticators}] when Authenticators =/= [] ->
+                    do_authenticate(Authenticators, Credential);
+                _ ->
+                    ignore
+            end
+    end.
+
+do_authenticate([], _) ->
+    {stop, {error, not_authorized}};
+do_authenticate([#authenticator{provider = Provider, state = State} | More], Credential) ->
+    case Provider:authenticate(Credential, State) of
+        ignore ->
+            do_authenticate(More, Credential);
+        Result ->
+            %% {ok, Extra}
+            %% {ok, Extra, AuthData}
+            %% {continue, AuthCache}
+            %% {continue, AuthData, AuthCache}
+            %% {error, Reason}
+            {stop, Result}
+    end.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+initialize_authentication(_, []) ->
+    ok;
+initialize_authentication(ChainName, AuthenticatorsConfig) ->
+    _ = create_chain(ChainName),
+    CheckedConfig = check_config(to_list(AuthenticatorsConfig)),
+    lists:foreach(fun(AuthenticatorConfig) ->
+        case create_authenticator(ChainName, AuthenticatorConfig) of
+            {ok, _} ->
+                ok;
+            {error, Reason} ->
+                ?LOG(error, "Failed to create authenticator '~s': ~p", [generate_id(AuthenticatorConfig), Reason])
+        end
+    end, CheckedConfig).
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+    gen_server:stop(?MODULE).
+
+get_refs() ->
+    gen_server:call(?MODULE, get_refs).
+
+add_provider(AuthNType, Provider) ->
+    gen_server:call(?MODULE, {add_provider, AuthNType, Provider}).
+
+remove_provider(AuthNType) ->
+    gen_server:call(?MODULE, {remove_provider, AuthNType}).
+
+create_chain(Name) ->
+    gen_server:call(?MODULE, {create_chain, Name}).
+
+delete_chain(Name) ->
+    gen_server:call(?MODULE, {delete_chain, Name}).
+
+lookup_chain(Name) ->
+    gen_server:call(?MODULE, {lookup_chain, Name}).
+
+list_chains() ->
+    Chains = ets:tab2list(?CHAINS_TAB),
+    {ok, [serialize_chain(Chain) || Chain <- Chains]}.
+
+create_authenticator(ChainName, Config) ->
+    gen_server:call(?MODULE, {create_authenticator, ChainName, Config}).
+
+delete_authenticator(ChainName, AuthenticatorID) ->
+    gen_server:call(?MODULE, {delete_authenticator, ChainName, AuthenticatorID}).
+
+update_authenticator(ChainName, AuthenticatorID, Config) ->
+    gen_server:call(?MODULE, {update_authenticator, ChainName, AuthenticatorID, Config}).
+
+lookup_authenticator(ChainName, AuthenticatorID) ->
+    case ets:lookup(?CHAINS_TAB, ChainName) of
+        [] ->
+            {error, {not_found, {chain, ChainName}}};
+        [#chain{authenticators = Authenticators}] ->
+            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                Authenticator ->
+                    {ok, serialize_authenticator(Authenticator)}
+            end
+    end.
+
+list_authenticators(ChainName) ->
+    case ets:lookup(?CHAINS_TAB, ChainName) of
+        [] ->
+            {error, {not_found, {chain, ChainName}}};
+        [#chain{authenticators = Authenticators}] ->
+            {ok, serialize_authenticators(Authenticators)}
+    end.
+
+move_authenticator(ChainName, AuthenticatorID, Position) ->
+    gen_server:call(?MODULE, {move_authenticator, ChainName, AuthenticatorID, Position}).
+
+import_users(ChainName, AuthenticatorID, Filename) ->
+    gen_server:call(?MODULE, {import_users, ChainName, AuthenticatorID, Filename}).
+
+add_user(ChainName, AuthenticatorID, UserInfo) ->
+    gen_server:call(?MODULE, {add_user, ChainName, AuthenticatorID, UserInfo}).
+
+delete_user(ChainName, AuthenticatorID, UserID) ->
+    gen_server:call(?MODULE, {delete_user, ChainName, AuthenticatorID, UserID}).
+
+update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) ->
+    gen_server:call(?MODULE, {update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}).
+
+lookup_user(ChainName, AuthenticatorID, UserID) ->
+    gen_server:call(?MODULE, {lookup_user, ChainName, AuthenticatorID, UserID}).
+
+%% TODO: Support pagination
+list_users(ChainName, AuthenticatorID) ->
+    gen_server:call(?MODULE, {list_users, ChainName, AuthenticatorID}).
+
+generate_id(#{mechanism := Mechanism0, backend := Backend0}) ->
+    Mechanism = atom_to_binary(Mechanism0),
+    Backend = atom_to_binary(Backend0),
+    <<Mechanism/binary, ":", Backend/binary>>;
+generate_id(#{mechanism := Mechanism}) ->
+    atom_to_binary(Mechanism);
+generate_id(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}) ->
+    <<Mechanism/binary, ":", Backend/binary>>;
+generate_id(#{<<"mechanism">> := Mechanism}) ->
+    Mechanism.
+
+%%--------------------------------------------------------------------
+%% gen_server callbacks
+%%--------------------------------------------------------------------
+
+init(_Opts) ->
+    _ = ets:new(?CHAINS_TAB, [ named_table, set, public
+                             , {keypos, #chain.name}
+                             , {read_concurrency, true}]),
+    ok = emqx_config_handler:add_handler([authentication], ?MODULE),
+    ok = emqx_config_handler:add_handler([listeners, '?', '?', authentication], ?MODULE),
+    {ok, #{hooked => false, providers => #{}}}.
+
+handle_call({add_provider, AuthNType, Provider}, _From, #{providers := Providers} = State) ->
+    reply(ok, State#{providers := Providers#{AuthNType => Provider}});
+
+handle_call({remove_provider, AuthNType}, _From, #{providers := Providers} = State) ->
+    reply(ok, State#{providers := maps:remove(AuthNType, Providers)});
+
+handle_call(get_refs, _From, #{providers := Providers} = State) ->
+    Refs = lists:foldl(fun({_, Provider}, Acc) ->
+                           Acc ++ Provider:refs()
+                       end, [], maps:to_list(Providers)),
+    reply({ok, Refs}, State);
+
+handle_call({create_chain, Name}, _From, State) ->
+    case ets:member(?CHAINS_TAB, Name) of
+        true ->
+            reply({error, {already_exists, {chain, Name}}}, State);
+        false ->
+            Chain = #chain{name = Name,
+                           authenticators = []},
+            true = ets:insert(?CHAINS_TAB, Chain),
+            reply({ok, serialize_chain(Chain)}, State)
+    end;
+
+handle_call({delete_chain, Name}, _From, State) ->
+    case ets:lookup(?CHAINS_TAB, Name) of
+        [] ->
+            reply({error, {not_found, {chain, Name}}}, State);
+        [#chain{authenticators = Authenticators}] ->
+            _ = [do_delete_authenticator(Authenticator) || Authenticator <- Authenticators],
+            true = ets:delete(?CHAINS_TAB, Name),
+            reply(ok, may_unhook(State))
+    end;
+
+handle_call({lookup_chain, Name}, _From, State) ->
+    case ets:lookup(?CHAINS_TAB, Name) of
+        [] ->
+            reply({error, {not_found, {chain, Name}}}, State);
+        [Chain] ->
+            reply({ok, serialize_chain(Chain)}, State)
+    end;
+
+handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Providers} = State) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            AuthenticatorID = generate_id(Config),
+            case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of
+                true ->
+                    {error, {already_exists, {authenticator, AuthenticatorID}}};
+                false ->
+                    case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of
+                        {ok, Authenticator} ->
+                            NAuthenticators = Authenticators ++ [Authenticator],
+                            true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
+                            {ok, serialize_authenticator(Authenticator)};
+                        {error, Reason} ->
+                            {error, Reason}
+                    end
+            end
+        end,
+    Reply = update_chain(ChainName, UpdateFun),
+    reply(Reply, may_hook(State));
+
+handle_call({delete_authenticator, ChainName, AuthenticatorID}, _From, State) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            case lists:keytake(AuthenticatorID, #authenticator.id, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                {value, Authenticator, NAuthenticators} ->
+                    _ = do_delete_authenticator(Authenticator),
+                    true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
+                    ok
+            end
+        end,
+    Reply = update_chain(ChainName, UpdateFun),
+    reply(Reply, may_unhook(State));
+
+handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, State) ->
+    UpdateFun =
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                #authenticator{provider = Provider,
+                               state    = #{version := Version} = ST} = Authenticator ->
+                    case AuthenticatorID =:= generate_id(Config) of
+                        true ->
+                            Unique = <<ChainName/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
+                            case Provider:update(Config#{'_unique' => Unique}, ST) of
+                                {ok, NewST} ->
+                                    NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)},
+                                    NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
+                                    true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}),
+                                    {ok, serialize_authenticator(NewAuthenticator)};
+                                {error, Reason} ->
+                                    {error, Reason}
+                            end;
+                        false ->
+                            {error, mechanism_or_backend_change_is_not_alloed}
+                    end
+            end
+        end,
+    Reply = update_chain(ChainName, UpdateFun),
+    reply(Reply, State);
+
+handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, State) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
+                {ok, NAuthenticators} ->
+                    true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
+                    ok;
+                {error, Reason} ->
+                    {error, Reason}
+            end
+        end,
+    Reply = update_chain(ChainName, UpdateFun),
+    reply(Reply, State);
+
+handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) ->
+    Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]),
+    reply(Reply, State);
+
+handle_call({add_user, ChainName, AuthenticatorID, UserInfo}, _From, State) ->
+    Reply = call_authenticator(ChainName, AuthenticatorID, add_user, [UserInfo]),
+    reply(Reply, State);
+
+handle_call({delete_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
+    Reply = call_authenticator(ChainName, AuthenticatorID, delete_user, [UserID]),
+    reply(Reply, State);
+
+handle_call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
+    Reply = call_authenticator(ChainName, AuthenticatorID, update_user, [UserID, NewUserInfo]),
+    reply(Reply, State);
+
+handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
+    Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]),
+    reply(Reply, State);
+
+handle_call({list_users, ChainName, AuthenticatorID}, _From, State) ->
+    Reply = call_authenticator(ChainName, AuthenticatorID, list_users, []),
+    reply(Reply, State);
+
+handle_call(Req, _From, State) ->
+    ?LOG(error, "Unexpected call: ~p", [Req]),
+    {reply, ignored, State}.
+
+handle_cast(Req, State) ->
+    ?LOG(error, "Unexpected case: ~p", [Req]),
+    {noreply, State}.
+
+handle_info(Info, State) ->
+    ?LOG(error, "Unexpected info: ~p", [Info]),
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    emqx_config_handler:remove_handler([authentication]),
+    emqx_config_handler:remove_handler([listeners, '?', '?', authentication]),
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+reply(Reply, State) ->
+    {reply, Reply, State}.
+
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+split_by_id(ID, AuthenticatorsConfig) ->
+    case lists:foldl(
+             fun(C, {P1, P2, F0}) ->
+                 F = case ID =:= generate_id(C) of
+                         true -> true;
+                         false -> F0
+                     end,
+                 case F of
+                     false -> {[C | P1], P2, F};
+                     true -> {P1, [C | P2], F}
+                 end
+             end, {[], [], false}, AuthenticatorsConfig) of
+        {_, _, false} ->
+            {error, {not_found, {authenticator, ID}}};
+        {Part1, Part2, true} ->
+            {ok, lists:reverse(Part1), lists:reverse(Part2)}
+    end.
+
+global_chain(mqtt) ->
+    <<"mqtt:global">>;
+global_chain('mqtt-sn') ->
+    <<"mqtt-sn:global">>;
+global_chain(coap) ->
+    <<"coap:global">>;
+global_chain(lwm2m) ->
+    <<"lwm2m:global">>;
+global_chain(stomp) ->
+    <<"stomp:global">>;
+global_chain(_) ->
+    <<"unknown:global">>.
+
+may_hook(#{hooked := false} = State) ->
+    case lists:any(fun(#chain{authenticators = []}) -> false;
+                      (_) -> true
+                   end, ets:tab2list(?CHAINS_TAB)) of
+        true ->
+            _ = emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}),
+            State#{hooked => true};
+        false ->
+            State
+    end;
+may_hook(State) ->
+    State.
+
+may_unhook(#{hooked := true} = State) ->
+    case lists:all(fun(#chain{authenticators = []}) -> true;
+                      (_) -> false
+                   end, ets:tab2list(?CHAINS_TAB)) of
+        true ->
+            _ = emqx:unhook('client.authenticate', {emqx_authentication, authenticate, []}),
+            State#{hooked => false};
+        false ->
+            State
+    end;
+may_unhook(State) ->
+    State.
+
+do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config, Providers) ->
+    case maps:get(authn_type(Config), Providers, undefined) of
+        undefined ->
+            {error, no_available_provider};
+        Provider ->
+            Unique = <<ChainName/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
+            case Provider:create(Config#{'_unique' => Unique}) of
+                {ok, State} ->
+                    Authenticator = #authenticator{id = AuthenticatorID,
+                                                   provider = Provider,
+                                                   enable = Enable,
+                                                   state = switch_version(State)},
+                    {ok, Authenticator};
+                {error, Reason} ->
+                    {error, Reason}
+            end
+    end.
+
+do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
+    _ = Provider:destroy(State),
+    ok.
+    
+replace_authenticator(ID, Authenticator, Authenticators) ->
+    lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator).
+
+do_move_authenticator(ID, Authenticators, Position) ->
+    case lists:keytake(ID, #authenticator.id, Authenticators) of
+        false ->
+            {error, {not_found, {authenticator, ID}}};
+        {value, Authenticator, NAuthenticators} ->
+            case Position of
+                top ->
+                    {ok, [Authenticator | NAuthenticators]};
+                bottom ->
+                    {ok, NAuthenticators ++ [Authenticator]};
+                {before, ID0} ->
+                    insert(Authenticator, NAuthenticators, ID0, [])
+            end
+    end.
+
+insert(_, [], ID, _) ->
+    {error, {not_found, {authenticator, ID}}};
+insert(Authenticator, [#authenticator{id = ID} | _] = Authenticators, ID, Acc) ->
+    {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]};
+insert(Authenticator, [Authenticator0 | More], ID, Acc) ->
+    insert(Authenticator, More, ID, [Authenticator0 | Acc]).
+
+update_chain(ChainName, UpdateFun) ->
+    case ets:lookup(?CHAINS_TAB, ChainName) of
+        [] ->
+            {error, {not_found, {chain, ChainName}}};
+        [Chain] ->
+            UpdateFun(Chain)
+    end.
+
+call_authenticator(ChainName, AuthenticatorID, Func, Args) ->
+    UpdateFun =
+        fun(#chain{authenticators = Authenticators}) ->
+            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                #authenticator{provider = Provider, state = State} ->
+                    case erlang:function_exported(Provider, Func, length(Args) + 1) of
+                        true ->
+                            erlang:apply(Provider, Func, Args ++ [State]);
+                        false ->
+                            {error, unsupported_feature}
+                    end
+            end
+        end,
+    update_chain(ChainName, UpdateFun).
+
+serialize_chain(#chain{name = Name,
+                       authenticators = Authenticators}) ->
+    #{ name => Name
+     , authenticators => serialize_authenticators(Authenticators)
+     }.
+
+serialize_authenticators(Authenticators) ->
+    [serialize_authenticator(Authenticator) || Authenticator <- Authenticators].
+
+serialize_authenticator(#authenticator{id = ID,
+                                       provider = Provider,
+                                       enable = Enable,
+                                       state = State}) ->
+    #{ id => ID
+     , provider => Provider
+     , enable => Enable
+     , state => State
+     }.
+
+switch_version(State = #{version := ?VER_1}) ->
+    State#{version := ?VER_2};
+switch_version(State = #{version := ?VER_2}) ->
+    State#{version := ?VER_1};
+switch_version(State) ->
+    State#{version => ?VER_1}.
+
+authn_type(#{mechanism := Mechanism, backend := Backend}) ->
+    {Mechanism, Backend};
+authn_type(#{mechanism := Mechanism}) ->
+    Mechanism.
+
+may_to_map([L]) ->
+    L;
+may_to_map(L) ->
+    L.
+
+to_list(undefined) ->
+    [];
+to_list(M) when M =:= #{} ->
+    [];
+to_list(M) when is_map(M) ->
+    [M];
+to_list(L) when is_list(L) ->
+    L.

+ 9 - 1
apps/emqx/src/emqx_broker_sup.erl

@@ -43,6 +43,14 @@ init([]) ->
                   type => worker,
                   modules => [emqx_shared_sub]},
 
+    %% Authentication
+    AuthN = #{id => authn,
+              start => {emqx_authentication, start_link, []},
+              restart => permanent,
+              shutdown => 2000,
+              type => worker,
+              modules => [emqx_authentication]},
+
     %% Broker helper
     Helper = #{id => helper,
                start => {emqx_broker_helper, start_link, []},
@@ -51,5 +59,5 @@ init([]) ->
                type => worker,
                modules => [emqx_broker_helper]},
 
-    {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}.
+    {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, AuthN, Helper]}}.
 

+ 2 - 0
apps/emqx/src/emqx_config_handler.erl

@@ -138,6 +138,8 @@ terminate(_Reason, _State) ->
 code_change(_OldVsn, State, _Extra) ->
     {ok, State}.
 
+deep_put_handler([], Handlers, Mod) when is_map(Handlers) ->
+    {ok, Handlers#{?MOD => Mod}};
 deep_put_handler([], _Handlers, Mod) ->
     {ok, #{?MOD => Mod}};
 deep_put_handler([?WKEY | KeyPath], Handlers, Mod) ->

+ 4 - 0
apps/emqx/src/emqx_listeners.erl

@@ -252,11 +252,15 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) ->
             {ok, {skipped, quic_app_missing}}
     end.
 
+delete_authentication(Type, ListenerName, _Conf) ->
+    emqx_authentication:delete_chain(atom_to_binary(listener_id(Type, ListenerName))).
+
 %% Update the listeners at runtime
 post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) ->
     #{added := Added, removed := Removed, changed := Updated}
         = diff_listeners(NewListeners, OldListeners),
     perform_listener_changes(fun stop_listener/3, Removed),
+    perform_listener_changes(fun delete_authentication/3, Removed),
     perform_listener_changes(fun start_listener/3, Added),
     perform_listener_changes(fun restart_listener/3, Updated).
 

+ 0 - 2
apps/emqx/src/emqx_metrics.erl

@@ -22,8 +22,6 @@
 -include("logger.hrl").
 -include("types.hrl").
 -include("emqx_mqtt.hrl").
--include("emqx.hrl").
-
 
 -export([ start_link/0
         , stop/0

+ 6 - 1
apps/emqx/src/emqx_schema.erl

@@ -94,7 +94,8 @@ roots() ->
      "stats",
      "sysmon",
      "alarm",
-     "authorization"
+     "authorization",
+     {"authentication", sc(hoconsc:lazy(hoconsc:array(map())), #{})}
     ].
 
 fields("stats") ->
@@ -819,6 +820,10 @@ mqtt_listener() ->
        sc(duration(),
           #{})
       }
+    , {"authentication",
+       sc(hoconsc:lazy(hoconsc:array(map())),
+          #{})
+      }
     ].
 
 base_listener() ->

+ 6 - 37
apps/emqx_authn/etc/emqx_authn.conf

@@ -1,37 +1,6 @@
-authentication {
-    enable = false
-    authenticators = [
-        # {
-        #     name: "authenticator1"
-        #     mechanism: password-based
-        #     server_type: built-in-database
-        #     user_id_type: clientid
-        # },
-        # {
-        #     name: "authenticator2"
-        #     mechanism: password-based
-        #     server_type: mongodb
-        #     server: "127.0.0.1:27017"
-        #     database: mqtt
-        #     collection: users
-        #     selector: {
-        #         username: "${mqtt-username}"
-        #     }
-        #     password_hash_field: password_hash
-        #     salt_field: salt
-        #     password_hash_algorithm: sha256
-        #     salt_position: prefix
-        # },
-        # {
-        #     name: "authenticator 3"
-        #     mechanism: password-based
-        #     server_type: redis
-        #     server: "127.0.0.1:6379"
-        #     password: "public"
-        #     database: 0
-        #     query: "HMGET ${mqtt-username} password_hash salt"
-        #     password_hash_algorithm: sha256
-        #     salt_position: prefix
-        # }
-    ]
-}
+# authentication: {
+#     mechanism: password-based
+#     backend: built-in-database
+#     user_id_type: clientid
+# }
+

+ 3 - 16
apps/emqx_authn/include/emqx_authn.hrl

@@ -15,24 +15,11 @@
 %%--------------------------------------------------------------------
 
 -define(APP, emqx_authn).
--define(CHAIN, <<"mqtt">>).
 
--define(VER_1, <<"1">>).
--define(VER_2, <<"2">>).
+-define(AUTHN, emqx_authentication).
 
--define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
-
--record(authenticator,
-        { id :: binary()
-        , name :: binary()
-        , provider :: module()
-        , state :: map()
-        }).
+-define(GLOBAL, <<"mqtt:global">>).
 
--record(chain,
-        { id :: binary()
-        , authenticators :: [{binary(), binary(), #authenticator{}}]
-        , created_at :: integer()
-        }).
+-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
 
 -define(AUTH_SHARD, emqx_authn_shard).

+ 1 - 3
apps/emqx_authn/rebar.config

@@ -1,6 +1,4 @@
-{deps, [
-  {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
-]}.
+{deps, []}.
 
 {edoc_opts, [{preprocess, true}]}.
 {erl_opts, [warn_unused_vars,

+ 0 - 637
apps/emqx_authn/src/emqx_authn.erl

@@ -15,640 +15,3 @@
 %%--------------------------------------------------------------------
 
 -module(emqx_authn).
-
--behaviour(gen_server).
-
--behaviour(emqx_config_handler).
-
--include("emqx_authn.hrl").
--include_lib("emqx/include/logger.hrl").
-
--export([ pre_config_update/2
-        , post_config_update/4
-        , update_config/2
-        ]).
-
--export([ enable/0
-        , disable/0
-        , is_enabled/0
-        ]).
-
--export([authenticate/2]).
-
--export([ start_link/0
-        , stop/0
-        ]).
-
--export([ create_chain/1
-        , delete_chain/1
-        , lookup_chain/1
-        , list_chains/0
-        , create_authenticator/2
-        , delete_authenticator/2
-        , update_authenticator/3
-        , update_or_create_authenticator/3
-        , lookup_authenticator/2
-        , list_authenticators/1
-        , move_authenticator/3
-        ]).
-
--export([ import_users/3
-        , add_user/3
-        , delete_user/3
-        , update_user/4
-        , lookup_user/3
-        , list_users/2
-        ]).
-
-%% gen_server callbacks
--export([ init/1
-        , handle_call/3
-        , handle_cast/2
-        , handle_info/2
-        , terminate/2
-        , code_change/3
-        ]).
-
--define(CHAIN_TAB, emqx_authn_chain).
-
-%%------------------------------------------------------------------------------
-%% APIs
-%%------------------------------------------------------------------------------
-
-pre_config_update({enable, Enable}, _OldConfig) ->
-    {ok, Enable};
-pre_config_update({create_authenticator, Config}, OldConfig) ->
-    {ok, OldConfig ++ [Config]};
-pre_config_update({delete_authenticator, ID}, OldConfig) ->
-    case lookup_authenticator(?CHAIN, ID) of
-        {error, Reason} -> {error, Reason};
-        {ok, #{name := Name}} ->
-            NewConfig = lists:filter(fun(#{<<"name">> := N}) ->
-                                         N =/= Name
-                                     end, OldConfig),
-            {ok, NewConfig}
-    end;
-pre_config_update({update_authenticator, ID, Config}, OldConfig) ->
-    case lookup_authenticator(?CHAIN, ID) of
-        {error, Reason} -> {error, Reason};
-        {ok, #{name := Name}} ->
-            NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
-                                      case N =:= Name of
-                                          true -> Config;
-                                          false -> C
-                                      end
-                                  end, OldConfig),
-            {ok, NewConfig}
-    end;
-pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) ->
-    case lookup_authenticator(?CHAIN, ID) of
-        {error, _Reason} -> OldConfig ++ [Config];
-        {ok, #{name := Name}} ->
-            NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
-                                      case N =:= Name of
-                                          true -> Config;
-                                          false -> C
-                                      end
-                                  end, OldConfig),
-            {ok, NewConfig}
-    end;
-pre_config_update({move_authenticator, ID, Position}, OldConfig) ->
-    case lookup_authenticator(?CHAIN, ID) of
-        {error, Reason} -> {error, Reason};
-        {ok, #{name := Name}} ->
-            {ok, Found, Part1, Part2} = split_by_name(Name, OldConfig),
-            case Position of
-                <<"top">> ->
-                    {ok, [Found | Part1] ++ Part2};
-                <<"bottom">> ->
-                    {ok, Part1 ++ Part2 ++ [Found]};
-                Before ->
-                    case binary:split(Before, <<":">>, [global]) of
-                        [<<"before">>, ID0] ->
-                            case lookup_authenticator(?CHAIN, ID0) of
-                                {error, Reason} -> {error, Reason};
-                                {ok, #{name := Name1}} ->
-                                    {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2),
-                                    {ok, NPart1 ++ [Found, NFound | NPart2]}
-                            end;
-                        _ ->
-                            {error, {invalid_parameter, position}}
-                    end
-            end
-    end.
-
-post_config_update({enable, true}, _NewConfig, _OldConfig, _AppEnvs) ->
-    emqx_authn:enable();
-post_config_update({enable, false}, _NewConfig, _OldConfig, _AppEnvs) ->
-    emqx_authn:disable();
-post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) ->
-    case lists:filter(
-             fun(#{name := N}) ->
-                 N =:= Name
-             end, NewConfig) of
-        [Config] ->
-            create_authenticator(?CHAIN, Config);
-        [_Config | _] ->
-            {error, name_has_be_used}
-    end;
-post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig, _AppEnvs) ->
-    case delete_authenticator(?CHAIN, ID) of
-        ok -> ok;
-        {error, Reason} -> throw(Reason)
-    end;
-post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) ->
-    case lists:filter(
-             fun(#{name := N}) ->
-                 N =:= Name
-             end, NewConfig) of
-        [Config] ->
-            update_authenticator(?CHAIN, ID, Config);
-        [_Config | _] ->
-            {error, name_has_be_used}
-    end;
-post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) ->
-    case lists:filter(
-             fun(#{name := N}) ->
-                 N =:= Name
-             end, NewConfig) of
-        [Config] ->
-            update_or_create_authenticator(?CHAIN, ID, Config);
-        [_Config | _] ->
-            {error, name_has_be_used}
-    end;
-post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig, _AppEnvs) ->
-    NPosition = case Position of
-                    <<"top">> -> top;
-                    <<"bottom">> -> bottom;
-                    Before ->
-                        case binary:split(Before, <<":">>, [global]) of
-                            [<<"before">>, ID0] ->
-                                {before, ID0};
-                            _ ->
-                                {error, {invalid_parameter, position}}
-                        end
-                end,
-    move_authenticator(?CHAIN, ID, NPosition).
-
-update_config(Path, ConfigRequest) ->
-    emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
-
-enable() ->
-    case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of
-        ok -> ok;
-        {error, already_exists} -> ok
-    end.
-
-disable() ->
-    emqx:unhook('client.authenticate', {?MODULE, authenticate, []}),
-    ok.
-
-is_enabled() ->
-    Callbacks = emqx_hooks:lookup('client.authenticate'),
-    lists:any(fun({callback, {?MODULE, authenticate, []}, _, _}) ->
-                  true;
-                 (_) ->
-                  false
-              end, Callbacks).
-
-authenticate(Credential, _AuthResult) ->
-    case ets:lookup(?CHAIN_TAB, ?CHAIN) of
-        [#chain{authenticators = Authenticators}] ->
-            do_authenticate(Authenticators, Credential);
-        [] ->
-            {stop, {error, not_authorized}}
-    end.
-
-do_authenticate([], _) ->
-    {stop, {error, not_authorized}};
-do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | More], Credential) ->
-    case Provider:authenticate(Credential, State) of
-        ignore ->
-            do_authenticate(More, Credential);
-        Result ->
-            %% {ok, Extra}
-            %% {ok, Extra, AuthData}
-            %% {ok, MetaData}
-            %% {continue, AuthCache}
-            %% {continue, AuthData, AuthCache}
-            %% {error, Reason}
-            {stop, Result}
-    end.
-
-start_link() ->
-    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-
-stop() ->
-    gen_server:stop(?MODULE).
-
-create_chain(#{id := ID}) ->
-    gen_server:call(?MODULE, {create_chain, ID}).
-
-delete_chain(ID) ->
-    gen_server:call(?MODULE, {delete_chain, ID}).
-
-lookup_chain(ID) ->
-    gen_server:call(?MODULE, {lookup_chain, ID}).
-
-list_chains() ->
-    Chains = ets:tab2list(?CHAIN_TAB),
-    {ok, [serialize_chain(Chain) || Chain <- Chains]}.
-
-create_authenticator(ChainID, Config) ->
-    gen_server:call(?MODULE, {create_authenticator, ChainID, Config}).
-
-delete_authenticator(ChainID, AuthenticatorID) ->
-    gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}).
-
-update_authenticator(ChainID, AuthenticatorID, Config) ->
-    gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}).
-
-update_or_create_authenticator(ChainID, AuthenticatorID, Config) ->
-    gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}).
-
-lookup_authenticator(ChainID, AuthenticatorID) ->
-    case ets:lookup(?CHAIN_TAB, ChainID) of
-        [] ->
-            {error, {not_found, {chain, ChainID}}};
-        [#chain{authenticators = Authenticators}] ->
-            case lists:keyfind(AuthenticatorID, 1, Authenticators) of
-                false ->
-                    {error, {not_found, {authenticator, AuthenticatorID}}};
-                {_, _, Authenticator} ->
-                    {ok, serialize_authenticator(Authenticator)}
-            end
-    end.
-
-list_authenticators(ChainID) ->
-    case ets:lookup(?CHAIN_TAB, ChainID) of
-        [] ->
-            {error, {not_found, {chain, ChainID}}};
-        [#chain{authenticators = Authenticators}] ->
-            {ok, serialize_authenticators(Authenticators)}
-    end.
-
-move_authenticator(ChainID, AuthenticatorID, Position) ->
-    gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}).
-
-import_users(ChainID, AuthenticatorID, Filename) ->
-    gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}).
-
-add_user(ChainID, AuthenticatorID, UserInfo) ->
-    gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}).
-
-delete_user(ChainID, AuthenticatorID, UserID) ->
-    gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}).
-
-update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
-    gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}).
-
-lookup_user(ChainID, AuthenticatorID, UserID) ->
-    gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}).
-
-%% TODO: Support pagination
-list_users(ChainID, AuthenticatorID) ->
-    gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}).
-
-%%--------------------------------------------------------------------
-%% gen_server callbacks
-%%--------------------------------------------------------------------
-
-init(_Opts) ->
-    _ = ets:new(?CHAIN_TAB, [ named_table, set, public
-                            , {keypos, #chain.id}
-                            , {read_concurrency, true}]),
-    {ok, #{}}.
-
-handle_call({create_chain, ID}, _From, State) ->
-    case ets:member(?CHAIN_TAB, ID) of
-        true ->
-            reply({error, {already_exists, {chain, ID}}}, State);
-        false ->
-            Chain = #chain{id = ID,
-                           authenticators = [],
-                           created_at = erlang:system_time(millisecond)},
-            true = ets:insert(?CHAIN_TAB, Chain),
-            reply({ok, serialize_chain(Chain)}, State)
-    end;
-
-handle_call({delete_chain, ID}, _From, State) ->
-    case ets:lookup(?CHAIN_TAB, ID) of
-        [] ->
-            reply({error, {not_found, {chain, ID}}}, State);
-        [#chain{authenticators = Authenticators}] ->
-            _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
-            true = ets:delete(?CHAIN_TAB, ID),
-            reply(ok, State)
-    end;
-
-handle_call({lookup_chain, ID}, _From, State) ->
-    case ets:lookup(?CHAIN_TAB, ID) of
-        [] ->
-            reply({error, {not_found, {chain, ID}}}, State);
-        [Chain] ->
-            reply({ok, serialize_chain(Chain)}, State)
-    end;
-
-handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) ->
-    UpdateFun =
-        fun(#chain{authenticators = Authenticators} = Chain) ->
-            case lists:keymember(Name, 2, Authenticators) of
-                true ->
-                    {error, name_has_be_used};
-                false ->
-                    AlreadyExist = fun(ID) ->
-                                       lists:keymember(ID, 1, Authenticators)
-                                   end,
-                    AuthenticatorID = gen_id(AlreadyExist),
-                    case do_create_authenticator(ChainID, AuthenticatorID, Config) of
-                        {ok, Authenticator} ->
-                            NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}],
-                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
-                            {ok, serialize_authenticator(Authenticator)};
-                        {error, Reason} ->
-                            {error, Reason}
-                    end
-            end
-        end,
-    Reply = update_chain(ChainID, UpdateFun),
-    reply(Reply, State);
-
-handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) ->
-    UpdateFun =
-        fun(#chain{authenticators = Authenticators} = Chain) ->
-            case lists:keytake(AuthenticatorID, 1, Authenticators) of
-                false ->
-                    {error, {not_found, {authenticator, AuthenticatorID}}};
-                {value, {_, _, Authenticator}, NAuthenticators} ->
-                    _ = do_delete_authenticator(Authenticator),
-                    true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
-                    ok
-            end
-        end,
-    Reply = update_chain(ChainID, UpdateFun),
-    reply(Reply, State);
-
-handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
-    Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false),
-    reply(Reply, State);
-
-handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
-    Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true),
-    reply(Reply, State);
-
-handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) ->
-    UpdateFun =
-        fun(#chain{authenticators = Authenticators} = Chain) ->
-            case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
-                {ok, NAuthenticators} ->
-                    true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
-                    ok;
-                {error, Reason} ->
-                    {error, Reason}
-            end
-        end,
-    Reply = update_chain(ChainID, UpdateFun),
-    reply(Reply, State);
-
-handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) ->
-    Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]),
-    reply(Reply, State);
-
-handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) ->
-    Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]),
-    reply(Reply, State);
-
-handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
-    Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]),
-    reply(Reply, State);
-
-handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
-    Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]),
-    reply(Reply, State);
-
-handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
-    Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]),
-    reply(Reply, State);
-
-handle_call({list_users, ChainID, AuthenticatorID}, _From, State) ->
-    Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []),
-    reply(Reply, State);
-
-handle_call(Req, _From, State) ->
-    ?LOG(error, "Unexpected call: ~p", [Req]),
-    {reply, ignored, State}.
-
-handle_cast(Req, State) ->
-    ?LOG(error, "Unexpected case: ~p", [Req]),
-    {noreply, State}.
-
-handle_info(Info, State) ->
-    ?LOG(error, "Unexpected info: ~p", [Info]),
-    {noreply, State}.
-
-terminate(_Reason, _State) ->
-    ok.
-
-code_change(_OldVsn, State, _Extra) ->
-    {ok, State}.
-
-reply(Reply, State) ->
-    {reply, Reply, State}.
-
-%%------------------------------------------------------------------------------
-%% Internal functions
-%%------------------------------------------------------------------------------
-
-authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) ->
-    emqx_authn_mnesia;
-authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) ->
-    emqx_authn_mysql;
-authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) ->
-    emqx_authn_pgsql;
-authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) ->
-    emqx_authn_mongodb;
-authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) ->
-    emqx_authn_redis;
-authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
-    emqx_authn_http;
-authenticator_provider(#{mechanism := jwt}) ->
-    emqx_authn_jwt;
-authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) ->
-    emqx_enhanced_authn_scram_mnesia.
-
-gen_id(AlreadyExist) ->
-    ID = list_to_binary(emqx_rule_id:gen()),
-    case AlreadyExist(ID) of
-        true -> gen_id(AlreadyExist);
-        false -> ID
-    end.
-
-switch_version(State = #{version := ?VER_1}) ->
-    State#{version := ?VER_2};
-switch_version(State = #{version := ?VER_2}) ->
-    State#{version := ?VER_1};
-switch_version(State) ->
-    State#{version => ?VER_1}.
-
-split_by_name(Name, Config) ->
-    {Part1, Part2, true} = lists:foldl(
-             fun(#{<<"name">> := N} = C, {P1, P2, F0}) ->
-                 F = case N =:= Name of
-                         true -> true;
-                         false -> F0
-                     end,
-                 case F of
-                     false -> {[C | P1], P2, F};
-                     true -> {P1, [C | P2], F}
-                 end
-             end, {[], [], false}, Config),
-    [Found | NPart2] = lists:reverse(Part2),
-    {ok, Found, lists:reverse(Part1), NPart2}.
-
-do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
-    Provider = authenticator_provider(Config),
-    Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
-    case Provider:create(Config#{'_unique' => Unique}) of
-        {ok, State} ->
-            Authenticator = #authenticator{id = AuthenticatorID,
-                                           name = Name,
-                                           provider = Provider,
-                                           state = switch_version(State)},
-            {ok, Authenticator};
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
-    _ = Provider:destroy(State),
-    ok.
-
-update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) ->
-    UpdateFun =
-        fun(#chain{authenticators = Authenticators} = Chain) ->
-            case lists:keytake(AuthenticatorID, 1, Authenticators) of
-                false ->
-                    case CreateWhenNotFound of
-                        true ->
-                            case lists:keymember(NewName, 2, Authenticators) of
-                                true ->
-                                    {error, name_has_be_used};
-                                false ->
-                                    case do_create_authenticator(ChainID, AuthenticatorID, Config) of
-                                        {ok, Authenticator} ->
-                                            NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}],
-                                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
-                                            {ok, serialize_authenticator(Authenticator)};
-                                        {error, Reason} ->
-                                            {error, Reason}
-                                    end
-                            end;
-                        false ->
-                            {error, {not_found, {authenticator, AuthenticatorID}}}
-                    end;
-                {value,
-                 {_, _, #authenticator{provider = Provider,
-                                       state    = #{version := Version} = State} = Authenticator},
-                 Others} ->
-                    case lists:keymember(NewName, 2, Others) of
-                        true ->
-                            {error, name_has_be_used};
-                        false ->
-                            case (NewProvider = authenticator_provider(Config)) =:= Provider of
-                                true ->
-                                    Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
-                                    case Provider:update(Config#{'_unique' => Unique}, State) of
-                                        {ok, NewState} ->
-                                            NewAuthenticator = Authenticator#authenticator{name = NewName,
-                                                                                           state = switch_version(NewState)},
-                                            NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
-                                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
-                                            {ok, serialize_authenticator(NewAuthenticator)};
-                                        {error, Reason} ->
-                                            {error, Reason}
-                                    end;
-                                false ->
-                                    Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
-                                    case NewProvider:create(Config#{'_unique' => Unique}) of
-                                        {ok, NewState} ->
-                                            NewAuthenticator = Authenticator#authenticator{name = NewName,
-                                                                                           provider = NewProvider,
-                                                                                           state = switch_version(NewState)},
-                                            NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
-                                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
-                                            _ = Provider:destroy(State),
-                                            {ok, serialize_authenticator(NewAuthenticator)};
-                                        {error, Reason} ->
-                                            {error, Reason}
-                                    end
-                            end
-                    end
-            end
-        end,
-    update_chain(ChainID, UpdateFun).
-
-replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) ->
-    lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}).
-
-do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) ->
-    case lists:keytake(AuthenticatorID, 1, Authenticators) of
-        false ->
-            {error, {not_found, {authenticator, AuthenticatorID}}};
-        {value, Authenticator, NAuthenticators} ->
-            do_move_authenticator(Authenticator, NAuthenticators, Position)
-    end;
-
-do_move_authenticator(Authenticator, Authenticators, top) ->
-    {ok, [Authenticator | Authenticators]};
-do_move_authenticator(Authenticator, Authenticators, bottom) ->
-    {ok, Authenticators ++ [Authenticator]};
-do_move_authenticator(Authenticator, Authenticators, {before, ID}) ->
-    insert(Authenticator, Authenticators, ID, []).
-
-insert(_, [], ID, _) ->
-    {error, {not_found, {authenticator, ID}}};
-insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) ->
-    {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]};
-insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) ->
-    insert(Authenticator, More, ID, [Authenticator0 | Acc]).
-
-update_chain(ChainID, UpdateFun) ->
-    case ets:lookup(?CHAIN_TAB, ChainID) of
-        [] ->
-            {error, {not_found, {chain, ChainID}}};
-        [Chain] ->
-            UpdateFun(Chain)
-    end.
-
-call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
-    UpdateFun =
-        fun(#chain{authenticators = Authenticators}) ->
-            case lists:keyfind(AuthenticatorID, 1, Authenticators) of
-                false ->
-                    {error, {not_found, {authenticator, AuthenticatorID}}};
-                {_, _, #authenticator{provider = Provider, state = State}} ->
-                    case erlang:function_exported(Provider, Func, length(Args) + 1) of
-                        true ->
-                            erlang:apply(Provider, Func, Args ++ [State]);
-                        false ->
-                            {error, unsupported_feature}
-                    end
-            end
-        end,
-    update_chain(ChainID, UpdateFun).
-
-serialize_chain(#chain{id = ID,
-                       authenticators = Authenticators,
-                       created_at = CreatedAt}) ->
-    #{id => ID,
-      authenticators => serialize_authenticators(Authenticators),
-      created_at => CreatedAt}.
-
-serialize_authenticators(Authenticators) ->
-    [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
-
-serialize_authenticator(#authenticator{id = ID,
-                                       name = Name,
-                                       provider = Provider,
-                                       state = State}) ->
-    #{id => ID, name => Name, provider => Provider, state => State}.

Fichier diff supprimé car celui-ci est trop grand
+ 787 - 520
apps/emqx_authn/src/emqx_authn_api.erl


+ 31 - 20
apps/emqx_authn/src/emqx_authn_app.erl

@@ -17,7 +17,6 @@
 -module(emqx_authn_app).
 
 -include("emqx_authn.hrl").
--include_lib("emqx/include/logger.hrl").
 
 -behaviour(application).
 
@@ -26,33 +25,45 @@
         , stop/1
         ]).
 
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
 start(_StartType, _StartArgs) ->
     ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
     {ok, Sup} = emqx_authn_sup:start_link(),
-    emqx_config_handler:add_handler([authentication, authenticators], emqx_authn),
-    initialize(),
+    ok = add_providers(),
+    ok = initialize(),
     {ok, Sup}.
 
 stop(_State) ->
+    ok = remove_providers(),
     ok.
 
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+add_providers() ->
+    _ = [?AUTHN:add_provider(AuthNType, Provider) || {AuthNType, Provider} <- providers()], ok.
+
+remove_providers() ->
+    _ = [?AUTHN:remove_provider(AuthNType) || {AuthNType, _} <- providers()], ok.
+
 initialize() ->
-    AuthNConfig = emqx:get_config([authentication], #{enable => false,
-                                                      authenticators => []}),
-    initialize(AuthNConfig).
-
-initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) ->
-    {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}),
-    initialize_authenticators(AuthenticatorsConfig),
-    Enable =:= true andalso emqx_authn:enable(),
+    ?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])),
+    lists:foreach(fun({ListenerID, ListenerConfig}) ->
+                      ?AUTHN:initialize_authentication(atom_to_binary(ListenerID), maps:get(authentication, ListenerConfig, []))
+                  end, emqx_listeners:list()),
     ok.
 
-initialize_authenticators([]) ->
-    ok;
-initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) ->
-    case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of
-        {ok, _} ->
-            initialize_authenticators(More);
-        {error, Reason} ->
-            ?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason])
-    end.
+providers() ->
+    [ {{'password-based', 'built-in-database'}, emqx_authn_mnesia}
+    , {{'password-based', mysql}, emqx_authn_mysql}
+    , {{'password-based', posgresql}, emqx_authn_pgsql}
+    , {{'password-based', mongodb}, emqx_authn_mongodb}
+    , {{'password-based', redis}, emqx_authn_redis}
+    , {{'password-based', 'http-server'}, emqx_authn_http}
+    , {jwt, emqx_authn_jwt}
+    , {{scram, 'built-in-database'}, emqx_enhanced_authn_scram_mnesia}
+    ].

+ 4 - 45
apps/emqx_authn/src/emqx_authn_schema.erl

@@ -16,56 +16,15 @@
 
 -module(emqx_authn_schema).
 
--include("emqx_authn.hrl").
 -include_lib("typerefl/include/types.hrl").
 
--behaviour(hocon_schema).
-
--export([ namespace/0
-        , roots/0
-        , fields/1
-        ]).
-
--export([ authenticator_name/1
-        ]).
-
-%% Export it for emqx_gateway_schema module
--export([ authenticators/1
+-export([ common_fields/0
         ]).
 
-namespace() -> authn.
-
-roots() -> [ "authentication" ].
-
-fields("authentication") ->
-    [ {enable, fun enable/1}
-    , {authenticators, fun authenticators/1}
+common_fields() ->
+    [ {enable,    fun enable/1}
     ].
 
-authenticator_name(type) -> binary();
-authenticator_name(nullable) -> false;
-authenticator_name(_) -> undefined.
-
 enable(type) -> boolean();
-enable(default) -> false;
+enable(default) -> true;
 enable(_) -> undefined.
-
-authenticators(type) ->
-    hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config)
-                          , hoconsc:ref(emqx_authn_mysql, config)
-                          , hoconsc:ref(emqx_authn_pgsql, config)
-                          , hoconsc:ref(emqx_authn_mongodb, standalone)
-                          , hoconsc:ref(emqx_authn_mongodb, 'replica-set')
-                          , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
-                          , hoconsc:ref(emqx_authn_redis, standalone)
-                          , hoconsc:ref(emqx_authn_redis, cluster)
-                          , hoconsc:ref(emqx_authn_redis, sentinel)
-                          , hoconsc:ref(emqx_authn_http, get)
-                          , hoconsc:ref(emqx_authn_http, post)
-                          , hoconsc:ref(emqx_authn_jwt, 'hmac-based')
-                          , hoconsc:ref(emqx_authn_jwt, 'public-key')
-                          , hoconsc:ref(emqx_authn_jwt, 'jwks')
-                          , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
-                          ]});
-authenticators(default) -> [];
-authenticators(_) -> undefined.

+ 1 - 7
apps/emqx_authn/src/emqx_authn_sup.erl

@@ -26,11 +26,5 @@ start_link() ->
     supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
 init([]) ->
-    ChildSpecs = [
-        #{id => emqx_authn,
-          start => {emqx_authn, start_link, []},
-          restart => permanent,
-          type => worker,
-          modules => [emqx_authn]}
-    ],
+    ChildSpecs = [],
     {ok, {{one_for_one, 10, 10}, ChildSpecs}}.

+ 10 - 10
apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl

@@ -20,13 +20,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
 -export([ namespace/0
         , roots/0
         , fields/1
         ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -75,21 +77,16 @@ mnesia(copy) ->
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:scram:builtin_db".
+namespace() -> "authn:scram:builtin-db".
 
 roots() -> [config].
 
 fields(config) ->
-    [ {name,            fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,       {enum, [scram]}}
-    , {server_type,     fun server_type/1}
+    [ {mechanism,       {enum, [scram]}}
+    , {backend,         {enum, ['built-in-database']}}
     , {algorithm,       fun algorithm/1}
     , {iteration_count, fun iteration_count/1}
-    ].
-
-server_type(type) -> hoconsc:enum(['built-in-database']);
-server_type(default) -> 'built-in-database';
-server_type(_) -> undefined.
+    ] ++ emqx_authn_schema:common_fields().
 
 algorithm(type) -> hoconsc:enum([sha256, sha512]);
 algorithm(default) -> sha256;
@@ -103,6 +100,9 @@ iteration_count(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+   [hoconsc:ref(?MODULE, config)].
+
 create(#{ algorithm := Algorithm
         , iteration_count := IterationCount
         , '_unique' := Unique

+ 33 - 30
apps/emqx_authn/src/simple_authn/emqx_authn_http.erl

@@ -21,6 +21,7 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
 -export([ namespace/0
         , roots/0
@@ -28,7 +29,8 @@
         , validations/0
         ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -38,7 +40,7 @@
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:http".
+namespace() -> "authn:password-based:http-server".
 
 roots() ->
     [ {config, {union, [ hoconsc:ref(?MODULE, get)
@@ -59,15 +61,15 @@ fields(post) ->
     ] ++ common_fields().
 
 common_fields() ->
-    [ {name,            fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,       {enum, ['password-based']}}
-    , {server_type,     {enum, ['http-server']}}
+    [ {mechanism,       {enum, ['password-based']}}
+    , {backend,         {enum, ['http-server']}}
     , {url,             fun url/1}
-    , {form_data,       fun form_data/1}
+    , {body,            fun body/1}
     , {request_timeout, fun request_timeout/1}
-    ] ++ maps:to_list(maps:without([ base_url
-                                   , pool_type],
-                      maps:from_list(emqx_connector_http:fields(config)))).
+    ] ++ emqx_authn_schema:common_fields()
+    ++ maps:to_list(maps:without([ base_url
+                                 , pool_type],
+                    maps:from_list(emqx_connector_http:fields(config)))).
 
 validations() ->
     [ {check_ssl_opts, fun check_ssl_opts/1}
@@ -95,11 +97,10 @@ headers_no_content_type(converter) ->
 headers_no_content_type(default) -> default_headers_no_content_type();
 headers_no_content_type(_) -> undefined.
 
-%% TODO: Using map()
-form_data(type) -> map();
-form_data(nullable) -> false;
-form_data(validate) -> [fun check_form_data/1];
-form_data(_) -> undefined.
+body(type) -> map();
+body(nullable) -> false;
+body(validate) -> [fun check_body/1];
+body(_) -> undefined.
 
 request_timeout(type) -> non_neg_integer();
 request_timeout(default) -> 5000;
@@ -109,10 +110,15 @@ request_timeout(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+    [ hoconsc:ref(?MODULE, get)
+    , hoconsc:ref(?MODULE, post)
+    ].
+
 create(#{ method := Method
         , url := URL
         , headers := Headers
-        , form_data := FormData
+        , body := Body
         , request_timeout := RequestTimeout
         , '_unique' := Unique
         } = Config) ->
@@ -121,8 +127,8 @@ create(#{ method := Method
     State = #{ method          => Method
              , path            => Path
              , base_query      => cow_qs:parse_qs(list_to_binary(Query))
-             , headers         => normalize_headers(Headers)
-             , form_data       => maps:to_list(FormData)
+             , headers         => maps:to_list(Headers)
+             , body            => maps:to_list(Body)
              , request_timeout => RequestTimeout
              , '_unique'       => Unique
              },
@@ -189,10 +195,10 @@ check_url(URL) ->
         {error, _} -> false
     end.
 
-check_form_data(FormData) ->
+check_body(Body) ->
     lists:any(fun({_, V}) ->
                   not is_binary(V)
-              end, maps:to_list(FormData)).
+              end, maps:to_list(Body)).
 
 default_headers() ->
     maps:put(<<"content-type">>,
@@ -232,23 +238,20 @@ parse_url(URL) ->
             URIMap
     end.
 
-normalize_headers(Headers) ->
-    [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)].
-
 generate_request(Credential, #{method := Method,
                                path := Path,
                                base_query := BaseQuery,
                                headers := Headers,
-                               form_data := FormData0}) ->
-    FormData = replace_placeholders(FormData0, Credential),
+                               body := Body0}) ->
+    Body = replace_placeholders(Body0, Credential),
     case Method of
         get ->
-            NPath = append_query(Path, BaseQuery ++ FormData),
+            NPath = append_query(Path, BaseQuery ++ Body),
             {NPath, Headers};
         post ->
             NPath = append_query(Path, BaseQuery),
             ContentType = proplists:get_value(<<"content-type">>, Headers),
-            Body = serialize_body(ContentType, FormData),
+            Body = serialize_body(ContentType, Body),
             {NPath, Headers, Body}
     end.
 
@@ -279,10 +282,10 @@ qs([], Acc) ->
 qs([{K, V} | More], Acc) ->
     qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]).
 
-serialize_body(<<"application/json">>, FormData) ->
-    emqx_json:encode(FormData);
-serialize_body(<<"application/x-www-form-urlencoded">>, FormData) ->
-    qs(FormData).
+serialize_body(<<"application/json">>, Body) ->
+    emqx_json:encode(Body);
+serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->
+    qs(Body).
 
 safely_parse_body(ContentType, Body) ->
     try parse_body(ContentType, Body) of

+ 12 - 5
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -19,13 +19,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
 -export([ namespace/0
         , roots/0
         , fields/1
         ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -81,12 +83,11 @@ fields(ssl_disable) ->
     [ {enable, #{type => false}} ].
 
 common_fields() ->
-    [ {name,            fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,       {enum, [jwt]}}
+    [ {mechanism,       {enum, [jwt]}}
     , {verify_claims,   fun verify_claims/1}
-    ].
+    ] ++ emqx_authn_schema:common_fields().
 
-secret(type) -> string();
+secret(type) -> binary();
 secret(_) -> undefined.
 
 secret_base64_encoded(type) -> boolean();
@@ -133,6 +134,12 @@ verify_claims(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+   [ hoconsc:ref(?MODULE, 'hmac-based')
+   , hoconsc:ref(?MODULE, 'public-key')
+   , hoconsc:ref(?MODULE, 'jwks')
+   ].
+
 create(#{verify_claims := VerifyClaims} = Config) ->
     create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
 

+ 14 - 7
apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl

@@ -20,10 +20,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
--export([ namespace/0, roots/0, fields/1 ]).
+-export([ namespace/0
+        , roots/0
+        , fields/1
+        ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -79,17 +84,16 @@ mnesia(copy) ->
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:builtin_db".
+namespace() -> "authn:password-based:builtin-db".
 
 roots() -> [config].
 
 fields(config) ->
-    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,               {enum, ['password-based']}}
-    , {server_type,             {enum, ['built-in-database']}}
+    [ {mechanism,               {enum, ['password-based']}}
+    , {backend,                 {enum, ['built-in-database']}}
     , {user_id_type,            fun user_id_type/1}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
-    ];
+    ] ++ emqx_authn_schema:common_fields();
 
 fields(bcrypt) ->
     [ {name, {enum, [bcrypt]}}
@@ -117,6 +121,9 @@ salt_rounds(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+   [hoconsc:ref(?MODULE, config)].
+
 create(#{ user_id_type := Type
         , password_hash_algorithm := #{name := bcrypt,
                                        salt_rounds := SaltRounds}

+ 13 - 6
apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl

@@ -21,13 +21,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
 -export([ namespace/0
         , roots/0
         , fields/1
         ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -37,7 +39,7 @@
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:mongodb".
+namespace() -> "authn:password-based:mongodb".
 
 roots() ->
     [ {config, {union, [ hoconsc:mk(standalone)
@@ -56,16 +58,15 @@ fields('sharded-cluster') ->
     common_fields() ++ emqx_connector_mongo:fields(sharded).
 
 common_fields() ->
-    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,               {enum, ['password-based']}}
-    , {server_type,             {enum, [mongodb]}}
+    [ {mechanism,               {enum, ['password-based']}}
+    , {backend,                 {enum, [mongodb]}}
     , {collection,              fun collection/1}
     , {selector,                fun selector/1}
     , {password_hash_field,     fun password_hash_field/1}
     , {salt_field,              fun salt_field/1}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
     , {salt_position,           fun salt_position/1}
-    ].
+    ] ++ emqx_authn_schema:common_fields().
 
 collection(type) -> binary();
 collection(nullable) -> false;
@@ -95,6 +96,12 @@ salt_position(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+    [ hoconsc:ref(?MODULE, standalone)
+    , hoconsc:ref(?MODULE, 'replica-set')
+    , hoconsc:ref(?MODULE, 'sharded-cluster')
+    ].
+
 create(#{ selector := Selector
         , '_unique' := Unique
         } = Config) ->

+ 11 - 6
apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl

@@ -21,13 +21,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
 -export([ namespace/0
         , roots/0
         , fields/1
         ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -37,19 +39,19 @@
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:mysql".
+namespace() -> "authn:password-based:mysql".
 
 roots() -> [config].
 
 fields(config) ->
-    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,               {enum, ['password-based']}}
-    , {server_type,             {enum, [mysql]}}
+    [ {mechanism,               {enum, ['password-based']}}
+    , {backend,                 {enum, [mysql]}}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
     , {salt_position,           fun salt_position/1}
     , {query,                   fun query/1}
     , {query_timeout,           fun query_timeout/1}
-    ] ++ emqx_connector_schema_lib:relational_db_fields()
+    ] ++ emqx_authn_schema:common_fields()
+    ++ emqx_connector_schema_lib:relational_db_fields()
     ++ emqx_connector_schema_lib:ssl_fields().
 
 password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
@@ -72,6 +74,9 @@ query_timeout(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+   [hoconsc:ref(?MODULE, config)].
+
 create(#{ password_hash_algorithm := Algorithm
         , salt_position := SaltPosition
         , query := Query0

+ 15 - 7
apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl

@@ -22,10 +22,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
--export([ namespace/0, roots/0, fields/1 ]).
+-export([ namespace/0
+        , roots/0
+        , fields/1
+        ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -35,18 +40,18 @@
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:postgres".
+namespace() -> "authn:password-based:postgresql".
 
 roots() -> [config].
 
 fields(config) ->
-    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,               {enum, ['password-based']}}
-    , {server_type,             {enum, [pgsql]}}
+    [ {mechanism,               {enum, ['password-based']}}
+    , {backend,                 {enum, [postgresql]}}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
     , {salt_position,           {enum, [prefix, suffix]}}
     , {query,                   fun query/1}
-    ] ++ emqx_connector_schema_lib:relational_db_fields()
+    ] ++ emqx_authn_schema:common_fields()
+    ++ emqx_connector_schema_lib:relational_db_fields()
     ++ emqx_connector_schema_lib:ssl_fields().
 
 password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
@@ -61,6 +66,9 @@ query(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+    [hoconsc:ref(?MODULE, config)].
+
 create(#{ query := Query0
         , password_hash_algorithm := Algorithm
         , salt_position := SaltPosition

+ 14 - 6
apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl

@@ -21,13 +21,15 @@
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
+-behaviour(emqx_authentication).
 
 -export([ namespace/0
         , roots/0
         , fields/1
         ]).
 
--export([ create/1
+-export([ refs/0
+        , create/1
         , update/2
         , authenticate/2
         , destroy/1
@@ -37,7 +39,8 @@
 %% Hocon Schema
 %%------------------------------------------------------------------------------
 
-namespace() -> "authn:redis".
+namespace() -> "authn:password-based:redis".
+
 roots() ->
     [ {config, {union, [ hoconsc:mk(standalone)
                        , hoconsc:mk(cluster)
@@ -55,13 +58,12 @@ fields(sentinel) ->
     common_fields() ++ emqx_connector_redis:fields(sentinel).
 
 common_fields() ->
-    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
-    , {mechanism,               {enum, ['password-based']}}
-    , {server_type,             {enum, [redis]}}
+    [ {mechanism,               {enum, ['password-based']}}
+    , {backend,                 {enum, [redis]}}
     , {query,                   fun query/1}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
     , {salt_position,           fun salt_position/1}
-    ].
+    ] ++ emqx_authn_schema:common_fields().
 
 query(type) -> string();
 query(nullable) -> false;
@@ -79,6 +81,12 @@ salt_position(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
+refs() ->
+    [ hoconsc:ref(?MODULE, standalone)
+    , hoconsc:ref(?MODULE, cluster)
+    , hoconsc:ref(?MODULE, sentinel)
+    ].
+
 create(#{ query := Query
         , '_unique' := Unique
         } = Config) ->

+ 0 - 98
apps/emqx_authn/test/emqx_authn_SUITE.erl

@@ -15,101 +15,3 @@
 %%--------------------------------------------------------------------
 
 -module(emqx_authn_SUITE).
-
--compile(export_all).
--compile(nowarn_export_all).
-
--include_lib("common_test/include/ct.hrl").
--include_lib("eunit/include/eunit.hrl").
-
--include("emqx_authn.hrl").
-
--define(AUTH, emqx_authn).
-
-all() ->
-    emqx_ct:all(?MODULE).
-
-init_per_suite(Config) ->
-    application:set_env(ekka, strict_mode, true),
-    emqx_ct_helpers:start_apps([emqx_authn]),
-    Config.
-
-end_per_suite(_) ->
-    emqx_ct_helpers:stop_apps([emqx_authn]),
-    ok.
-
-t_chain(_) ->
-    ?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)),
-
-    ChainID = <<"mychain">>,
-    Chain = #{id => ChainID},
-    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
-    ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)),
-    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)),
-    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
-    ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)),
-    ok.
-
-t_authenticator(_) ->
-    AuthenticatorName1 = <<"myauthenticator1">>,
-    AuthenticatorConfig1 = #{name => AuthenticatorName1,
-                             mechanism => 'password-based',
-                             server_type => 'built-in-database',
-                             user_id_type => username,
-                             password_hash_algorithm => #{
-                                 name => sha256
-                             }},
-    {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
-    ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)),
-    ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
-    ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
-
-    AuthenticatorConfig2 = #{name => AuthenticatorName1,
-                             mechanism => jwt,
-                             use_jwks => false,
-                             algorithm => 'hmac-based',
-                             secret => <<"abcdef">>,
-                             secret_base64_encoded => false,
-                             verify_claims => []},
-    {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
-
-    ID2 = <<"random">>,
-    ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
-    ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
-
-    AuthenticatorName2 = <<"myauthenticator2">>,
-    AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
-    {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
-    ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)),
-    {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}),
-
-    ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)),
-    ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
-
-    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
-    ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
-
-    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, bottom)),
-    ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
-
-    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})),
-
-    ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
-
-    ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})),
-
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
-    ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
-    ok.
-
-t_authenticate(_) ->
-    ClientInfo = #{zone => default,
-                   listener => {tcp, default},
-                   username => <<"myuser">>,
-			       password => <<"mypass">>},
-    ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
-    ?assertEqual(false, emqx_authn:is_enabled()),
-    emqx_authn:enable(),
-    ?assertEqual(true, emqx_authn:is_enabled()),
-    ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)).

+ 140 - 140
apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl

@@ -16,143 +16,143 @@
 
 -module(emqx_authn_jwt_SUITE).
 
--compile(export_all).
--compile(nowarn_export_all).
-
--include_lib("common_test/include/ct.hrl").
--include_lib("eunit/include/eunit.hrl").
-
--include("emqx_authn.hrl").
-
--define(AUTH, emqx_authn).
-
-all() ->
-    emqx_ct:all(?MODULE).
-
-init_per_suite(Config) ->
-    emqx_ct_helpers:start_apps([emqx_authn]),
-    Config.
-
-end_per_suite(_) ->
-    emqx_ct_helpers:stop_apps([emqx_authn]),
-    ok.
-
-t_jwt_authenticator(_) ->
-    AuthenticatorName = <<"myauthenticator">>,
-    Config = #{name => AuthenticatorName,
-               mechanism => jwt,
-               use_jwks => false,
-               algorithm => 'hmac-based',
-               secret => <<"abcdef">>,
-               secret_base64_encoded => false,
-               verify_claims => []},
-    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
-
-    Payload = #{<<"username">> => <<"myuser">>},
-    JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
-    ClientInfo = #{username => <<"myuser">>,
-			       password => JWS},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
-
-    Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true},
-    JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
-    ClientInfo1 = #{username => <<"myuser">>,
-			        password => JWS1},
-    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
-
-    BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
-    ClientInfo2 = ClientInfo#{password => BadJWS},
-    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
-
-    %% secret_base64_encoded
-    Config2 = Config#{secret => base64:encode(<<"abcdef">>),
-                      secret_base64_encoded => true},
-    ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
-
-    Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
-    ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
-
-    %% Expiration
-    Payload3 = #{ <<"username">> => <<"myuser">>
-                , <<"exp">> => erlang:system_time(second) - 60},
-    JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
-    ClientInfo3 = ClientInfo#{password => JWS3},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
-
-    Payload4 = #{ <<"username">> => <<"myuser">>
-                , <<"exp">> => erlang:system_time(second) + 60},
-    JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
-    ClientInfo4 = ClientInfo#{password => JWS4},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
-
-    %% Issued At
-    Payload5 = #{ <<"username">> => <<"myuser">>
-                , <<"iat">> => erlang:system_time(second) - 60},
-    JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
-    ClientInfo5 = ClientInfo#{password => JWS5},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
-
-    Payload6 = #{ <<"username">> => <<"myuser">>
-                , <<"iat">> => erlang:system_time(second) + 60},
-    JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
-    ClientInfo6 = ClientInfo#{password => JWS6},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
-
-    %% Not Before
-    Payload7 = #{ <<"username">> => <<"myuser">>
-                , <<"nbf">> => erlang:system_time(second) - 60},
-    JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
-    ClientInfo7 = ClientInfo#{password => JWS7},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
-
-    Payload8 = #{ <<"username">> => <<"myuser">>
-                , <<"nbf">> => erlang:system_time(second) + 60},
-    JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
-    ClientInfo8 = ClientInfo#{password => JWS8},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)),
-
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
-    ok.
-
-t_jwt_authenticator2(_) ->
-    Dir = code:lib_dir(emqx_authn, test),
-    PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
-    PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
-    AuthenticatorName = <<"myauthenticator">>,
-    Config = #{name => AuthenticatorName,
-               mechanism => jwt,
-               use_jwks => false,
-               algorithm => 'public-key',
-               certificate => PublicKey,
-               verify_claims => []},
-    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
-
-    Payload = #{<<"username">> => <<"myuser">>},
-    JWS = generate_jws('public-key', Payload, PrivateKey),
-    ClientInfo = #{username => <<"myuser">>,
-			       password => JWS},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
-    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
-
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
-    ok.
-
-generate_jws('hmac-based', Payload, Secret) ->
-    JWK = jose_jwk:from_oct(Secret),
-    Header = #{ <<"alg">> => <<"HS256">>
-              , <<"typ">> => <<"JWT">>
-              },
-    Signed = jose_jwt:sign(JWK, Header, Payload),
-    {_, JWS} = jose_jws:compact(Signed),
-    JWS;
-generate_jws('public-key', Payload, PrivateKey) ->
-    JWK = jose_jwk:from_pem_file(PrivateKey),
-    Header = #{ <<"alg">> => <<"RS256">>
-              , <<"typ">> => <<"JWT">>
-              },
-    Signed = jose_jwt:sign(JWK, Header, Payload),
-    {_, JWS} = jose_jws:compact(Signed),
-    JWS.
+% -compile(export_all).
+% -compile(nowarn_export_all).
+
+% -include_lib("common_test/include/ct.hrl").
+% -include_lib("eunit/include/eunit.hrl").
+
+% -include("emqx_authn.hrl").
+
+% -define(AUTH, emqx_authn).
+
+% all() ->
+%     emqx_ct:all(?MODULE).
+
+% init_per_suite(Config) ->
+%     emqx_ct_helpers:start_apps([emqx_authn]),
+%     Config.
+
+% end_per_suite(_) ->
+%     emqx_ct_helpers:stop_apps([emqx_authn]),
+%     ok.
+
+% t_jwt_authenticator(_) ->
+%     AuthenticatorName = <<"myauthenticator">>,
+%     Config = #{name => AuthenticatorName,
+%                mechanism => jwt,
+%                use_jwks => false,
+%                algorithm => 'hmac-based',
+%                secret => <<"abcdef">>,
+%                secret_base64_encoded => false,
+%                verify_claims => []},
+%     {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
+
+%     Payload = #{<<"username">> => <<"myuser">>},
+%     JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
+%     ClientInfo = #{username => <<"myuser">>,
+% 			       password => JWS},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+
+%     Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true},
+%     JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
+%     ClientInfo1 = #{username => <<"myuser">>,
+% 			        password => JWS1},
+%     ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
+
+%     BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
+%     ClientInfo2 = ClientInfo#{password => BadJWS},
+%     ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
+
+%     %% secret_base64_encoded
+%     Config2 = Config#{secret => base64:encode(<<"abcdef">>),
+%                       secret_base64_encoded => true},
+%     ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+
+%     Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
+%     ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+%     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
+
+%     %% Expiration
+%     Payload3 = #{ <<"username">> => <<"myuser">>
+%                 , <<"exp">> => erlang:system_time(second) - 60},
+%     JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
+%     ClientInfo3 = ClientInfo#{password => JWS3},
+%     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
+
+%     Payload4 = #{ <<"username">> => <<"myuser">>
+%                 , <<"exp">> => erlang:system_time(second) + 60},
+%     JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
+%     ClientInfo4 = ClientInfo#{password => JWS4},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
+
+%     %% Issued At
+%     Payload5 = #{ <<"username">> => <<"myuser">>
+%                 , <<"iat">> => erlang:system_time(second) - 60},
+%     JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
+%     ClientInfo5 = ClientInfo#{password => JWS5},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
+
+%     Payload6 = #{ <<"username">> => <<"myuser">>
+%                 , <<"iat">> => erlang:system_time(second) + 60},
+%     JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
+%     ClientInfo6 = ClientInfo#{password => JWS6},
+%     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
+
+%     %% Not Before
+%     Payload7 = #{ <<"username">> => <<"myuser">>
+%                 , <<"nbf">> => erlang:system_time(second) - 60},
+%     JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
+%     ClientInfo7 = ClientInfo#{password => JWS7},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
+
+%     Payload8 = #{ <<"username">> => <<"myuser">>
+%                 , <<"nbf">> => erlang:system_time(second) + 60},
+%     JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
+%     ClientInfo8 = ClientInfo#{password => JWS8},
+%     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)),
+
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
+%     ok.
+
+% t_jwt_authenticator2(_) ->
+%     Dir = code:lib_dir(emqx_authn, test),
+%     PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
+%     PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
+%     AuthenticatorName = <<"myauthenticator">>,
+%     Config = #{name => AuthenticatorName,
+%                mechanism => jwt,
+%                use_jwks => false,
+%                algorithm => 'public-key',
+%                certificate => PublicKey,
+%                verify_claims => []},
+%     {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
+
+%     Payload = #{<<"username">> => <<"myuser">>},
+%     JWS = generate_jws('public-key', Payload, PrivateKey),
+%     ClientInfo = #{username => <<"myuser">>,
+% 			       password => JWS},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+%     ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
+
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
+%     ok.
+
+% generate_jws('hmac-based', Payload, Secret) ->
+%     JWK = jose_jwk:from_oct(Secret),
+%     Header = #{ <<"alg">> => <<"HS256">>
+%               , <<"typ">> => <<"JWT">>
+%               },
+%     Signed = jose_jwt:sign(JWK, Header, Payload),
+%     {_, JWS} = jose_jws:compact(Signed),
+%     JWS;
+% generate_jws('public-key', Payload, PrivateKey) ->
+%     JWK = jose_jwk:from_pem_file(PrivateKey),
+%     Header = #{ <<"alg">> => <<"RS256">>
+%               , <<"typ">> => <<"JWT">>
+%               },
+%     Signed = jose_jwt:sign(JWK, Header, Payload),
+%     {_, JWS} = jose_jws:compact(Signed),
+%     JWS.

+ 146 - 146
apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl

@@ -16,149 +16,149 @@
 
 -module(emqx_authn_mnesia_SUITE).
 
--compile(export_all).
--compile(nowarn_export_all).
-
--include_lib("common_test/include/ct.hrl").
--include_lib("eunit/include/eunit.hrl").
-
--include("emqx_authn.hrl").
-
--define(AUTH, emqx_authn).
-
-all() ->
-    emqx_ct:all(?MODULE).
-
-init_per_suite(Config) ->
-    emqx_ct_helpers:start_apps([emqx_authn]),
-    Config.
-
-end_per_suite(_) ->
-    emqx_ct_helpers:stop_apps([emqx_authn]),
-    ok.
-
-t_mnesia_authenticator(_) ->
-    AuthenticatorName = <<"myauthenticator">>,
-    AuthenticatorConfig = #{name => AuthenticatorName,
-                            mechanism => 'password-based',
-                            server_type => 'built-in-database',
-                            user_id_type => username,
-                            password_hash_algorithm => #{
-                                name => sha256
-                            }},
-    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
-
-    UserInfo = #{user_id => <<"myuser">>,
-                 password => <<"mypass">>},
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
-
-    ClientInfo = #{zone => external,
-                   username => <<"myuser">>,
-			       password => <<"mypass">>},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
-    ?AUTH:enable(),
-    ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
-
-    ClientInfo2 = ClientInfo#{username => <<"baduser">>},
-    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
-    ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
-
-    ClientInfo3 = ClientInfo#{password => <<"badpass">>},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
-    ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
-
-    UserInfo2 = UserInfo#{password => <<"mypass2">>},
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
-    ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
-
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})),
-    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
-
-    ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
-    ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
-
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
-
-    {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
-    ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
-    ok.
-
-t_import(_) ->
-    AuthenticatorName = <<"myauthenticator">>,
-    AuthenticatorConfig = #{name => AuthenticatorName,
-                            mechanism => 'password-based',
-                            server_type => 'built-in-database',
-                            user_id_type => username,
-                            password_hash_algorithm => #{
-                                name => sha256
-                            }},
-    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
-
-    Dir = code:lib_dir(emqx_authn, test),
-    ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))),
-    ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))),
-    ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)),
-    ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)),
-
-    ClientInfo1 = #{username => <<"myuser1">>,
-			        password => <<"mypassword1">>},
-    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
-
-    ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
-                               password => <<"mypassword2">>},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
-
-    ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
-                               password => <<"mypassword3">>},
-    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
-
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
-    ok.
-
-t_multi_mnesia_authenticator(_) ->
-    AuthenticatorName1 = <<"myauthenticator1">>,
-    AuthenticatorConfig1 = #{name => AuthenticatorName1,
-                             mechanism => 'password-based',
-                             server_type => 'built-in-database',
-                             user_id_type => username,
-                             password_hash_algorithm => #{
-                                 name => sha256
-                             }},
-    AuthenticatorName2 = <<"myauthenticator2">>,
-    AuthenticatorConfig2 = #{name => AuthenticatorName2,
-                             mechanism => 'password-based',
-                             server_type => 'built-in-database',
-                             user_id_type => clientid,
-                             password_hash_algorithm => #{
-                                 name => sha256
-                             }},
-    {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
-    {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
-
-    ?assertMatch({ok, #{user_id := <<"myuser">>}},
-                 ?AUTH:add_user(?CHAIN, ID1,
-                                #{user_id => <<"myuser">>,
-                                  password => <<"mypass1">>})),
-    ?assertMatch({ok, #{user_id := <<"myclient">>}},
-                 ?AUTH:add_user(?CHAIN, ID2,
-                                #{user_id => <<"myclient">>,
-                                  password => <<"mypass2">>})),
-
-    ClientInfo1 = #{username => <<"myuser">>,
-                    clientid => <<"myclient">>,
-			        password => <<"mypass1">>},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
-    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
-
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
-    ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
-    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
-
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
-    ok.
+% -compile(export_all).
+% -compile(nowarn_export_all).
+
+% -include_lib("common_test/include/ct.hrl").
+% -include_lib("eunit/include/eunit.hrl").
+
+% -include("emqx_authn.hrl").
+
+% -define(AUTH, emqx_authn).
+
+% all() ->
+%     emqx_ct:all(?MODULE).
+
+% init_per_suite(Config) ->
+%     emqx_ct_helpers:start_apps([emqx_authn]),
+%     Config.
+
+% end_per_suite(_) ->
+%     emqx_ct_helpers:stop_apps([emqx_authn]),
+%     ok.
+
+% t_mnesia_authenticator(_) ->
+%     AuthenticatorName = <<"myauthenticator">>,
+%     AuthenticatorConfig = #{name => AuthenticatorName,
+%                             mechanism => 'password-based',
+%                             server_type => 'built-in-database',
+%                             user_id_type => username,
+%                             password_hash_algorithm => #{
+%                                 name => sha256
+%                             }},
+%     {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
+
+%     UserInfo = #{user_id => <<"myuser">>,
+%                  password => <<"mypass">>},
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
+
+%     ClientInfo = #{zone => external,
+%                    username => <<"myuser">>,
+% 			       password => <<"mypass">>},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+%     ?AUTH:enable(),
+%     ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
+
+%     ClientInfo2 = ClientInfo#{username => <<"baduser">>},
+%     ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
+%     ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
+
+%     ClientInfo3 = ClientInfo#{password => <<"badpass">>},
+%     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
+%     ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
+
+%     UserInfo2 = UserInfo#{password => <<"mypass2">>},
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
+%     ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
+
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})),
+%     ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
+
+%     ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
+%     ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
+
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
+
+%     {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
+%     ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)),
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
+%     ok.
+
+% t_import(_) ->
+%     AuthenticatorName = <<"myauthenticator">>,
+%     AuthenticatorConfig = #{name => AuthenticatorName,
+%                             mechanism => 'password-based',
+%                             server_type => 'built-in-database',
+%                             user_id_type => username,
+%                             password_hash_algorithm => #{
+%                                 name => sha256
+%                             }},
+%     {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
+
+%     Dir = code:lib_dir(emqx_authn, test),
+%     ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))),
+%     ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))),
+%     ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)),
+%     ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)),
+
+%     ClientInfo1 = #{username => <<"myuser1">>,
+% 			        password => <<"mypassword1">>},
+%     ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
+
+%     ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
+%                                password => <<"mypassword2">>},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
+
+%     ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
+%                                password => <<"mypassword3">>},
+%     ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
+
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
+%     ok.
+
+% t_multi_mnesia_authenticator(_) ->
+%     AuthenticatorName1 = <<"myauthenticator1">>,
+%     AuthenticatorConfig1 = #{name => AuthenticatorName1,
+%                              mechanism => 'password-based',
+%                              server_type => 'built-in-database',
+%                              user_id_type => username,
+%                              password_hash_algorithm => #{
+%                                  name => sha256
+%                              }},
+%     AuthenticatorName2 = <<"myauthenticator2">>,
+%     AuthenticatorConfig2 = #{name => AuthenticatorName2,
+%                              mechanism => 'password-based',
+%                              server_type => 'built-in-database',
+%                              user_id_type => clientid,
+%                              password_hash_algorithm => #{
+%                                  name => sha256
+%                              }},
+%     {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
+%     {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
+
+%     ?assertMatch({ok, #{user_id := <<"myuser">>}},
+%                  ?AUTH:add_user(?CHAIN, ID1,
+%                                 #{user_id => <<"myuser">>,
+%                                   password => <<"mypass1">>})),
+%     ?assertMatch({ok, #{user_id := <<"myclient">>}},
+%                  ?AUTH:add_user(?CHAIN, ID2,
+%                                 #{user_id => <<"myclient">>,
+%                                   password => <<"mypass2">>})),
+
+%     ClientInfo1 = #{username => <<"myuser">>,
+%                     clientid => <<"myclient">>,
+% 			        password => <<"mypass1">>},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
+%     ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
+
+%     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
+%     ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
+%     ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
+
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
+%     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
+%     ok.

+ 2 - 9
apps/emqx_connector/src/emqx_connector_mongo.erl

@@ -242,15 +242,8 @@ init_worker_options([_ | R], Acc) ->
     init_worker_options(R, Acc);
 init_worker_options([], Acc) -> Acc.
 
-host_port(HostPort) ->
-    case string:split(HostPort, ":") of
-        [Host, Port] ->
-            {ok, Host1} = inet:parse_address(Host),
-            [{host, Host1}, {port, list_to_integer(Port)}];
-        [Host] ->
-            {ok, Host1} = inet:parse_address(Host),
-            [{host, Host1}]
-    end.
+host_port({Host, Port}) ->
+    [{host, Host}, {port, Port}].
 
 server(type) -> server();
 server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")];

+ 12 - 2
apps/emqx_connector/src/emqx_connector_redis.erl

@@ -19,9 +19,13 @@
 -include_lib("typerefl/include/types.hrl").
 -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
 
--type server() :: emqx_schema:ip_port().
+-type server() :: tuple().
+
 -reflect_type([server/0]).
--typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}).
+
+-typerefl_from_string({server/0, ?MODULE, to_server}).
+
+-export([to_server/1]).
 
 -export([roots/0, fields/1]).
 
@@ -168,3 +172,9 @@ redis_fields() ->
                    default => 0}}
     , {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1}
     ].
+
+to_server(Server) ->
+    case string:tokens(Server, ":") of
+        [Host, Port] -> {ok, {Host, list_to_integer(Port)}};
+        _ -> {error, Server}
+    end.

+ 7 - 13
apps/emqx_gateway/etc/emqx_gateway.conf

@@ -29,12 +29,13 @@ gateway.stomp {
     password = "${Packet.headers.passcode}"
   }
 
-  authentication {
-    name = "authenticator1"
-    mechanism = password-based
-    server_type = built-in-database
-    user_id_type = clientid
-  }
+  authentication: [
+      # {
+      #   name = "authenticator1"
+      #   type = "password-based:built-in-database"
+      #   user_id_type = clientid
+      #  }
+  ]
 
   listeners.tcp.default {
     bind = 61613
@@ -63,13 +64,6 @@ gateway.coap {
   subscribe_qos = qos0
   publish_qos = qos1
 
-  authentication {
-    name = "authenticator1"
-    mechanism = password-based
-    server_type = built-in-database
-    user_id_type = clientid
-  }
-
   listeners.udp.default {
     bind = 5683
   }

+ 20 - 20
apps/emqx_gateway/src/emqx_gateway_schema.erl

@@ -222,25 +222,25 @@ fields(ExtraField) ->
     Mod = list_to_atom(ExtraField++"_schema"),
     Mod:fields(ExtraField).
 
-authentication() ->
-    hoconsc:union(
-      [ undefined
-      , hoconsc:ref(emqx_authn_mnesia, config)
-      , hoconsc:ref(emqx_authn_mysql, config)
-      , hoconsc:ref(emqx_authn_pgsql, config)
-      , hoconsc:ref(emqx_authn_mongodb, standalone)
-      , hoconsc:ref(emqx_authn_mongodb, 'replica-set')
-      , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
-      , hoconsc:ref(emqx_authn_redis, standalone)
-      , hoconsc:ref(emqx_authn_redis, cluster)
-      , hoconsc:ref(emqx_authn_redis, sentinel)
-      , hoconsc:ref(emqx_authn_http, get)
-      , hoconsc:ref(emqx_authn_http, post)
-      , hoconsc:ref(emqx_authn_jwt, 'hmac-based')
-      , hoconsc:ref(emqx_authn_jwt, 'public-key')
-      , hoconsc:ref(emqx_authn_jwt, 'jwks')
-      , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
-      ]).
+% authentication() ->
+%     hoconsc:union(
+%       [ undefined
+%       , hoconsc:ref(emqx_authn_mnesia, config)
+%       , hoconsc:ref(emqx_authn_mysql, config)
+%       , hoconsc:ref(emqx_authn_pgsql, config)
+%       , hoconsc:ref(emqx_authn_mongodb, standalone)
+%       , hoconsc:ref(emqx_authn_mongodb, 'replica-set')
+%       , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
+%       , hoconsc:ref(emqx_authn_redis, standalone)
+%       , hoconsc:ref(emqx_authn_redis, cluster)
+%       , hoconsc:ref(emqx_authn_redis, sentinel)
+%       , hoconsc:ref(emqx_authn_http, get)
+%       , hoconsc:ref(emqx_authn_http, post)
+%       , hoconsc:ref(emqx_authn_jwt, 'hmac-based')
+%       , hoconsc:ref(emqx_authn_jwt, 'public-key')
+%       , hoconsc:ref(emqx_authn_jwt, 'jwks')
+%       , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
+%       ]).
 
 gateway_common_options() ->
     [ {enable, sc(boolean(), undefined, true)}
@@ -248,7 +248,7 @@ gateway_common_options() ->
     , {idle_timeout, sc(duration(), undefined, <<"30s">>)}
     , {mountpoint, sc(binary())}
     , {clientinfo_override, sc(ref(clientinfo_override))}
-    , {authentication,  sc(authentication(), undefined, undefined)}
+    , {authentication,  sc(hoconsc:lazy(map()))}
     ].
 
 %%--------------------------------------------------------------------

+ 0 - 1
apps/emqx_machine/src/emqx_machine_schema.erl

@@ -46,7 +46,6 @@
         , emqx_data_bridge_schema
         , emqx_retainer_schema
         , emqx_statsd_schema
-        , emqx_authn_schema
         , emqx_authz_schema
         , emqx_auto_subscribe_schema
         , emqx_bridge_mqtt_schema

+ 0 - 2
apps/emqx_retainer/src/emqx_retainer.erl

@@ -19,10 +19,8 @@
 -behaviour(gen_server).
 
 -include("emqx_retainer.hrl").
--include_lib("emqx/include/emqx.hrl").
 -include_lib("emqx/include/logger.hrl").
 
-
 -export([start_link/0]).
 
 -export([ on_session_subscribed/4

+ 1 - 0
rebar.config

@@ -63,6 +63,7 @@
     , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}}
     , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}}
     , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
+    , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
     ]}.
 
 {xref_ignores,