Procházet zdrojové kódy

refactor(authn): support hocon for authn (#5068)

* refactor(use hocon): rename to authn, , support hocon, support two types of chains and support bind listener to chain
tigercl před 4 roky
rodič
revize
cdcb63374a
33 změnil soubory, kde provedl 2653 přidání a 1651 odebrání
  1. 0 0
      apps/emqx_authentication/etc/emqx_authentication.conf
  2. 0 43
      apps/emqx_authentication/include/emqx_authentication.hrl
  3. 0 0
      apps/emqx_authentication/priv/emqx_authentication.schema
  4. 0 522
      apps/emqx_authentication/src/emqx_authentication.erl
  5. 0 407
      apps/emqx_authentication/src/emqx_authentication_api.erl
  6. 0 409
      apps/emqx_authentication/src/emqx_authentication_jwt.erl
  7. 0 189
      apps/emqx_authentication/test/emqx_authentication_SUITE.erl
  8. 0 0
      apps/emqx_authn/data/user-credentials.csv
  9. 0 0
      apps/emqx_authn/data/user-credentials.json
  10. 26 0
      apps/emqx_authn/etc/emqx_authn.conf
  11. 67 0
      apps/emqx_authn/include/emqx_authn.hrl
  12. 0 0
      apps/emqx_authn/rebar.config
  13. 3 3
      apps/emqx_authentication/src/emqx_authentication.app.src
  14. 490 0
      apps/emqx_authn/src/emqx_authn.erl
  15. 544 0
      apps/emqx_authn/src/emqx_authn_api.erl
  16. 80 0
      apps/emqx_authn/src/emqx_authn_app.erl
  17. 114 0
      apps/emqx_authn/src/emqx_authn_schema.erl
  18. 1 1
      apps/emqx_authentication/src/emqx_authentication_sup.erl
  19. 55 0
      apps/emqx_authn/src/emqx_authn_utils.erl
  20. 1 21
      apps/emqx_authentication/src/emqx_authentication_app.erl
  21. 11 17
      apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl
  22. 343 0
      apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl
  23. 69 38
      apps/emqx_authentication/src/emqx_authentication_mnesia.erl
  24. 160 0
      apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl
  25. 155 0
      apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl
  26. 15 0
      apps/emqx_authn/test/data/private_key.pem
  27. 6 0
      apps/emqx_authn/test/data/public_key.pem
  28. 0 0
      apps/emqx_authn/test/data/user-credentials.csv
  29. 0 0
      apps/emqx_authn/test/data/user-credentials.json
  30. 142 0
      apps/emqx_authn/test/emqx_authn_SUITE.erl
  31. 182 0
      apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl
  32. 187 0
      apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl
  33. 2 1
      rebar.config.erl

+ 0 - 0
apps/emqx_authentication/etc/emqx_authentication.conf


+ 0 - 43
apps/emqx_authentication/include/emqx_authentication.hrl

@@ -1,43 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 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.
-%%--------------------------------------------------------------------
-
--define(APP, emqx_authentication).
-
--type(service_type_name() :: atom()).
--type(service_name() :: binary()).
--type(chain_id() :: binary()).
-
--record(service_type,
-        { name :: service_type_name()
-        , provider :: module()
-        , params_spec :: #{atom() => term()}
-        }).
-
--record(service,
-        { name :: service_name()
-        , type :: service_type_name()
-        , provider :: module()
-        , params :: map()
-        , state :: map()
-        }).
-
--record(chain,
-        { id :: chain_id()
-        , services :: [{service_name(), #service{}}]
-        , created_at :: integer()
-        }).
-
--define(AUTH_SHARD, emqx_authentication_shard).

+ 0 - 0
apps/emqx_authentication/priv/emqx_authentication.schema


+ 0 - 522
apps/emqx_authentication/src/emqx_authentication.erl

@@ -1,522 +0,0 @@
-%%--------------------------------------------------------------------
-%% 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).
-
--include("emqx_authentication.hrl").
-
--export([ enable/0
-        , disable/0
-        ]).
-
--export([authenticate/1]).
-
--export([register_service_types/0]).
-
--export([ create_chain/1
-        , delete_chain/1
-        , lookup_chain/1
-        , list_chains/0
-        , add_services/2
-        , delete_services/2
-        , update_service/3
-        , lookup_service/2
-        , list_services/1
-        , move_service_to_the_front/2
-        , move_service_to_the_end/2
-        , move_service_to_the_nth/3
-        ]).
-
--export([ import_users/3
-        , add_user/3
-        , delete_user/3
-        , update_user/4
-        , lookup_user/3
-        , list_users/2
-        ]).
-
--export([mnesia/1]).
-
--boot_mnesia({mnesia, [boot]}).
--copy_mnesia({mnesia, [copy]}).
-
--define(CHAIN_TAB, emqx_authentication_chain).
--define(SERVICE_TYPE_TAB, emqx_authentication_service_type).
-
--rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}).
--rlog_shard({?AUTH_SHARD, ?SERVICE_TYPE_TAB}).
-
-%%------------------------------------------------------------------------------
-%% Mnesia bootstrap
-%%------------------------------------------------------------------------------
-
-%% @doc Create or replicate tables.
--spec(mnesia(boot | copy) -> ok).
-mnesia(boot) ->
-    %% Optimize storage
-    StoreProps = [{ets, [{read_concurrency, true}]}],
-    %% Chain table
-    ok = ekka_mnesia:create_table(?CHAIN_TAB, [
-                {disc_copies, [node()]},
-                {record_name, chain},
-                {attributes, record_info(fields, chain)},
-                {storage_properties, StoreProps}]),
-    %% Service type table
-    ok = ekka_mnesia:create_table(?SERVICE_TYPE_TAB, [
-                {ram_copies, [node()]},
-                {record_name, service_type},
-                {attributes, record_info(fields, service_type)},
-                {storage_properties, StoreProps}]);
-
-mnesia(copy) ->
-    %% Copy chain table
-    ok = ekka_mnesia:copy_table(?CHAIN_TAB, disc_copies),
-    %% Copy service type table
-    ok = ekka_mnesia:copy_table(?SERVICE_TYPE_TAB, ram_copies).
-
-enable() ->
-    case emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}) of
-        ok -> ok;
-        {error, already_exists} -> ok
-    end.
-
-disable() ->
-    emqx:unhook('client.authenticate', {emqx_authentication, authenticate}),
-    ok.
-
-authenticate(#{chain_id := ChainID} = ClientInfo) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
-        [#chain{services = []}] ->
-            {error, no_services};
-        [#chain{services = Services}] ->
-            do_authenticate(Services, ClientInfo);
-        [] ->
-            {error, todo}
-    end.
-
-do_authenticate([], _) ->
-    {error, user_not_found};
-do_authenticate([{_, #service{provider = Provider, state = State}} | More], ClientInfo) ->
-    case Provider:authenticate(ClientInfo, State) of
-        ignore -> do_authenticate(More, ClientInfo);
-        ok -> ok;
-        {ok, NewClientInfo} -> {ok, NewClientInfo};
-        {stop, Reason} -> {error, Reason}
-    end.
-
-register_service_types() ->
-    Attrs = find_attrs(?APP, service_type),
-    register_service_types(Attrs).
-
-register_service_types(Attrs) ->
-    register_service_types(Attrs, []).
-
-register_service_types([], Acc) ->
-    do_register_service_types(Acc);
-register_service_types([{_App, Mod, #{name := Name,
-                                      params_spec := ParamsSpec}} | Types], Acc) ->
-    %% TODO: Temporary realization
-    ok = emqx_rule_validator:validate_spec(ParamsSpec),
-    ServiceType = #service_type{name = Name,
-                                provider = Mod,
-                                params_spec = ParamsSpec},
-    register_service_types(Types, [ServiceType | Acc]).
-
-create_chain(#{id := ID}) ->
-    trans(
-        fun() ->
-            case mnesia:read(?CHAIN_TAB, ID, write) of
-                [] ->
-                    Chain = #chain{id = ID,
-                                   services = [],
-                                   created_at = erlang:system_time(millisecond)},
-                    mnesia:write(?CHAIN_TAB, Chain, write),
-                    {ok, serialize_chain(Chain)};
-                [_ | _] ->
-                    {error, {already_exists, {chain, ID}}}
-            end
-        end).
-
-delete_chain(ID) ->
-    trans(
-        fun() ->
-            case mnesia:read(?CHAIN_TAB, ID, write) of
-                [] ->
-                    {error, {not_found, {chain, ID}}};
-                [#chain{services = Services}] ->
-                    ok = delete_services_(Services),
-                    mnesia:delete(?CHAIN_TAB, ID, write)
-            end
-        end).
-
-lookup_chain(ID) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ID) of
-        [] ->
-            {error, {not_found, {chain, ID}}};
-        [Chain] ->
-            {ok, serialize_chain(Chain)}
-    end.
-
-list_chains() ->
-    Chains = ets:tab2list(?CHAIN_TAB),
-    {ok, [serialize_chain(Chain) || Chain <- Chains]}.
-
-add_services(ChainID, ServiceParams) ->
-    case validate_service_params(ServiceParams) of
-        {ok, NServiceParams} ->
-            UpdateFun = fun(Chain = #chain{services = Services}) ->
-                            Names = [Name || {Name, _} <- Services] ++ [Name || #{name := Name} <- NServiceParams],
-                            case no_duplicate_names(Names) of
-                                ok ->
-                                    case create_services(ChainID, NServiceParams) of
-                                        {ok, NServices} ->
-                                            NChain = Chain#chain{services = Services ++ NServices},
-                                            ok = mnesia:write(?CHAIN_TAB, NChain, write),
-                                            {ok, serialize_services(NServices)};
-                                        {error, Reason} ->
-                                            {error, Reason}
-                                    end;
-                                {error, {duplicate, Name}} ->
-                                    {error, {already_exists, {service, Name}}}
-                            end
-                        end,
-            update_chain(ChainID, UpdateFun);
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-delete_services(ChainID, ServiceNames) ->
-    case no_duplicate_names(ServiceNames) of
-        ok ->
-            UpdateFun = fun(Chain = #chain{services = Services}) ->
-                            case extract_services(ServiceNames, Services) of
-                                {ok, Extracted, Rest} ->
-                                    ok = delete_services_(Extracted),
-                                    NChain = Chain#chain{services = Rest},
-                                    mnesia:write(?CHAIN_TAB, NChain, write);
-                                {error, Reason} ->
-                                    {error, Reason}
-                            end
-                        end,
-            update_chain(ChainID, UpdateFun);
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-update_service(ChainID, ServiceName, NewParams) ->
-    UpdateFun = fun(Chain = #chain{services = Services}) ->
-                    case proplists:get_value(ServiceName, Services, undefined) of
-                        undefined ->
-                            {error, {not_found, {service, ServiceName}}};
-                        #service{type     = Type,
-                                 provider = Provider,
-                                 params   = OriginalParams,
-                                 state    = State} = Service ->
-                            Params = maps:merge(OriginalParams, NewParams),
-                            {ok, #service_type{params_spec = ParamsSpec}} = find_service_type(Type),
-                            NParams = emqx_rule_validator:validate_params(Params, ParamsSpec),
-                            case Provider:update(ChainID, ServiceName, NParams, State) of
-                                {ok, NState} ->
-                                    NService = Service#service{params = Params,
-                                                               state = NState},
-                                    NServices = lists:keyreplace(ServiceName, 1, Services, {ServiceName, NService}),
-                                    ok = mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write),
-                                    {ok, serialize_service({ServiceName, NService})};
-                                {error, Reason} ->
-                                    {error, Reason}
-                            end
-                    end
-                 end,
-    update_chain(ChainID, UpdateFun).
-
-lookup_service(ChainID, ServiceName) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
-        [] ->
-            {error, {not_found, {chain, ChainID}}};
-        [#chain{services = Services}] ->
-            case lists:keytake(ServiceName, 1, Services) of
-                {value, Service, _} ->
-                    {ok, serialize_service(Service)};
-                false ->
-                    {error, {not_found, {service, ServiceName}}}
-            end
-    end.
-
-list_services(ChainID) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
-        [] ->
-            {error, {not_found, {chain, ChainID}}};
-        [#chain{services = Services}] ->
-            {ok, serialize_services(Services)}
-    end.
-
-move_service_to_the_front(ChainID, ServiceName) ->
-    UpdateFun = fun(Chain = #chain{services = Services}) ->
-                    case move_service_to_the_front_(ServiceName, Services) of
-                        {ok, NServices} ->
-                            NChain = Chain#chain{services = NServices},
-                            mnesia:write(?CHAIN_TAB, NChain, write);
-                        {error, Reason} ->
-                            {error, Reason}
-                    end
-                 end,
-    update_chain(ChainID, UpdateFun).
-
-move_service_to_the_end(ChainID, ServiceName) ->
-    UpdateFun = fun(Chain = #chain{services = Services}) ->
-                    case move_service_to_the_end_(ServiceName, Services) of
-                        {ok, NServices} ->
-                            NChain = Chain#chain{services = NServices},
-                            mnesia:write(?CHAIN_TAB, NChain, write);
-                        {error, Reason} ->
-                            {error, Reason}
-                    end
-                 end,
-    update_chain(ChainID, UpdateFun).
-
-move_service_to_the_nth(ChainID, ServiceName, N) ->
-    UpdateFun = fun(Chain = #chain{services = Services}) ->
-                    case move_service_to_the_nth_(ServiceName, Services, N) of
-                        {ok, NServices} ->
-                            NChain = Chain#chain{services = NServices},
-                            mnesia:write(?CHAIN_TAB, NChain, write);
-                        {error, Reason} ->
-                            {error, Reason}
-                    end
-                 end,
-    update_chain(ChainID, UpdateFun).
-
-import_users(ChainID, ServiceName, Filename) ->
-    call_service(ChainID, ServiceName, import_users, [Filename]).
-
-add_user(ChainID, ServiceName, UserInfo) ->
-    call_service(ChainID, ServiceName, add_user, [UserInfo]).
-
-delete_user(ChainID, ServiceName, UserID) ->
-    call_service(ChainID, ServiceName, delete_user, [UserID]).
-
-update_user(ChainID, ServiceName, UserID, NewUserInfo) ->
-    call_service(ChainID, ServiceName, update_user, [UserID, NewUserInfo]).
-
-lookup_user(ChainID, ServiceName, UserID) ->
-    call_service(ChainID, ServiceName, lookup_user, [UserID]).
-
-list_users(ChainID, ServiceName) ->
-    call_service(ChainID, ServiceName, list_users, []).
-
-%%------------------------------------------------------------------------------
-%% Internal functions
-%%------------------------------------------------------------------------------
-
-find_attrs(App, AttrName) ->
-    [{App, Mod, Attr} || {ok, Modules} <- [application:get_key(App, modules)],
-                         Mod <- Modules,
-                         {Name, Attrs} <- module_attributes(Mod), Name =:= AttrName,
-                         Attr <- Attrs].
-
-module_attributes(Module) ->
-    try Module:module_info(attributes)
-    catch
-        error:undef -> []
-    end.
-
-do_register_service_types(ServiceTypes) ->
-    trans(fun lists:foreach/2, [fun insert_service_type/1, ServiceTypes]).
-
-insert_service_type(ServiceType) ->
-    mnesia:write(?SERVICE_TYPE_TAB, ServiceType, write).
-
-find_service_type(Name) ->
-    case mnesia:dirty_read(?SERVICE_TYPE_TAB, Name) of
-        [ServiceType] -> {ok, ServiceType};
-        [] -> {error, not_found}
-    end.
-
-validate_service_params(ServiceParams) ->
-    case validate_service_names(ServiceParams) of
-        ok ->
-            validate_other_service_params(ServiceParams);
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-validate_service_names(ServiceParams) ->
-    Names = [Name || #{name := Name} <- ServiceParams],
-    no_duplicate_names(Names).
-
-validate_other_service_params(ServiceParams) ->
-    validate_other_service_params(ServiceParams, []).
-
-validate_other_service_params([], Acc) ->
-    {ok, lists:reverse(Acc)};
-validate_other_service_params([#{type := Type, params := Params} = ServiceParams | More], Acc) ->
-    case find_service_type(Type) of
-        {ok, #service_type{provider = Provider, params_spec = ParamsSpec}} ->
-            NParams = emqx_rule_validator:validate_params(Params, ParamsSpec),
-            validate_other_service_params(More,
-                                          [ServiceParams#{params => NParams,
-                                                          original_params => Params,
-                                                          provider => Provider} | Acc]);
-        {error, not_found} ->
-            {error, {not_found, {service_type, Type}}}
-    end.
-
-no_duplicate_names(Names) ->
-    no_duplicate_names(Names, #{}).
-
-no_duplicate_names([], _) ->
-    ok;
-no_duplicate_names([Name | More], Acc) ->
-    case maps:is_key(Name, Acc) of
-        false -> no_duplicate_names(More, Acc#{Name => true});
-        true -> {error, {duplicate, Name}}
-    end.
-
-create_services(ChainID, ServiceParams) ->
-    create_services(ChainID, ServiceParams, []).
-
-create_services(_ChainID, [], Acc) ->
-    {ok, lists:reverse(Acc)};
-create_services(ChainID, [#{name := Name,
-                            type := Type,
-                            provider := Provider,
-                            params := Params,
-                            original_params := OriginalParams} | More], Acc) ->
-    case Provider:create(ChainID, Name, Params) of
-        {ok, State} ->
-            Service = #service{name = Name,
-                               type = Type,
-                               provider = Provider,
-                               params = OriginalParams,
-                               state = State},
-            create_services(ChainID, More, [{Name, Service} | Acc]);
-        {error, Reason} ->
-            delete_services_(Acc),
-            {error, Reason}
-    end.
-
-delete_services_([]) ->
-    ok;
-delete_services_([{_, #service{provider = Provider, state = State}} | More]) ->
-    Provider:destroy(State),
-    delete_services_(More).
-
-extract_services(ServiceNames, Services) ->
-    extract_services(ServiceNames, Services, []).
-
-extract_services([], Rest, Extracted) ->
-    {ok, lists:reverse(Extracted), Rest};
-extract_services([ServiceName | More], Services, Acc) ->
-    case lists:keytake(ServiceName, 1, Services) of
-        {value, Extracted, Rest} ->
-            extract_services(More, Rest, [Extracted | Acc]);
-        false ->
-            {error, {not_found, {service, ServiceName}}}
-    end.
-
-move_service_to_the_front_(ServiceName, Services) ->
-    move_service_to_the_front_(ServiceName, Services, []).
-
-move_service_to_the_front_(ServiceName, [], _) ->
-    {error, {not_found, {service, ServiceName}}};
-move_service_to_the_front_(ServiceName, [{ServiceName, _} = Service | More], Passed) ->
-    {ok, [Service | (lists:reverse(Passed) ++ More)]};
-move_service_to_the_front_(ServiceName, [Service | More], Passed) ->
-    move_service_to_the_front_(ServiceName, More, [Service | Passed]).
-
-move_service_to_the_end_(ServiceName, Services) ->
-    move_service_to_the_end_(ServiceName, Services, []).
-
-move_service_to_the_end_(ServiceName, [], _) ->
-    {error, {not_found, {service, ServiceName}}};
-move_service_to_the_end_(ServiceName, [{ServiceName, _} = Service | More], Passed) ->
-    {ok, lists:reverse(Passed) ++ More ++ [Service]};
-move_service_to_the_end_(ServiceName, [Service | More], Passed) ->
-    move_service_to_the_end_(ServiceName, More, [Service | Passed]).
-
-move_service_to_the_nth_(ServiceName, Services, N)
-  when N =< length(Services) andalso N > 0 ->
-    move_service_to_the_nth_(ServiceName, Services, N, []);
-move_service_to_the_nth_(_, _, _) ->
-    {error, out_of_range}.
-
-move_service_to_the_nth_(ServiceName, [], _, _) ->
-    {error, {not_found, {service, ServiceName}}};
-move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed)
-  when N =< length(Passed) ->
-    {L1, L2} = lists:split(N - 1, lists:reverse(Passed)),
-    {ok, L1 ++ [Service] ++ L2 ++ More};
-move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) ->
-    {L1, L2} = lists:split(N - length(Passed) - 1, More),
-    {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2};
-move_service_to_the_nth_(ServiceName, [Service | More], N, Passed) ->
-    move_service_to_the_nth_(ServiceName, More, N, [Service | Passed]).
-
-update_chain(ChainID, UpdateFun) ->
-    trans(
-        fun() ->
-            case mnesia:read(?CHAIN_TAB, ChainID, write) of
-                [] ->
-                    {error, {not_found, {chain, ChainID}}};
-                [Chain] ->
-                    UpdateFun(Chain)
-            end
-        end).
-
-call_service(ChainID, ServiceName, Func, Args) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
-        [] ->
-            {error, {not_found, {chain, ChainID}}};
-        [#chain{services = Services}] ->
-            case proplists:get_value(ServiceName, Services, undefined) of
-                undefined ->
-                    {error, {not_found, {service, ServiceName}}};
-                #service{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.
-
-serialize_chain(#chain{id = ID,
-                       services = Services,
-                       created_at = CreatedAt}) ->
-    #{id => ID,
-      services => serialize_services(Services),
-      created_at => CreatedAt}.
-
-serialize_services(Services) ->
-    [serialize_service(Service) || Service <- Services].
-
-serialize_service({_, #service{name = Name,
-                               type = Type,
-                               params = Params}}) ->
-    #{name => Name,
-      type => Type,
-      params => Params}.
-
-trans(Fun) ->
-    trans(Fun, []).
-
-trans(Fun, Args) ->
-    case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of
-        {atomic, Res} -> Res;
-        {aborted, Reason} -> {error, Reason}
-    end.

+ 0 - 407
apps/emqx_authentication/src/emqx_authentication_api.erl

@@ -1,407 +0,0 @@
-%%--------------------------------------------------------------------
-%% 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_api).
-
--export([ create_chain/2
-        , delete_chain/2
-        , lookup_chain/2
-        , list_chains/2
-        , add_service/2
-        , delete_service/2
-        , update_service/2
-        , lookup_service/2
-        , list_services/2
-        , move_service/2
-        , import_users/2
-        , add_user/2
-        , delete_user/2
-        , update_user/2
-        , lookup_user/2
-        , list_users/2
-        ]).
-
--import(minirest,  [return/1]).
-
--rest_api(#{name   => create_chain,
-            method => 'POST',
-            path   => "/authentication/chains",
-            func   => create_chain,
-            descr  => "Create a chain"
-           }).
-
--rest_api(#{name   => delete_chain,
-            method => 'DELETE',
-            path   => "/authentication/chains/:bin:id",
-            func   => delete_chain,
-            descr  => "Delete chain"
-           }).
-
--rest_api(#{name   => lookup_chain,
-            method => 'GET',
-            path   => "/authentication/chains/:bin:id",
-            func   => lookup_chain,
-            descr  => "Lookup chain"
-           }).
-
--rest_api(#{name   => list_chains,
-            method => 'GET',
-            path   => "/authentication/chains",
-            func   => list_chains,
-            descr  => "List all chains"
-           }).
-
--rest_api(#{name   => add_service,
-            method => 'POST',
-            path   => "/authentication/chains/:bin:id/services",
-            func   => add_service,
-            descr  => "Add service to chain"
-           }).
-
--rest_api(#{name   => delete_service,
-            method => 'DELETE',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name",
-            func   => delete_service,
-            descr  => "Delete service from chain"
-           }).
-
--rest_api(#{name   => update_service,
-            method => 'PUT',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name",
-            func   => update_service,
-            descr  => "Update service in chain"
-           }).
-
--rest_api(#{name   => lookup_service,
-            method => 'GET',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name",
-            func   => lookup_service,
-            descr  => "Lookup service in chain"
-           }).
-
--rest_api(#{name   => list_services,
-            method => 'GET',
-            path   => "/authentication/chains/:bin:id/services",
-            func   => list_services,
-            descr  => "List services in chain"
-           }).
-
--rest_api(#{name   => move_service,
-            method => 'POST',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/position",
-            func   => move_service,
-            descr  => "Change the order of services"
-           }).
-
--rest_api(#{name   => import_users,
-            method => 'POST',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/import-users",
-            func   => import_users,
-            descr  => "Import users"
-           }).
-
--rest_api(#{name   => add_user,
-            method => 'POST',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/users",
-            func   => add_user,
-            descr  => "Add user"
-           }).
-
--rest_api(#{name   => delete_user,
-            method => 'DELETE',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id",
-            func   => delete_user,
-            descr  => "Delete user"
-           }).
-
--rest_api(#{name   => update_user,
-            method => 'PUT',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id",
-            func   => update_user,
-            descr  => "Update user"
-           }).
-
--rest_api(#{name   => lookup_user,
-            method => 'GET',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id",
-            func   => lookup_user,
-            descr  => "Lookup user"
-           }).
-
-%% TODO: Support pagination
--rest_api(#{name   => list_users,
-            method => 'GET',
-            path   => "/authentication/chains/:bin:id/services/:bin:service_name/users",
-            func   => list_users,
-            descr  => "List all users"
-           }).
-
-create_chain(Binding, Params) ->
-    do_create_chain(uri_decode(Binding), maps:from_list(Params)).
-
-do_create_chain(_Binding, #{<<"id">> := ChainID}) ->
-    case emqx_authentication:create_chain(#{id => ChainID}) of
-        {ok, Chain} ->
-            return({ok, Chain});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end;
-do_create_chain(_Binding, _Params) ->
-    return(serialize_error({missing_parameter, id})).
-
-delete_chain(Binding, Params) ->
-    do_delete_chain(uri_decode(Binding), maps:from_list(Params)).
-
-do_delete_chain(#{id := ChainID}, _Params) ->
-    case emqx_authentication:delete_chain(ChainID) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-lookup_chain(Binding, Params) ->
-    do_lookup_chain(uri_decode(Binding), maps:from_list(Params)).
-
-do_lookup_chain(#{id := ChainID}, _Params) ->
-    case emqx_authentication:lookup_chain(ChainID) of
-        {ok, Chain} ->
-            return({ok, Chain});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-list_chains(Binding, Params) ->
-    do_list_chains(uri_decode(Binding), maps:from_list(Params)).
-
-do_list_chains(_Binding, _Params) ->
-    {ok, Chains} = emqx_authentication:list_chains(),
-    return({ok, Chains}).
-
-add_service(Binding, Params) ->
-    do_add_service(uri_decode(Binding), maps:from_list(Params)).
-
-do_add_service(#{id := ChainID}, #{<<"name">> := Name,
-                                   <<"type">> := Type,
-                                   <<"params">> := Params}) ->
-    case emqx_authentication:add_services(ChainID, [#{name => Name,
-                                                      type => binary_to_existing_atom(Type, utf8),
-                                                      params => maps:from_list(Params)}]) of
-        {ok, Services} ->
-            return({ok, Services});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end;
-%% TODO: Check missed field in params
-do_add_service(_Binding, Params) ->
-    Missed = get_missed_params(Params, [<<"name">>, <<"type">>, <<"params">>]),
-    return(serialize_error({missing_parameter, Missed})).
-
-delete_service(Binding, Params) ->
-    do_delete_service(uri_decode(Binding), maps:from_list(Params)).
-
-do_delete_service(#{id := ChainID,
-                    service_name := ServiceName}, _Params) ->
-    case emqx_authentication:delete_services(ChainID, [ServiceName]) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-update_service(Binding, Params) ->
-    do_update_service(uri_decode(Binding), maps:from_list(Params)).
-
-%% TOOD: PUT method supports creation and update
-do_update_service(#{id := ChainID,
-                    service_name := ServiceName}, Params) ->
-    case emqx_authentication:update_service(ChainID, ServiceName, Params) of
-        {ok, Service} ->
-            return({ok, Service});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-lookup_service(Binding, Params) ->
-    do_lookup_service(uri_decode(Binding), maps:from_list(Params)).
-
-do_lookup_service(#{id := ChainID,
-                    service_name := ServiceName}, _Params) ->
-    case emqx_authentication:lookup_service(ChainID, ServiceName) of
-        {ok, Service} ->
-            return({ok, Service});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-list_services(Binding, Params) ->
-    do_list_services(uri_decode(Binding), maps:from_list(Params)).
-
-do_list_services(#{id := ChainID}, _Params) ->
-    case emqx_authentication:list_services(ChainID) of
-        {ok, Services} ->
-            return({ok, Services});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-move_service(Binding, Params) ->
-    do_move_service(uri_decode(Binding), maps:from_list(Params)).
-
-do_move_service(#{id := ChainID,
-                  service_name := ServiceName}, #{<<"position">> := <<"the front">>}) ->
-    case emqx_authentication:move_service_to_the_front(ChainID, ServiceName) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end;
-do_move_service(#{id := ChainID,
-                  service_name := ServiceName}, #{<<"position">> := <<"the end">>}) ->
-    case emqx_authentication:move_service_to_the_end(ChainID, ServiceName) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end;
-do_move_service(#{id := ChainID,
-                  service_name := ServiceName}, #{<<"position">> := N}) when is_number(N) ->
-    case emqx_authentication:move_service_to_the_nth(ChainID, ServiceName, N) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end;
-do_move_service(_Binding, _Params) ->
-    return(serialize_error({missing_parameter, <<"position">>})).
-
-import_users(Binding, Params) ->
-    do_import_users(uri_decode(Binding), maps:from_list(Params)).
-
-do_import_users(#{id := ChainID, service_name := ServiceName},
-                #{<<"filename">> := Filename}) ->
-    case emqx_authentication:import_users(ChainID, ServiceName, Filename) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end;
-do_import_users(_Binding, Params) ->
-    Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]),
-    return(serialize_error({missing_parameter, Missed})).
-
-add_user(Binding, Params) ->
-    do_add_user(uri_decode(Binding), maps:from_list(Params)).
-
-do_add_user(#{id := ChainID,
-              service_name := ServiceName}, UserInfo) ->
-    case emqx_authentication:add_user(ChainID, ServiceName, UserInfo) of
-        {ok, User} ->
-            return({ok, User});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-delete_user(Binding, Params) ->
-    do_delete_user(uri_decode(Binding), maps:from_list(Params)).
-
-do_delete_user(#{id := ChainID,
-                 service_name := ServiceName,
-                 user_id := UserID}, _Params) ->
-    case emqx_authentication:delete_user(ChainID, ServiceName, UserID) of
-        ok ->
-            return(ok);
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-update_user(Binding, Params) ->
-    do_update_user(uri_decode(Binding), maps:from_list(Params)).
-
-do_update_user(#{id := ChainID,
-                 service_name := ServiceName,
-                 user_id := UserID}, NewUserInfo) ->
-    case emqx_authentication:update_user(ChainID, ServiceName, UserID, NewUserInfo) of
-        {ok, User} ->
-            return({ok, User});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-lookup_user(Binding, Params) ->
-    do_lookup_user(uri_decode(Binding), maps:from_list(Params)).
-
-do_lookup_user(#{id := ChainID,
-                 service_name := ServiceName,
-                 user_id := UserID}, _Params) ->
-    case emqx_authentication:lookup_user(ChainID, ServiceName, UserID) of
-        {ok, User} ->
-            return({ok, User});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-list_users(Binding, Params) ->
-    do_list_users(uri_decode(Binding), maps:from_list(Params)).
-
-do_list_users(#{id := ChainID,
-                service_name := ServiceName}, _Params) ->
-    case emqx_authentication:list_users(ChainID, ServiceName) of
-        {ok, Users} ->
-            return({ok, Users});
-        {error, Reason} ->
-            return(serialize_error(Reason))
-    end.
-
-%%------------------------------------------------------------------------------
-%% Internal functions
-%%------------------------------------------------------------------------------
-
-uri_decode(Params) ->
-    maps:fold(fun(K, V, Acc) ->
-                  Acc#{K => emqx_http_lib:uri_decode(V)}
-              end, #{}, Params).
-
-serialize_error({already_exists, {Type, ID}}) ->
-    {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))};
-serialize_error({not_found, {Type, ID}}) ->
-    {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))};
-serialize_error({duplicate, Name}) ->
-    {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name '~s' is duplicated", [Name]))};
-serialize_error({missing_parameter, Names = [_ | Rest]}) ->
-    Format = ["~s," || _ <- Rest] ++ ["~s"],
-    NFormat = binary_to_list(iolist_to_binary(Format)),
-    {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))};
-serialize_error({missing_parameter, Name}) ->
-    {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))};
-serialize_error(_) ->
-    {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}.
-
-serialize_type(service) ->
-    "Service";
-serialize_type(chain) ->
-    "Chain";
-serialize_type(service_type) ->
-    "Service type".
-
-get_missed_params(Actual, Expected) ->
-    Keys = lists:foldl(fun(Key, Acc) ->
-                           case maps:is_key(Key, Actual) of
-                               true -> Acc;
-                               false -> [Key | Acc]
-                           end
-                       end, [], Expected),
-    lists:reverse(Keys).

+ 0 - 409
apps/emqx_authentication/src/emqx_authentication_jwt.erl

@@ -1,409 +0,0 @@
-%%--------------------------------------------------------------------
-%% 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_jwt).
-
--export([ create/3
-        , update/4
-        , authenticate/2
-        , destroy/1
-        ]).
-
--service_type(#{
-    name => jwt,
-    params_spec => #{
-        use_jwks => #{
-            order => 1,
-            type => boolean
-        },
-        jwks_endpoint => #{
-            order => 2,
-            type => string
-        },
-        refresh_interval => #{
-            order => 3,
-            type => number
-        },
-        algorithm => #{
-            order => 3,
-            type => string,
-            enum => [<<"hmac-based">>, <<"public-key">>]
-        },
-        secret => #{
-            order => 4,
-            type => string
-        },
-        secret_base64_encoded => #{
-            order => 5,
-            type => boolean    
-        },
-        jwt_certfile => #{
-            order => 6,
-            type => file
-        },
-        cacertfile => #{
-            order => 7,
-            type => file
-        },
-        keyfile => #{
-            order => 8,
-            type => file
-        },
-        certfile => #{
-            order => 9,
-            type => file
-        },
-        verify => #{
-            order => 10,
-            type => boolean
-        },
-        server_name_indication => #{
-            order => 11,
-            type => string
-        }
-    }
-}).
-
--define(RULES,
-        #{
-            use_jwks               => [],
-            jwks_endpoint          => [use_jwks],
-            refresh_interval       => [use_jwks],
-            algorithm              => [use_jwks],
-            secret                 => [algorithm],
-            secret_base64_encoded  => [algorithm],
-            jwt_certfile           => [algorithm],
-            cacertfile             => [jwks_endpoint],
-            keyfile                => [jwks_endpoint],
-            certfile               => [jwks_endpoint],
-            verify                 => [jwks_endpoint],
-            server_name_indication => [jwks_endpoint],
-            verify_claims          => []
-         }).
-
-create(_ChainID, _ServiceName, Params) ->
-    try handle_options(Params) of
-        Opts ->
-            do_create(Opts)
-    catch
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-update(_ChainID, _ServiceName, Params, State) ->
-    try handle_options(Params) of
-        Opts ->
-            do_update(Opts, State)
-    catch
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK,
-                                                verify_claims := VerifyClaims0}) ->
-    JWKs = case erlang:is_pid(JWK) of
-               false ->
-                   [JWK];
-               true ->
-                   {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(JWK),
-                   JWKs0
-           end,
-    VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo),
-    case verify(JWT, JWKs, VerifyClaims) of
-        ok -> ok;
-        {error, invalid_signature} -> ignore;
-        {error, {claims, _}} -> {stop, bad_passowrd}
-    end.
-
-destroy(#{jwks_connector := undefined}) ->
-    ok;
-destroy(#{jwks_connector := Connector}) ->
-    _ = emqx_authentication_jwks_connector:stop(Connector),
-    ok.
-
-%%--------------------------------------------------------------------
-%% Internal functions
-%%--------------------------------------------------------------------
-
-do_create(#{use_jwks := false,
-            algorithm := 'hmac-based',
-            secret := Secret0,
-            secret_base64_encoded := Base64Encoded} = Opts) ->
-    Secret = case Base64Encoded of
-                 true ->
-                     base64:decode(Secret0);
-                 false ->
-                     Secret0
-             end,
-    JWK = jose_jwk:from_oct(Secret),
-    {ok, #{jwk => JWK,
-           verify_claims => maps:get(verify_claims, Opts)}};
-
-do_create(#{use_jwks := false,
-            algorithm := 'public-key',
-            jwt_certfile := Certfile} = Opts) ->
-    JWK = jose_jwk:from_pem_file(Certfile),
-    {ok, #{jwk => JWK,
-           verify_claims => maps:get(verify_claims, Opts)}};
-
-do_create(#{use_jwks := true} = Opts) ->
-    case emqx_authentication_jwks_connector:start_link(Opts) of
-        {ok, Connector} ->
-            {ok, #{jwk => Connector,
-                   verify_claims => maps:get(verify_claims, Opts)}};
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-do_update(Opts, #{jwk_connector := undefined}) ->
-    do_create(Opts);
-do_update(#{use_jwks := false} = Opts, #{jwk_connector := Connector}) ->
-    _ = emqx_authentication_jwks_connector:stop(Connector),
-    do_create(Opts);
-do_update(#{use_jwks := true} = Opts, #{jwk_connector := Connector} = State) ->
-    ok = emqx_authentication_jwks_connector:update(Connector, Opts),
-    {ok, State}.
-
-replace_placeholder(L, Variables) ->
-    replace_placeholder(L, Variables, []).
-
-replace_placeholder([], _Variables, Acc) ->
-    Acc;
-replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) ->
-    Value = maps:get(PL, Variables),
-    replace_placeholder(More, Variables, [{Name, Value} | Acc]);
-replace_placeholder([{Name, Value} | More], Variables, Acc) ->
-    replace_placeholder(More, Variables, [{Name, Value} | Acc]).
-
-verify(_JWS, [], _VerifyClaims) ->
-    {error, invalid_signature};
-verify(JWS, [JWK | More], VerifyClaims) ->
-    case jose_jws:verify(JWK, JWS) of
-        {true, Payload, _JWS} ->
-            Claims = emqx_json:decode(Payload, [return_maps]),
-            verify_claims(Claims, VerifyClaims);
-        {false, _, _} ->
-            verify(JWS, More, VerifyClaims)
-    end.
-
-verify_claims(Claims, VerifyClaims0) ->
-    Now = os:system_time(seconds),
-    VerifyClaims = [{<<"exp">>, fun(ExpireTime) ->
-                                    Now < ExpireTime
-                                end},
-                    {<<"iat">>, fun(IssueAt) ->
-                                    IssueAt =< Now
-                                end},
-                    {<<"nbf">>, fun(NotBefore) ->
-                                    NotBefore =< Now
-                                end}] ++ VerifyClaims0,
-    do_verify_claims(Claims, VerifyClaims).
-
-do_verify_claims(_Claims, []) ->
-    ok;
-do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) ->
-    case maps:take(Name, Claims) of
-        error ->
-            do_verify_claims(Claims, More);
-        {Value, NClaims} ->
-            case Fun(Value) of
-                true ->
-                    do_verify_claims(NClaims, More);
-                _ ->
-                    {error, {claims, {Name, Value}}}
-            end
-    end;
-do_verify_claims(Claims, [{Name, Value} | More]) ->
-    case maps:take(Name, Claims) of
-        error ->
-             do_verify_claims(Claims, More);
-        {Value, NClaims} ->
-            do_verify_claims(NClaims, More);
-        {Value0, _} ->
-            {error, {claims, {Name, Value0}}}
-    end.
-
-handle_options(Opts0) when is_map(Opts0) ->
-    Ks = maps:fold(fun(K, _, Acc) ->
-                       [atom_to_binary(K, utf8) | Acc]
-                   end, [], ?RULES),
-    Opts1 = maps:to_list(maps:with(Ks, Opts0)),
-    handle_options([{binary_to_existing_atom(K, utf8), V} || {K, V} <- Opts1]);
-
-handle_options(Opts0) when is_list(Opts0) ->
-    Opts1 = add_missing_options(Opts0),
-    process_options({Opts1, [], length(Opts1)}, #{}).
-
-add_missing_options(Opts) ->
-    AllOpts = maps:keys(?RULES),
-    Fun = fun(K, Acc) ->
-               case proplists:is_defined(K, Acc) of
-                   true ->
-                       Acc;
-                   false ->
-                       [{K, unbound} | Acc]
-                  end
-          end,
-    lists:foldl(Fun, Opts, AllOpts).
-
-process_options({[], [], _}, OptsMap) ->
-    OptsMap;
-process_options({[], Skipped, Counter}, OptsMap)
-  when length(Skipped) < Counter ->
-    process_options({Skipped, [], length(Skipped)}, OptsMap);
-process_options({[], _Skipped, _Counter}, _OptsMap) ->
-    throw({error, faulty_configuration});
-process_options({[{K, V} = Opt | More], Skipped, Counter}, OptsMap0) ->
-    case check_dependencies(K, OptsMap0) of
-        true ->
-            OptsMap1 = handle_option(K, V, OptsMap0),
-            process_options({More, Skipped, Counter}, OptsMap1);
-        false ->
-            process_options({More, [Opt | Skipped], Counter}, OptsMap0)
-    end.
-
-%% TODO: This is not a particularly good implementation(K => needless), it needs to be improved
-handle_option(use_jwks, true, OptsMap) ->
-    OptsMap#{use_jwks => true,
-             algorithm => needless};
-handle_option(use_jwks, false, OptsMap) ->
-    OptsMap#{use_jwks => false,
-             jwks_endpoint => needless};      
-handle_option(jwks_endpoint = Opt, unbound, #{use_jwks := true}) ->
-    throw({error, {options, {Opt, unbound}}});
-handle_option(jwks_endpoint, Value, #{use_jwks := true} = OptsMap)
-  when Value =/= unbound ->
-    case emqx_http_lib:uri_parse(Value) of
-        {ok, #{scheme := http}} ->
-            OptsMap#{enable_ssl => false,
-                     jwks_endpoint => Value};
-        {ok, #{scheme := https}} ->
-            OptsMap#{enable_ssl => true,
-                     jwks_endpoint => Value};
-        {error, _Reason} ->
-            throw({error, {options, {jwks_endpoint, Value}}})
-    end;
-handle_option(refresh_interval = Opt, Value0, #{use_jwks := true} = OptsMap) ->
-    Value = validate_option(Opt, Value0),
-    OptsMap#{Opt => Value};
-handle_option(algorithm = Opt, Value0, #{use_jwks := false} = OptsMap) ->
-    Value = validate_option(Opt, Value0),
-    OptsMap#{Opt => Value};
-handle_option(secret = Opt, unbound, #{algorithm := 'hmac-based'}) ->
-    throw({error, {options, {Opt, unbound}}});
-handle_option(secret = Opt, Value, #{algorithm := 'hmac-based'} = OptsMap) ->
-    OptsMap#{Opt => Value};
-handle_option(secret_base64_encoded = Opt, Value0, #{algorithm := 'hmac-based'} = OptsMap) ->
-    Value = validate_option(Opt, Value0),
-    OptsMap#{Opt => Value};
-handle_option(jwt_certfile = Opt, unbound, #{algorithm := 'public-key'}) ->
-    throw({error, {options, {Opt, unbound}}});
-handle_option(jwt_certfile = Opt, Value, #{algorithm := 'public-key'} = OptsMap) ->
-    OptsMap#{Opt => Value};
-handle_option(verify = Opt, Value0, #{enable_ssl := true} = OptsMap) ->
-    Value = validate_option(Opt, Value0),
-    OptsMap#{Opt => Value};
-handle_option(cacertfile = Opt, Value, #{enable_ssl := true} = OptsMap)
-  when Value =/= unbound ->
-    OptsMap#{Opt => Value};
-handle_option(certfile, unbound, #{enable_ssl := true} = OptsMap) ->
-    OptsMap;
-handle_option(certfile = Opt, Value, #{enable_ssl := true} = OptsMap) ->
-    OptsMap#{Opt => Value};
-handle_option(keyfile, unbound, #{enable_ssl := true} = OptsMap) ->
-    OptsMap;
-handle_option(keyfile = Opt, Value, #{enable_ssl := true} = OptsMap) ->
-    OptsMap#{Opt => Value};
-handle_option(server_name_indication = Opt, Value0, #{enable_ssl := true} = OptsMap) ->
-    Value = validate_option(Opt, Value0),
-    OptsMap#{Opt => Value};
-handle_option(verify_claims = Opt, Value0, OptsMap) ->
-    Value = handle_verify_claims(Value0),
-    OptsMap#{Opt => Value};
-handle_option(_Opt, _Value, OptsMap) ->
-    OptsMap.
-
-validate_option(refresh_interval, unbound) ->
-    300;
-validate_option(refresh_interval, Value) when is_integer(Value) ->
-    Value;
-validate_option(algorithm, <<"hmac-based">>) ->
-    'hmac-based';
-validate_option(algorithm, <<"public-key">>) ->
-    'public-key';
-validate_option(secret_base64_encoded, unbound) ->
-    false;
-validate_option(secret_base64_encoded, Value) when is_boolean(Value) ->
-    Value;
-validate_option(verify, unbound) ->
-    verify_none;
-validate_option(verify, true) ->
-    verify_peer;
-validate_option(verify, false) ->
-    verify_none;
-validate_option(server_name_indication, unbound) ->
-    disable;
-validate_option(server_name_indication, <<"disable">>) ->
-    disable;
-validate_option(server_name_indication, Value) when is_list(Value) ->
-    Value;
-validate_option(Opt, Value) ->
-    throw({error, {options, {Opt, Value}}}).
-
-handle_verify_claims(Opts0) ->
-    try handle_verify_claims(Opts0, [])
-    catch
-        error:_ ->
-            throw({error, {options, {verify_claims, Opts0}}})
-    end.
-
-handle_verify_claims([], Acc) ->
-    Acc;
-handle_verify_claims([{Name, Expected0} | More], Acc)
-  when is_binary(Name) andalso is_binary(Expected0) ->
-    Expected = handle_placeholder(Expected0),
-    handle_verify_claims(More, [{Name, Expected} | Acc]).
-
-handle_placeholder(Placeholder0) ->
-    case re:run(Placeholder0, "^\\$\\{[a-z0-9\\_]+\\}$", [{capture, all}]) of
-        {match, [{Offset, Length}]} ->
-            Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3),
-            Placeholder2 = validate_placeholder(Placeholder1),
-            {placeholder, Placeholder2};
-        nomatch ->
-            Placeholder0
-    end.
-
-validate_placeholder(<<"clientid">>) ->
-    clientid;
-validate_placeholder(<<"username">>) ->
-    username.
-
-check_dependencies(Opt, OptsMap) ->
-    case maps:get(Opt, ?RULES) of
-        [] ->
-            true;
-        Deps ->
-            option_already_defined(Opt, OptsMap) orelse
-                dependecies_already_defined(Deps, OptsMap)
-    end.
-
-option_already_defined(Opt, OptsMap) ->
-    maps:get(Opt, OptsMap, unbound) =/= unbound.
-
-dependecies_already_defined(Deps, OptsMap) ->
-    Fun = fun(Opt) -> option_already_defined(Opt, OptsMap) end,
-    lists:all(Fun, Deps).

+ 0 - 189
apps/emqx_authentication/test/emqx_authentication_SUITE.erl

@@ -1,189 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020-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_SUITE).
-
--compile(export_all).
--compile(nowarn_export_all).
-
--include_lib("common_test/include/ct.hrl").
--include_lib("eunit/include/eunit.hrl").
-
--define(AUTH, emqx_authentication).
-
-all() ->
-    emqx_ct:all(?MODULE).
-
-init_per_suite(Config) ->
-    application:set_env(ekka, strict_mode, true),
-    emqx_ct_helpers:start_apps([emqx_authentication]),
-    Config.
-
-end_per_suite(_) ->
-    emqx_ct_helpers:stop_apps([emqx_authentication]),
-    ok.
-
-t_chain(_) ->
-    ChainID = <<"mychain">>,
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})),
-    ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(#{id => ChainID})),
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)),
-    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
-    ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)),
-    ok.
-
-t_service(_) ->
-    ChainID = <<"mychain">>,
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})),
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)),
-
-    ServiceName1 = <<"myservice1">>,
-    ServiceParams1 = #{name => ServiceName1,
-                       type => mnesia,
-                       params => #{
-                           user_id_type => <<"username">>,
-                           password_hash_algorithm => <<"sha256">>}},
-    ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])),
-    ?assertEqual({ok, ServiceParams1}, ?AUTH:lookup_service(ChainID, ServiceName1)),
-    ?assertEqual({ok, [ServiceParams1]}, ?AUTH:list_services(ChainID)),
-    ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services(ChainID, [ServiceParams1])),
-
-    ServiceName2 = <<"myservice2">>,
-    ServiceParams2 = ServiceParams1#{name => ServiceName2},
-    ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])),
-    ?assertMatch({ok, #{id := ChainID, services := [ServiceParams1, ServiceParams2]}}, ?AUTH:lookup_chain(ChainID)),
-    ?assertEqual({ok, ServiceParams2}, ?AUTH:lookup_service(ChainID, ServiceName2)),
-    ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)),
-
-    ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)),
-    ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)),
-    ?assertEqual(ok, ?AUTH:move_service_to_the_end(ChainID, ServiceName2)),
-    ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)),
-    ?assertEqual(ok, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 1)),
-    ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)),
-    ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 3)),
-    ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 0)),
-    ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName1, ServiceName2])),
-    ?assertEqual({ok, []}, ?AUTH:list_services(ChainID)),
-    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
-    ok.
-
-t_mnesia_service(_) ->
-    ChainID = <<"mychain">>,
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})),
-
-    ServiceName = <<"myservice">>,
-    ServiceParams = #{name => ServiceName,
-                      type => mnesia,
-                      params => #{
-                          user_id_type => <<"username">>,
-                          password_hash_algorithm => <<"sha256">>}},
-    ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])),
-
-    UserInfo = #{<<"user_id">> => <<"myuser">>,
-                 <<"password">> => <<"mypass">>},
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)),
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)),
-    ClientInfo = #{chain_id => ChainID,
-			       username => <<"myuser">>,
-			       password => <<"mypass">>},
-    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
-    ClientInfo2 = ClientInfo#{username => <<"baduser">>},
-    ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)),
-    ClientInfo3 = ClientInfo#{password => <<"badpass">>},
-    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)),
-    UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>},
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, ServiceName, <<"myuser">>, UserInfo2)),
-    ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
-    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)),
-    ?assertEqual(ok, ?AUTH:delete_user(ChainID, ServiceName, <<"myuser">>)),
-    ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)),
-
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)),
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)),
-    ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName])),
-    ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])),
-    ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)),
-
-    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
-    ?assertEqual([], ets:tab2list(mnesia_basic_auth)),
-    ok.
-
-t_import(_) ->
-    ChainID = <<"mychain">>,
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})),
-
-    ServiceName = <<"myservice">>,
-    ServiceParams = #{name => ServiceName,
-                      type => mnesia,
-                      params => #{
-                          user_id_type => <<"username">>,
-                          password_hash_algorithm => <<"sha256">>}},
-    ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])),
-
-    Dir = code:lib_dir(emqx_authentication, test),
-    ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]))),
-    ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.csv"]))),
-    ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser1">>)),
-    ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser3">>)),
-    ClientInfo1 = #{chain_id => ChainID,
-			        username => <<"myuser1">>,
-			        password => <<"mypassword1">>},
-    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)),
-    ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
-                               password => <<"mypassword3">>},
-    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)),
-    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
-    ok.
-
-t_multi_mnesia_service(_) ->
-    ChainID = <<"mychain">>,
-    ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})),
-
-    ServiceName1 = <<"myservice1">>,
-    ServiceParams1 = #{name => ServiceName1,
-                       type => mnesia,
-                       params => #{
-                           user_id_type => <<"username">>,
-                           password_hash_algorithm => <<"sha256">>}},
-    ServiceName2 = <<"myservice2">>,
-    ServiceParams2 = #{name => ServiceName2,
-                       type => mnesia,
-                       params => #{
-                           user_id_type => <<"clientid">>,
-                           password_hash_algorithm => <<"sha256">>}},
-    ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])),
-    ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])),
-
-    ?assertEqual({ok, #{user_id => <<"myuser">>}},
-                 ?AUTH:add_user(ChainID, ServiceName1,
-                                #{<<"user_id">> => <<"myuser">>,
-                                  <<"password">> => <<"mypass1">>})),
-    ?assertEqual({ok, #{user_id => <<"myclient">>}},
-                 ?AUTH:add_user(ChainID, ServiceName2,
-                                #{<<"user_id">> => <<"myclient">>,
-                                  <<"password">> => <<"mypass2">>})),
-    ClientInfo1 = #{chain_id => ChainID,
-			        username => <<"myuser">>,
-                    clientid => <<"myclient">>,
-			        password => <<"mypass1">>},
-    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)),
-    ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)),
-    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)),
-    ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
-    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)),
-    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
-    ok.

apps/emqx_authentication/data/user-credentials.csv → apps/emqx_authn/data/user-credentials.csv


apps/emqx_authentication/data/user-credentials.json → apps/emqx_authn/data/user-credentials.json


+ 26 - 0
apps/emqx_authn/etc/emqx_authn.conf

@@ -0,0 +1,26 @@
+authn: {
+    chains: [
+        # {
+        #     id: "chain1"
+        #     type: simple
+        #     authenticators: [
+        #         {
+        #             name: "authenticator1"
+        #             type: built-in-database
+        #             config: {
+        #                 user_id_type: clientid
+        #                 password_hash_algorithm: {
+        #                     name: sha256
+        #                 }
+        #             }
+        #         }
+        #     ]
+        # }
+    ]
+    bindings: [
+        # {
+        #     chain_id: "chain1"
+        #     listeners: ["mqtt-tcp", "mqtt-ssl"]
+        # }
+    ]
+}

+ 67 - 0
apps/emqx_authn/include/emqx_authn.hrl

@@ -0,0 +1,67 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020 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.
+%%--------------------------------------------------------------------
+
+-define(APP, emqx_authn).
+
+-type chain_id() :: binary().
+-type authn_type() :: simple | enhanced.
+-type authenticator_name() :: binary().
+-type authenticator_type() :: mnesia | jwt | mysql | postgresql.
+-type listener_id() :: binary().
+
+-record(authenticator,
+        { name :: authenticator_name()
+        , type :: authenticator_type()
+        , provider :: module()
+        , config :: map()
+        , state :: map()
+        }).
+
+-record(chain,
+        { id :: chain_id()
+        , type :: authn_type()
+        , authenticators :: [{authenticator_name(), #authenticator{}}]
+        , created_at :: integer()
+        }).
+
+-record(binding,
+        { bound :: {listener_id(), authn_type()}
+        , chain_id :: chain_id()
+        }).
+
+-define(AUTH_SHARD, emqx_authn_shard).
+
+-define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)).
+
+-define(CLUSTER_CALL(Module, Func, Args, ResParttern),
+    fun() ->
+        case LocalResult = erlang:apply(Module, Func, Args) of
+            ResParttern ->
+                Nodes = nodes(),
+                {ResL, BadNodes} = rpc:multicall(Nodes, Module, Func, Args, 5000),
+                NResL = lists:zip(Nodes - BadNodes, ResL),
+                Errors = lists:filter(fun({_, ResParttern}) -> false;
+                                         (_) -> true
+                                      end, NResL),
+                OtherErrors = [{BadNode, node_does_not_exist} || BadNode <- BadNodes],
+                case Errors ++ OtherErrors of
+                    [] -> LocalResult;
+                    NErrors -> {error, NErrors}
+                end;
+            ErrorResult ->
+                {error, ErrorResult}
+        end
+    end()).

apps/emqx_authentication/rebar.config → apps/emqx_authn/rebar.config


+ 3 - 3
apps/emqx_authentication/src/emqx_authentication.app.src

@@ -1,10 +1,10 @@
-{application, emqx_authentication,
+{application, emqx_authn,
  [{description, "EMQ X Authentication"},
   {vsn, "0.1.0"},
   {modules, []},
-  {registered, [emqx_authentication_sup, emqx_authentication_registry]},
+  {registered, [emqx_authn_sup, emqx_authn_registry]},
   {applications, [kernel,stdlib]},
-  {mod, {emqx_authentication_app,[]}},
+  {mod, {emqx_authn_app,[]}},
   {env, []},
   {licenses, ["Apache-2.0"]},
   {maintainers, ["EMQ X Team <contact@emqx.io>"]},

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

@@ -0,0 +1,490 @@
+%%--------------------------------------------------------------------
+%% 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_authn).
+
+-include("emqx_authn.hrl").
+
+-export([ enable/0
+        , disable/0
+        ]).
+
+-export([authenticate/1]).
+
+-export([ create_chain/1
+        , delete_chain/1
+        , lookup_chain/1
+        , list_chains/0
+        , bind/2
+        , unbind/2
+        , list_bindings/1
+        , list_bound_chains/1
+        , create_authenticator/2
+        , delete_authenticator/2
+        , update_authenticator/3
+        , lookup_authenticator/2
+        , list_authenticators/1
+        , move_authenticator_to_the_front/2
+        , move_authenticator_to_the_end/2
+        , move_authenticator_to_the_nth/3
+        ]).
+
+-export([ import_users/3
+        , add_user/3
+        , delete_user/3
+        , update_user/4
+        , lookup_user/3
+        , list_users/2
+        ]).
+
+-export([mnesia/1]).
+
+-boot_mnesia({mnesia, [boot]}).
+
+-define(CHAIN_TAB, emqx_authn_chain).
+-define(BINDING_TAB, emqx_authn_binding).
+
+-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}).
+-rlog_shard({?AUTH_SHARD, ?BINDING_TAB}).
+
+%%------------------------------------------------------------------------------
+%% Mnesia bootstrap
+%%------------------------------------------------------------------------------
+
+%% @doc Create or replicate tables.
+-spec(mnesia(boot) -> ok).
+mnesia(boot) ->
+    %% Optimize storage
+    StoreProps = [{ets, [{read_concurrency, true}]}],
+    %% Chain table
+    ok = ekka_mnesia:create_table(?CHAIN_TAB, [
+                {ram_copies, [node()]},
+                {record_name, chain},
+                {local_content, true},
+                {attributes, record_info(fields, chain)},
+                {storage_properties, StoreProps}]),
+    %% Binding table
+    ok = ekka_mnesia:create_table(?BINDING_TAB, [
+                {ram_copies, [node()]},
+                {record_name, binding},
+                {local_content, true},
+                {attributes, record_info(fields, binding)},
+                {storage_properties, StoreProps}]).
+
+enable() ->
+    case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of
+        ok -> ok;
+        {error, already_exists} -> ok
+    end.
+
+disable() ->
+    emqx:unhook('client.authenticate', {?MODULE, authenticate, []}),
+    ok.
+
+authenticate(#{listener_id := ListenerID} = ClientInfo) ->
+    case lookup_chain_by_listener(ListenerID, simple) of
+        {error, _} ->
+            {error, no_authenticators};
+        {ok, ChainID} ->
+            case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+                [#chain{authenticators = []}] ->
+                    {error, no_authenticators};
+                [#chain{authenticators = Authenticators}] ->
+                    do_authenticate(Authenticators, ClientInfo);
+                [] ->
+                    {error, no_authenticators}
+            end
+    end.
+
+do_authenticate([], _) ->
+    {error, user_not_found};
+do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], ClientInfo) ->
+    case Provider:authenticate(ClientInfo, State) of
+        ignore -> do_authenticate(More, ClientInfo);
+        ok -> ok;
+        {ok, NewClientInfo} -> {ok, NewClientInfo};
+        {stop, Reason} -> {error, Reason}
+    end.
+
+create_chain(#{id   := ID,
+               type := Type}) ->
+    trans(
+        fun() ->
+            case mnesia:read(?CHAIN_TAB, ID, write) of
+                [] ->
+                    Chain = #chain{id = ID,
+                                   type = Type,
+                                   authenticators = [],
+                                   created_at = erlang:system_time(millisecond)},
+                    mnesia:write(?CHAIN_TAB, Chain, write),
+                    {ok, serialize_chain(Chain)};
+                [_ | _] ->
+                    {error, {already_exists, {chain, ID}}}
+            end
+        end).
+
+delete_chain(ID) ->
+    trans(
+        fun() ->
+            case mnesia:read(?CHAIN_TAB, ID, write) of
+                [] ->
+                    {error, {not_found, {chain, ID}}};
+                [#chain{authenticators = Authenticators}] ->
+                    _ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators],
+                    mnesia:delete(?CHAIN_TAB, ID, write)
+            end
+        end).
+
+lookup_chain(ID) ->
+    case mnesia:dirty_read(?CHAIN_TAB, ID) of
+        [] ->
+            {error, {not_found, {chain, ID}}};
+        [Chain] ->
+            {ok, serialize_chain(Chain)}
+    end.
+
+list_chains() ->
+    Chains = ets:tab2list(?CHAIN_TAB),
+    {ok, [serialize_chain(Chain) || Chain <- Chains]}.
+
+bind(ChainID, Listeners) ->
+    %% TODO: ensure listener id is valid
+    trans(
+        fun() ->
+            case mnesia:read(?CHAIN_TAB, ChainID, write) of
+                [] ->
+                    {error, {not_found, {chain, ChainID}}};
+                [#chain{type = AuthNType}] ->
+                    Result = lists:foldl(
+                                 fun(ListenerID, Acc) ->
+                                     case mnesia:read(?BINDING_TAB, {ListenerID, AuthNType}, write) of
+                                         [] ->
+                                             Binding = #binding{bound = {ListenerID, AuthNType}, chain_id = ChainID},
+                                             mnesia:write(?BINDING_TAB, Binding, write),
+                                             Acc;
+                                         _ ->
+                                             [ListenerID | Acc]
+                                     end
+                                 end, [], Listeners),
+                    case Result of
+                        [] -> ok;
+                        Listeners0 -> {error, {already_bound, Listeners0}}
+                    end
+            end
+        end).
+
+unbind(ChainID, Listeners) ->
+    trans(
+        fun() ->
+            Result = lists:foldl(
+                        fun(ListenerID, Acc) ->
+                            MatchSpec = [{{binding, {ListenerID, '_'}, ChainID}, [], ['$_']}],
+                            case mnesia:select(?BINDING_TAB, MatchSpec, write) of
+                                [] ->
+                                    [ListenerID | Acc];
+                                [#binding{bound = Bound}] ->
+                                    mnesia:delete(?BINDING_TAB, Bound, write),
+                                    Acc
+                            end
+                        end, [], Listeners),
+            case Result of
+                [] -> ok;
+                Listeners0 ->
+                    {error, {not_found, Listeners0}}
+            end
+        end).
+
+list_bindings(ChainID) ->
+    trans(
+        fun() ->
+            MatchSpec = [{{binding, {'$1', '_'}, ChainID}, [], ['$1']}],
+            Listeners = mnesia:select(?BINDING_TAB, MatchSpec),
+            {ok, #{chain_id => ChainID, listeners => Listeners}}
+        end).
+
+list_bound_chains(ListenerID) ->
+    trans(
+        fun() ->
+            MatchSpec = [{{binding, {ListenerID, '_'}, '_'}, [], ['$_']}],
+            Bindings = mnesia:select(?BINDING_TAB, MatchSpec),
+            Chains = [{AuthNType, ChainID} || #binding{bound = {_, AuthNType},
+                                                    chain_id = ChainID} <- Bindings],
+            {ok, maps:from_list(Chains)}
+        end).
+
+create_authenticator(ChainID, #{name := Name,
+                                type := Type,
+                                config := Config}) ->
+    UpdateFun =
+        fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) ->
+            case lists:keymember(Name, 1, Authenticators) of
+                true ->
+                    {error, {already_exists, {authenticator, Name}}};
+                false ->
+                    Provider = authenticator_provider(AuthNType, Type),
+                    case Provider:create(ChainID, Name, Config) of
+                        {ok, State} ->
+                            Authenticator = #authenticator{name = Name,
+                                                           type = Type,
+                                                           provider = Provider,
+                                                           config = Config,
+                                                           state = State},
+                            NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]},
+                            ok = mnesia:write(?CHAIN_TAB, NChain, write),
+                            {ok, serialize_authenticator(Authenticator)};
+                        {error, Reason} ->
+                            {error, Reason}
+                    end
+            end
+        end,
+    update_chain(ChainID, UpdateFun).
+
+delete_authenticator(ChainID, AuthenticatorName) ->
+    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
+                    case lists:keytake(AuthenticatorName, 1, Authenticators) of
+                        false ->
+                            {error, {not_found, {authenticator, AuthenticatorName}}};
+                        {value, {_, Authenticator}, NAuthenticators} ->
+                            _ = do_delete_authenticator(Authenticator),
+                            NChain = Chain#chain{authenticators = NAuthenticators},
+                            mnesia:write(?CHAIN_TAB, NChain, write)
+                    end
+                end,
+    update_chain(ChainID, UpdateFun).
+
+update_authenticator(ChainID, AuthenticatorName, Config) ->
+    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
+                    case proplists:get_value(AuthenticatorName, Authenticators, undefined) of
+                        undefined ->
+                            {error, {not_found, {authenticator, AuthenticatorName}}};
+                        #authenticator{provider = Provider,
+                                       config   = OriginalConfig,
+                                       state    = State} = Authenticator ->
+                            NewConfig = maps:merge(OriginalConfig, Config),
+                            case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of
+                                {ok, NState} ->
+                                    NAuthenticator = Authenticator#authenticator{config = NewConfig,
+                                                                                 state = NState},
+                                    NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators),
+                                    ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write),
+                                    {ok, serialize_authenticator(NAuthenticator)};
+                                {error, Reason} ->
+                                    {error, Reason}
+                            end
+                    end
+                 end,
+    update_chain(ChainID, UpdateFun).
+
+lookup_authenticator(ChainID, AuthenticatorName) ->
+    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+        [] ->
+            {error, {not_found, {chain, ChainID}}};
+        [#chain{authenticators = Authenticators}] ->
+            case proplists:get_value(AuthenticatorName, Authenticators, undefined) of
+                undefined ->
+                    {error, {not_found, {authenticator, AuthenticatorName}}};
+                Authenticator ->
+                    {ok, serialize_authenticator(Authenticator)}
+            end
+    end.
+
+list_authenticators(ChainID) ->
+    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+        [] ->
+            {error, {not_found, {chain, ChainID}}};
+        [#chain{authenticators = Authenticators}] ->
+            {ok, serialize_authenticators(Authenticators)}
+    end.
+
+move_authenticator_to_the_front(ChainID, AuthenticatorName) ->
+    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
+                    case move_authenticator_to_the_front_(AuthenticatorName, Authenticators) of
+                        {ok, NAuthenticators} ->
+                            NChain = Chain#chain{authenticators = NAuthenticators},
+                            mnesia:write(?CHAIN_TAB, NChain, write);
+                        {error, Reason} ->
+                            {error, Reason}
+                    end
+                 end,
+    update_chain(ChainID, UpdateFun).
+
+move_authenticator_to_the_end(ChainID, AuthenticatorName) ->
+    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
+                    case move_authenticator_to_the_end_(AuthenticatorName, Authenticators) of
+                        {ok, NAuthenticators} ->
+                            NChain = Chain#chain{authenticators = NAuthenticators},
+                            mnesia:write(?CHAIN_TAB, NChain, write);
+                        {error, Reason} ->
+                            {error, Reason}
+                    end
+                 end,
+    update_chain(ChainID, UpdateFun).
+
+move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) ->
+    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
+                    case move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) of
+                        {ok, NAuthenticators} ->
+                            NChain = Chain#chain{authenticators = NAuthenticators},
+                            mnesia:write(?CHAIN_TAB, NChain, write);
+                        {error, Reason} ->
+                            {error, Reason}
+                    end
+                 end,
+    update_chain(ChainID, UpdateFun).
+
+import_users(ChainID, AuthenticatorName, Filename) ->
+    call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]).
+
+add_user(ChainID, AuthenticatorName, UserInfo) ->
+    call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]).
+
+delete_user(ChainID, AuthenticatorName, UserID) ->
+    call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]).
+
+update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) ->
+    call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]).
+
+lookup_user(ChainID, AuthenticatorName, UserID) ->
+    call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]).
+
+list_users(ChainID, AuthenticatorName) ->
+    call_authenticator(ChainID, AuthenticatorName, list_users, []).
+
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+authenticator_provider(simple, 'built-in-database') -> emqx_authn_mnesia;
+authenticator_provider(simple, jwt) -> emqx_authn_jwt;
+authenticator_provider(simple, mysql) -> emqx_authn_mysql;
+authenticator_provider(simple, postgresql) -> emqx_authn_pgsql.
+
+% authenticator_provider(enhanced, 'enhanced-built-in-database') -> emqx_enhanced_authn_mnesia.
+
+do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
+    Provider:destroy(State).
+    
+update_value(Key, Value, List) ->
+    lists:keyreplace(Key, 1, List, {Key, Value}).
+
+move_authenticator_to_the_front_(AuthenticatorName, Authenticators) ->
+    move_authenticator_to_the_front_(AuthenticatorName, Authenticators, []).
+
+move_authenticator_to_the_front_(AuthenticatorName, [], _) ->
+    {error, {not_found, {authenticator, AuthenticatorName}}};
+move_authenticator_to_the_front_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) ->
+    {ok, [Authenticator | (lists:reverse(Passed) ++ More)]};
+move_authenticator_to_the_front_(AuthenticatorName, [Authenticator | More], Passed) ->
+    move_authenticator_to_the_front_(AuthenticatorName, More, [Authenticator | Passed]).
+
+move_authenticator_to_the_end_(AuthenticatorName, Authenticators) ->
+    move_authenticator_to_the_end_(AuthenticatorName, Authenticators, []).
+
+move_authenticator_to_the_end_(AuthenticatorName, [], _) ->
+    {error, {not_found, {authenticator, AuthenticatorName}}};
+move_authenticator_to_the_end_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) ->
+    {ok, lists:reverse(Passed) ++ More ++ [Authenticator]};
+move_authenticator_to_the_end_(AuthenticatorName, [Authenticator | More], Passed) ->
+    move_authenticator_to_the_end_(AuthenticatorName, More, [Authenticator | Passed]).
+
+move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N)
+  when N =< length(Authenticators) andalso N > 0 ->
+    move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N, []);
+move_authenticator_to_the_nth_(_, _, _) ->
+    {error, out_of_range}.
+
+move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) ->
+    {error, {not_found, {authenticator, AuthenticatorName}}};
+move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed)
+  when N =< length(Passed) ->
+    {L1, L2} = lists:split(N - 1, lists:reverse(Passed)),
+    {ok, L1 ++ [Authenticator] ++ L2 ++ More};
+move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) ->
+    {L1, L2} = lists:split(N - length(Passed) - 1, More),
+    {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2};
+move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) ->
+    move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]).
+
+update_chain(ChainID, UpdateFun) ->
+    trans(
+        fun() ->
+            case mnesia:read(?CHAIN_TAB, ChainID, write) of
+                [] ->
+                    {error, {not_found, {chain, ChainID}}};
+                [Chain] ->
+                    UpdateFun(Chain)
+            end
+        end).
+
+lookup_chain_by_listener(ListenerID, AuthNType) ->
+    case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of
+        [] ->
+            {error, not_found};
+        [#binding{chain_id = ChainID}] ->
+            {ok, ChainID}
+    end.
+
+
+call_authenticator(ChainID, AuthenticatorName, Func, Args) ->
+    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+        [] ->
+            {error, {not_found, {chain, ChainID}}};
+        [#chain{authenticators = Authenticators}] ->
+            case proplists:get_value(AuthenticatorName, Authenticators, undefined) of
+                undefined ->
+                    {error, {not_found, {authenticator, AuthenticatorName}}};
+                #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.
+
+serialize_chain(#chain{id = ID,
+                       type = Type,
+                       authenticators = Authenticators,
+                       created_at = CreatedAt}) ->
+    #{id => ID,
+      type => Type,
+      authenticators => serialize_authenticators(Authenticators),
+      created_at => CreatedAt}.
+
+% serialize_binding(#binding{bound = {ListenerID, _},
+%                            chain_id = ChainID}) ->
+%     #{listener_id => ListenerID,
+%       chain_id => ChainID}.
+
+serialize_authenticators(Authenticators) ->
+    [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators].
+
+serialize_authenticator(#authenticator{name = Name,
+                                       type = Type,
+                                       config = Config}) ->
+    #{name => Name,
+      type => Type,
+      config => Config}.
+
+trans(Fun) ->
+    trans(Fun, []).
+
+trans(Fun, Args) ->
+    case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of
+        {atomic, Res} -> Res;
+        {aborted, Reason} -> {error, Reason}
+    end.

+ 544 - 0
apps/emqx_authn/src/emqx_authn_api.erl

@@ -0,0 +1,544 @@
+%%--------------------------------------------------------------------
+%% 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_authn_api).
+
+-include("emqx_authn.hrl").
+
+-export([ create_chain/2
+        , delete_chain/2
+        , lookup_chain/2
+        , list_chains/2
+        , bind/2
+        , unbind/2
+        , list_bindings/2
+        , list_bound_chains/2
+        , create_authenticator/2
+        , delete_authenticator/2
+        , update_authenticator/2
+        , lookup_authenticator/2
+        , list_authenticators/2
+        , move_authenticator/2
+        , import_users/2
+        , add_user/2
+        , delete_user/2
+        , update_user/2
+        , lookup_user/2
+        , list_users/2
+        ]).
+
+-import(minirest,  [return/1]).
+
+-rest_api(#{name   => create_chain,
+            method => 'POST',
+            path   => "/authentication/chains",
+            func   => create_chain,
+            descr  => "Create a chain"
+           }).
+
+-rest_api(#{name   => delete_chain,
+            method => 'DELETE',
+            path   => "/authentication/chains/:bin:id",
+            func   => delete_chain,
+            descr  => "Delete chain"
+           }).
+
+-rest_api(#{name   => lookup_chain,
+            method => 'GET',
+            path   => "/authentication/chains/:bin:id",
+            func   => lookup_chain,
+            descr  => "Lookup chain"
+           }).
+
+-rest_api(#{name   => list_chains,
+            method => 'GET',
+            path   => "/authentication/chains",
+            func   => list_chains,
+            descr  => "List all chains"
+           }).
+
+-rest_api(#{name   => bind,
+            method => 'POST',
+            path   => "/authentication/chains/:bin:id/bindings/bulk",
+            func   => bind,
+            descr  => "Bind"
+           }).
+
+-rest_api(#{name   => unbind,
+            method => 'DELETE',
+            path   => "/authentication/chains/:bin:id/bindings/bulk",
+            func   => unbind,
+            descr  => "Unbind"
+           }).
+
+-rest_api(#{name   => list_bindings,
+            method => 'GET',
+            path   => "/authentication/chains/:bin:id/bindings",
+            func   => list_bindings,
+            descr  => "List bindings"
+           }).
+
+-rest_api(#{name   => list_bound_chains,
+            method => 'GET',
+            path   => "/authentication/listeners/:bin:listener_id/bound_chains",
+            func   => list_bound_chains,
+            descr  => "List bound chains"
+           }).
+
+-rest_api(#{name   => create_authenticator,
+            method => 'POST',
+            path   => "/authentication/chains/:bin:id/authenticators",
+            func   => create_authenticator,
+            descr  => "Create authenticator to chain"
+           }).
+
+-rest_api(#{name   => delete_authenticator,
+            method => 'DELETE',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name",
+            func   => delete_authenticator,
+            descr  => "Delete authenticator from chain"
+           }).
+
+-rest_api(#{name   => update_authenticator,
+            method => 'PUT',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name",
+            func   => update_authenticator,
+            descr  => "Update authenticator in chain"
+           }).
+
+-rest_api(#{name   => lookup_authenticator,
+            method => 'GET',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name",
+            func   => lookup_authenticator,
+            descr  => "Lookup authenticator in chain"
+           }).
+
+-rest_api(#{name   => list_authenticators,
+            method => 'GET',
+            path   => "/authentication/chains/:bin:id/authenticators",
+            func   => list_authenticators,
+            descr  => "List authenticators in chain"
+           }).
+
+-rest_api(#{name   => move_authenticator,
+            method => 'POST',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position",
+            func   => move_authenticator,
+            descr  => "Change the order of authenticators"
+           }).
+
+-rest_api(#{name   => import_users,
+            method => 'POST',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/import-users",
+            func   => import_users,
+            descr  => "Import users"
+           }).
+
+-rest_api(#{name   => add_user,
+            method => 'POST',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users",
+            func   => add_user,
+            descr  => "Add user"
+           }).
+
+-rest_api(#{name   => delete_user,
+            method => 'DELETE',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id",
+            func   => delete_user,
+            descr  => "Delete user"
+           }).
+
+-rest_api(#{name   => update_user,
+            method => 'PUT',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id",
+            func   => update_user,
+            descr  => "Update user"
+           }).
+
+-rest_api(#{name   => lookup_user,
+            method => 'GET',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id",
+            func   => lookup_user,
+            descr  => "Lookup user"
+           }).
+
+%% TODO: Support pagination
+-rest_api(#{name   => list_users,
+            method => 'GET',
+            path   => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users",
+            func   => list_users,
+            descr  => "List all users"
+           }).
+
+create_chain(Binding, Params) ->
+    do_create_chain(uri_decode(Binding), maps:from_list(Params)).
+
+do_create_chain(_Binding, Chain0) ->
+    Config = #{<<"authn">> => #{<<"chains">> => [Chain0#{<<"authenticators">> => []}],
+                                <<"bindings">> => []}},
+    #{authn := #{chains := [Chain1]}}
+                = hocon_schema:check_plain(emqx_authn_schema, Config,
+                                           #{atom_key => true, nullable => true}),
+    case emqx_authn:create_chain(Chain1) of
+        {ok, Chain2} ->
+            return({ok, Chain2});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+delete_chain(Binding, Params) ->
+    do_delete_chain(uri_decode(Binding), maps:from_list(Params)).
+
+do_delete_chain(#{id := ChainID}, _Params) ->
+    case emqx_authn:delete_chain(ChainID) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+lookup_chain(Binding, Params) ->
+    do_lookup_chain(uri_decode(Binding), maps:from_list(Params)).
+
+do_lookup_chain(#{id := ChainID}, _Params) ->
+    case emqx_authn:lookup_chain(ChainID) of
+        {ok, Chain} ->
+            return({ok, Chain});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+list_chains(Binding, Params) ->
+    do_list_chains(uri_decode(Binding), maps:from_list(Params)).
+
+do_list_chains(_Binding, _Params) ->
+    {ok, Chains} = emqx_authn:list_chains(),
+    return({ok, Chains}).
+
+bind(Binding, Params) ->
+    do_bind(uri_decode(Binding), lists_to_map(Params)).
+
+do_bind(#{id := ChainID}, #{<<"listeners">> := Listeners}) ->
+    % Config = #{<<"authn">> => #{<<"chains">> => [],
+    %                             <<"bindings">> => [#{<<"chain">> := ChainID,
+    %                                                  <<"listeners">> := Listeners}]}},
+    % #{authn := #{bindings := [#{listeners := Listeners}]}}
+    %             = hocon_schema:check_plain(emqx_authn_schema, Config,
+    %                                        #{atom_key => true, nullable => true}),
+    case emqx_authn:bind(ChainID, Listeners) of
+        ok ->
+            return(ok);
+        {error, {alread_bound, Listeners}} ->
+            {ok, #{code => <<"ALREADY_EXISTS">>,
+                   message => <<"ALREADY_BOUND">>,
+                   detail => Listeners}};
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end;
+do_bind(_, _) ->
+    return(serialize_error({missing_parameter, <<"listeners">>})).
+
+unbind(Binding, Params) ->
+    do_unbind(uri_decode(Binding), lists_to_map(Params)).
+
+do_unbind(#{id := ChainID}, #{<<"listeners">> := Listeners0}) ->
+    case emqx_authn:unbind(ChainID, Listeners0) of
+        ok ->
+            return(ok);
+        {error, {not_found, Listeners1}} ->
+            {ok, #{code => <<"NOT_FOUND">>,
+                   detail => Listeners1}};
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end;
+do_unbind(_, _) ->
+    return(serialize_error({missing_parameter, <<"listeners">>})).
+
+list_bindings(Binding, Params) ->
+    do_list_bindings(uri_decode(Binding), lists_to_map(Params)).
+
+do_list_bindings(#{id := ChainID}, _) ->
+    {ok, Binding} = emqx_authn:list_bindings(ChainID),
+    return({ok, Binding}).
+
+list_bound_chains(Binding, Params) ->
+    do_list_bound_chains(uri_decode(Binding), lists_to_map(Params)).
+
+do_list_bound_chains(#{listener_id := ListenerID}, _) ->
+    {ok, Chains} = emqx_authn:list_bound_chains(ListenerID),
+    return({ok, Chains}).
+
+create_authenticator(Binding, Params) ->
+    do_create_authenticator(uri_decode(Binding), lists_to_map(Params)).
+
+do_create_authenticator(#{id := ChainID}, Authenticator0) ->
+    case emqx_authn:lookup_chain(ChainID) of
+        {ok, #{type := Type}} ->
+            Chain = #{<<"id">> => ChainID,
+                      <<"type">> => Type,
+                      <<"authenticators">> => [Authenticator0]},
+            Config = #{<<"authn">> => #{<<"chains">> => [Chain],
+                                        <<"bindings">> => []}},
+            #{authn := #{chains := [#{authenticators := [Authenticator1]}]}}
+                = hocon_schema:check_plain(emqx_authn_schema, Config,
+                                           #{atom_key => true, nullable => true}),
+            case emqx_authn:create_authenticator(ChainID, Authenticator1) of
+                {ok, Authenticator2} ->
+                    return({ok, Authenticator2});
+                {error, Reason} ->
+                    return(serialize_error(Reason))
+            end;
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+delete_authenticator(Binding, Params) ->
+    do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)).
+
+do_delete_authenticator(#{id := ChainID,
+                          authenticator_name := AuthenticatorName}, _Params) ->
+    case emqx_authn:delete_authenticator(ChainID, AuthenticatorName) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+%% TODO: Support incremental update
+update_authenticator(Binding, Params) ->
+    do_update_authenticator(uri_decode(Binding), lists_to_map(Params)).
+
+%% TOOD: PUT method supports creation and update
+do_update_authenticator(#{id := ChainID,
+                          authenticator_name := AuthenticatorName}, AuthenticatorConfig0) ->
+    case emqx_authn:lookup_chain(ChainID) of
+        {ok, #{type := ChainType}} ->
+            case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of
+                {ok, #{type := Type}} ->
+                    Authenticator = #{<<"name">> => AuthenticatorName,
+                                      <<"type">> => Type,
+                                      <<"config">> => AuthenticatorConfig0},
+                    Chain = #{<<"id">> => ChainID,
+                              <<"type">> => ChainType,
+                              <<"authenticators">> => [Authenticator]},
+                    Config = #{<<"authn">> => #{<<"chains">> => [Chain],
+                                                <<"bindings">> => []}},
+                    #{
+                        authn := #{
+                            chains := [#{
+                                authenticators := [#{
+                                    config := AuthenticatorConfig1
+                                }]
+                            }]
+                        }
+                    } = hocon_schema:check_plain(emqx_authn_schema, Config,
+                                                 #{atom_key => true, nullable => true}),
+                    case emqx_authn:update_authenticator(ChainID, AuthenticatorName, AuthenticatorConfig1) of
+                        {ok, NAuthenticator} ->
+                            return({ok, NAuthenticator});
+                        {error, Reason} ->
+                            return(serialize_error(Reason))
+                    end;
+                {error, Reason} ->
+                    return(serialize_error(Reason))
+            end;
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+lookup_authenticator(Binding, Params) ->
+    do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)).
+
+do_lookup_authenticator(#{id := ChainID,
+                    authenticator_name := AuthenticatorName}, _Params) ->
+    case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of
+        {ok, Authenticator} ->
+            return({ok, Authenticator});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+list_authenticators(Binding, Params) ->
+    do_list_authenticators(uri_decode(Binding), maps:from_list(Params)).
+
+do_list_authenticators(#{id := ChainID}, _Params) ->
+    case emqx_authn:list_authenticators(ChainID) of
+        {ok, Authenticators} ->
+            return({ok, Authenticators});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+move_authenticator(Binding, Params) ->
+    do_move_authenticator(uri_decode(Binding), maps:from_list(Params)).
+
+do_move_authenticator(#{id := ChainID,
+                  authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the front">>}) ->
+    case emqx_authn:move_authenticator_to_the_front(ChainID, AuthenticatorName) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end;
+do_move_authenticator(#{id := ChainID,
+                  authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the end">>}) ->
+    case emqx_authn:move_authenticator_to_the_end(ChainID, AuthenticatorName) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end;
+do_move_authenticator(#{id := ChainID,
+                  authenticator_name := AuthenticatorName}, #{<<"position">> := N}) when is_number(N) ->
+    case emqx_authn:move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end;
+do_move_authenticator(_Binding, _Params) ->
+    return(serialize_error({missing_parameter, <<"position">>})).
+
+import_users(Binding, Params) ->
+    do_import_users(uri_decode(Binding), maps:from_list(Params)).
+
+do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName},
+                #{<<"filename">> := Filename}) ->
+    case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end;
+do_import_users(_Binding, Params) ->
+    Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]),
+    return(serialize_error({missing_parameter, Missed})).
+
+add_user(Binding, Params) ->
+    do_add_user(uri_decode(Binding), maps:from_list(Params)).
+
+do_add_user(#{id := ChainID,
+              authenticator_name := AuthenticatorName}, UserInfo) ->
+    case emqx_authn:add_user(ChainID, AuthenticatorName, UserInfo) of
+        {ok, User} ->
+            return({ok, User});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+delete_user(Binding, Params) ->
+    do_delete_user(uri_decode(Binding), maps:from_list(Params)).
+
+do_delete_user(#{id := ChainID,
+                 authenticator_name := AuthenticatorName,
+                 user_id := UserID}, _Params) ->
+    case emqx_authn:delete_user(ChainID, AuthenticatorName, UserID) of
+        ok ->
+            return(ok);
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+update_user(Binding, Params) ->
+    do_update_user(uri_decode(Binding), maps:from_list(Params)).
+
+do_update_user(#{id := ChainID,
+                 authenticator_name := AuthenticatorName,
+                 user_id := UserID}, NewUserInfo) ->
+    case emqx_authn:update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) of
+        {ok, User} ->
+            return({ok, User});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+lookup_user(Binding, Params) ->
+    do_lookup_user(uri_decode(Binding), maps:from_list(Params)).
+
+do_lookup_user(#{id := ChainID,
+                 authenticator_name := AuthenticatorName,
+                 user_id := UserID}, _Params) ->
+    case emqx_authn:lookup_user(ChainID, AuthenticatorName, UserID) of
+        {ok, User} ->
+            return({ok, User});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+list_users(Binding, Params) ->
+    do_list_users(uri_decode(Binding), maps:from_list(Params)).
+
+do_list_users(#{id := ChainID,
+                authenticator_name := AuthenticatorName}, _Params) ->
+    case emqx_authn:list_users(ChainID, AuthenticatorName) of
+        {ok, Users} ->
+            return({ok, Users});
+        {error, Reason} ->
+            return(serialize_error(Reason))
+    end.
+
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+uri_decode(Params) ->
+    maps:fold(fun(K, V, Acc) ->
+                  Acc#{K => emqx_http_lib:uri_decode(V)}
+              end, #{}, Params).
+
+lists_to_map(L) ->
+    lists_to_map(L, #{}).
+
+lists_to_map([], Acc) ->
+    Acc;
+lists_to_map([{K, V} | More], Acc) when is_list(V) ->
+    NV = lists_to_map(V),
+    lists_to_map(More, Acc#{K => NV});
+lists_to_map([{K, V} | More], Acc) ->
+    lists_to_map(More, Acc#{K => V});
+lists_to_map([_ | _] = L, _) ->
+    L.
+
+serialize_error({already_exists, {Type, ID}}) ->
+    {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))};
+serialize_error({not_found, {Type, ID}}) ->
+    {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))};
+serialize_error({duplicate, Name}) ->
+    {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Authenticator name '~s' is duplicated", [Name]))};
+serialize_error({missing_parameter, Names = [_ | Rest]}) ->
+    Format = ["~s," || _ <- Rest] ++ ["~s"],
+    NFormat = binary_to_list(iolist_to_binary(Format)),
+    {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))};
+serialize_error({missing_parameter, Name}) ->
+    {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))};
+serialize_error(_) ->
+    {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}.
+
+serialize_type(authenticator) ->
+    "Authenticator";
+serialize_type(chain) ->
+    "Chain";
+serialize_type(authenticator_type) ->
+    "Authenticator type".
+
+get_missed_params(Actual, Expected) ->
+    Keys = lists:foldl(fun(Key, Acc) ->
+                           case maps:is_key(Key, Actual) of
+                               true -> Acc;
+                               false -> [Key | Acc]
+                           end
+                       end, [], Expected),
+    lists:reverse(Keys).

+ 80 - 0
apps/emqx_authn/src/emqx_authn_app.erl

@@ -0,0 +1,80 @@
+%%--------------------------------------------------------------------
+%% 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_authn_app).
+
+-include("emqx_authn.hrl").
+-include_lib("emqx/include/logger.hrl").
+
+-behaviour(application).
+
+-emqx_plugin(?MODULE).
+
+%% Application callbacks
+-export([ start/2
+        , stop/1
+        ]).
+
+start(_StartType, _StartArgs) ->
+    {ok, Sup} = emqx_authn_sup:start_link(),
+    ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
+    initialize(),
+    {ok, Sup}.
+
+stop(_State) ->
+    ok.
+
+initialize() ->
+    ConfFile = filename:join([emqx:get_env(plugins_etc_dir), ?APP]) ++ ".conf",
+    {ok, RawConfig} = hocon:load(ConfFile),
+    #{authn := #{chains := Chains,
+                 bindings := Bindings}}
+        = hocon_schema:check_plain(emqx_authn_schema, RawConfig, #{atom_key => true, nullable => true}),
+    initialize_chains(Chains),
+    initialize_bindings(Bindings).
+
+initialize_chains([]) ->
+    ok;
+initialize_chains([#{id := ChainID,
+                     type := Type,
+                     authenticators := Authenticators} | More]) ->
+    case emqx_authn:create_chain(#{id => ChainID,
+                                   type => Type}) of
+        {ok, _} ->
+            initialize_authenticators(ChainID, Authenticators),
+            initialize_chains(More);
+        {error, Reason} ->
+            ?LOG(error, "Failed to create chain '~s': ~p", [ChainID, Reason])
+    end.
+
+initialize_authenticators(_ChainID, []) ->
+    ok;
+initialize_authenticators(ChainID, [#{name := Name} = Authenticator | More]) ->
+    case emqx_authn:create_authenticator(ChainID, Authenticator) of
+        {ok, _} ->
+            initialize_authenticators(ChainID, More);
+        {error, Reason} ->
+            ?LOG(error, "Failed to create authenticator '~s' in chain '~s': ~p", [Name, ChainID, Reason])
+    end.
+
+initialize_bindings([]) ->
+    ok;
+initialize_bindings([#{chain_id := ChainID, listeners := Listeners} | More]) ->
+    case emqx_authn:bind(Listeners, ChainID) of
+        ok -> initialize_bindings(More);
+        {error, Reason} ->
+           ?LOG(error, "Failed to bind: ~p", [Reason])
+    end.

+ 114 - 0
apps/emqx_authn/src/emqx_authn_schema.erl

@@ -0,0 +1,114 @@
+%%--------------------------------------------------------------------
+%% 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_authn_schema).
+
+-include("emqx_authn.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([structs/0, fields/1]).
+
+-reflect_type([ chain_id/0
+              , authenticator_name/0
+              ]).
+
+structs() -> [authn].
+
+fields(authn) ->
+    [ {chains, fun chains/1}
+    , {bindings, fun bindings/1}];
+
+fields('simple-chain') ->
+    [ {id, fun chain_id/1}
+    , {type, {enum, [simple]}}
+    , {authenticators, fun simple_authenticators/1}
+    ];
+
+% fields('enhanced-chain') ->
+%     [ {id, fun chain_id/1}
+%     , {type, {enum, [enhanced]}}
+%     , {authenticators, fun enhanced_authenticators/1}
+%     ];
+
+fields(binding) ->
+    [ {chain_id, fun chain_id/1}
+    , {listeners, fun listeners/1}
+    ];
+
+fields('built-in-database') ->
+    [ {name, fun authenticator_name/1}
+    , {type, {enum, ['built-in-database']}}
+    , {config, hoconsc:t(hoconsc:ref(emqx_authn_mnesia, config))}
+    ];
+
+% fields('enhanced-built-in-database') ->
+%     [ {name, fun authenticator_name/1}
+%     , {type, {enum, ['built-in-database']}}
+%     , {config, hoconsc:t(hoconsc:ref(emqx_enhanced_authn_mnesia, config))}
+%     ];
+
+fields(jwt) ->
+    [ {name, fun authenticator_name/1}
+    , {type, {enum, [jwt]}}
+    , {config, hoconsc:t(hoconsc:ref(emqx_authn_jwt, config))}
+    ];
+
+fields(mysql) ->
+    [ {name, fun authenticator_name/1}
+    , {type, {enum, [mysql]}}
+    , {config, hoconsc:t(hoconsc:ref(emqx_authn_mysql, config))}
+    ];
+
+fields(pgsql) ->
+    [ {name, fun authenticator_name/1}
+    , {type, {enum, [postgresql]}}
+    , {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))}
+    ].
+
+chains(type) -> hoconsc:array({union, [hoconsc:ref('simple-chain')]});
+chains(default) -> [];
+chains(_) -> undefined.
+
+chain_id(type) -> chain_id();
+chain_id(nullable) -> false;
+chain_id(_) -> undefined.
+
+simple_authenticators(type) ->
+    hoconsc:array({union, [ hoconsc:ref('built-in-database')
+                          , hoconsc:ref(jwt)
+                          , hoconsc:ref(mysql)
+                          , hoconsc:ref(pgsql)]});
+simple_authenticators(default) -> [];
+simple_authenticators(_) -> undefined.
+
+% enhanced_authenticators(type) ->
+%     hoconsc:array({union, [hoconsc:ref('enhanced-built-in-database')]});
+% enhanced_authenticators(default) -> [];
+% enhanced_authenticators(_) -> undefined.
+
+authenticator_name(type) -> authenticator_name();
+authenticator_name(nullable) -> false;
+authenticator_name(_) -> undefined.
+
+bindings(type) -> hoconsc:array(hoconsc:ref(binding));
+bindings(default) -> [];
+bindings(_) -> undefined.
+
+listeners(type) -> hoconsc:array(binary());
+listeners(default) -> [];
+listeners(_) -> undefined.

+ 1 - 1
apps/emqx_authentication/src/emqx_authentication_sup.erl

@@ -14,7 +14,7 @@
 %% limitations under the License.
 %%--------------------------------------------------------------------
 
--module(emqx_authentication_sup).
+-module(emqx_authn_sup).
 
 -behaviour(supervisor).
 

+ 55 - 0
apps/emqx_authn/src/emqx_authn_utils.erl

@@ -0,0 +1,55 @@
+%%--------------------------------------------------------------------
+%% 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_authn_utils).
+
+-export([ replace_placeholder/2
+        ]).
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+replace_placeholder(PlaceHolders, Data) ->
+    replace_placeholder(PlaceHolders, Data, []).
+
+replace_placeholder([], _Data, Acc) ->
+    lists:reverse(Acc);
+replace_placeholder([<<"${mqtt-username}">> | More], #{username := Username} = Data, Acc) ->
+    replace_placeholder(More, Data, [convert_to_sql_param(Username) | Acc]);
+replace_placeholder([<<"${mqtt-clientid}">> | More], #{clientid := ClientID} = Data, Acc) ->
+    replace_placeholder(More, Data, [convert_to_sql_param(ClientID) | Acc]);
+replace_placeholder([<<"${ip-address}">> | More], #{peerhost := IPAddress} = Data, Acc) ->
+    replace_placeholder(More, Data, [convert_to_sql_param(IPAddress) | Acc]);
+replace_placeholder([<<"${cert-subject}">> | More], #{dn := Subject} = Data, Acc) ->
+    replace_placeholder(More, Data, [convert_to_sql_param(Subject) | Acc]);
+replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Data, Acc) ->
+    replace_placeholder(More, Data, [convert_to_sql_param(CommonName) | Acc]);
+replace_placeholder([_ | More], Data, Acc) ->
+    replace_placeholder(More, Data, [null | Acc]).
+
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+convert_to_sql_param(undefined) ->
+    null;
+convert_to_sql_param(V) ->
+    bin(V).
+
+bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
+bin(L) when is_list(L) -> list_to_binary(L);
+bin(X) -> X.

+ 1 - 21
apps/emqx_authentication/src/emqx_authentication_app.erl

@@ -14,24 +14,4 @@
 %% limitations under the License.
 %%--------------------------------------------------------------------
 
--module(emqx_authentication_app).
-
--behaviour(application).
-
--emqx_plugin(?MODULE).
-
--include("emqx_authentication.hrl").
-
-%% Application callbacks
--export([ start/2
-        , stop/1
-        ]).
-
-start(_StartType, _StartArgs) ->
-    {ok, Sup} = emqx_authentication_sup:start_link(),
-    ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
-    ok = emqx_authentication:register_service_types(),
-    {ok, Sup}.
-
-stop(_State) ->
-    ok.
+-module(emqx_enhanced_authn_mnesia).

+ 11 - 17
apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl

@@ -14,7 +14,7 @@
 %% limitations under the License.
 %%--------------------------------------------------------------------
 
--module(emqx_authentication_jwks_connector).
+-module(emqx_authn_jwks_connector).
 
 -behaviour(gen_server).
 
@@ -125,23 +125,17 @@ code_change(_OldVsn, State, _Extra) ->
 %% Internal functions
 %%--------------------------------------------------------------------
 
-handle_options(Opts) ->
-    #{endpoint => proplists:get_value(jwks_endpoint, Opts),
-      refresh_interval => limit_refresh_interval(proplists:get_value(refresh_interval, Opts)),
-      ssl_opts => get_ssl_opts(Opts),
+handle_options(#{endpoint := Endpoint,
+                 refresh_interval := RefreshInterval0,
+                 ssl_opts := SSLOpts}) ->
+    #{endpoint => Endpoint,
+      refresh_interval => limit_refresh_interval(RefreshInterval0),
+      ssl_opts => maps:to_list(SSLOpts),
       jwks => [],
-      request_id => undefined}.
-
-get_ssl_opts(Opts) ->
-    case proplists:get_value(enable_ssl, Opts) of
-        false -> [];
-        true ->
-            maps:to_list(maps:with([cacertfile,
-                                    keyfile,
-                                    certfile,
-                                    verify,
-                                    server_name_indication], maps:from_list(Opts)))
-    end.
+      request_id => undefined};
+
+handle_options(#{enable_ssl := false} = Opts) ->
+    handle_options(Opts#{ssl_opts => #{}}).
 
 refresh_jwks(#{endpoint := Endpoint,
                ssl_opts := SSLOpts} = State) ->

+ 343 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -0,0 +1,343 @@
+%%--------------------------------------------------------------------
+%% 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_authn_jwt).
+
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([ structs/0
+        , fields/1
+        , validations/0
+        ]).
+
+-export([ create/3
+        , update/4
+        , authenticate/2
+        , destroy/1
+        ]).
+
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+structs() -> [config].
+
+fields("") ->
+    [{config, {union, [ hoconsc:t('hmac-based')
+                      , hoconsc:t('public-key')
+                      , hoconsc:t('jwks')
+                      , hoconsc:t('jwks-using-ssl')
+                      ]}}];
+
+fields(config) ->
+    [{union, [ hoconsc:t('hmac-based')
+             , hoconsc:t('public-key')
+             , hoconsc:t('jwks')
+             , hoconsc:t('jwks-using-ssl')
+             ]}];
+
+fields('hmac-based') ->
+    [ {use_jwks,              {enum, [false]}}
+    , {algorithm,             {enum, ['hmac-based']}}
+    , {secret,                fun secret/1}
+    , {secret_base64_encoded, fun secret_base64_encoded/1}
+    , {verify_claims,         fun verify_claims/1}
+    ];
+
+fields('public-key') ->
+    [ {use_jwks,              {enum, [false]}}
+    , {algorithm,             {enum, ['public-key']}}
+    , {certificate,           fun certificate/1}
+    , {verify_claims,         fun verify_claims/1}
+    ];
+
+fields('jwks') ->
+    [ {enable_ssl,            {enum, [false]}}
+    ] ++ jwks_fields();
+
+fields('jwks-using-ssl') ->
+    [ {enable_ssl,            {enum, [true]}}
+    , {ssl_opts,              fun ssl_opts/1}
+    ] ++ jwks_fields();
+
+fields(ssl_opts) ->
+    [ {cacertfile,             fun cacertfile/1}
+    , {certfile,               fun certfile/1}
+    , {keyfile,                fun keyfile/1}
+    , {verify,                 fun verify/1}
+    , {server_name_indication, fun server_name_indication/1}
+    ];
+
+fields(claim) ->
+    [ {"$name", fun expected_claim_value/1} ].
+
+validations() ->
+    [ {check_verify_claims, fun check_verify_claims/1} ].
+
+jwks_fields() ->
+    [ {use_jwks,              {enum, [true]}}
+    , {endpoint,              fun endpoint/1}
+    , {refresh_interval,      fun refresh_interval/1}
+    , {verify_claims,         fun verify_claims/1}
+    ].
+
+secret(type) -> string();
+secret(_) -> undefined.
+
+secret_base64_encoded(type) -> boolean();
+secret_base64_encoded(defualt) -> false;
+secret_base64_encoded(_) -> undefined.
+
+certificate(type) -> string();
+certificate(_) -> undefined.
+
+endpoint(type) -> string();
+endpoint(_) -> undefined.
+
+ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts));
+ssl_opts(default) -> [];
+ssl_opts(_) -> undefined.
+
+refresh_interval(type) -> integer();
+refresh_interval(default) -> 300;
+refresh_interval(validator) -> [fun(I) -> I > 0 end];
+refresh_interval(_) -> undefined.
+
+cacertfile(type) -> string();
+cacertfile(_) -> undefined.
+
+certfile(type) -> string();
+certfile(_) -> undefined.
+
+keyfile(type) -> string();
+keyfile(_) -> undefined.
+
+verify(type) -> boolean();
+verify(default) -> false;
+verify(_) -> undefined.
+
+server_name_indication(type) -> string();
+server_name_indication(_) -> undefined.
+
+verify_claims(type) -> hoconsc:array(hoconsc:ref(claim));
+verify_claims(default) -> [];
+verify_claims(_) -> undefined.
+
+expected_claim_value(type) -> string();
+expected_claim_value(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+create(_ChainID, _AuthenticatorName, Config) ->
+    create(Config).
+
+update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, #{jwk := Connector})
+  when is_pid(Connector) ->
+    _ = emqx_authn_jwks_connector:stop(Connector),
+    create(Config);
+
+update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) ->
+    create(Config);
+
+update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Connector} = State)
+  when is_pid(Connector) ->
+    ok = emqx_authn_jwks_connector:update(Connector, Config),
+    case maps:get(verify_cliams, Config, undefined) of
+        undefined ->
+            {ok, State};
+        VerifyClaims ->
+            {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}}
+    end;
+
+update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) ->
+    create(Config).
+
+authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK,
+                                                verify_claims := VerifyClaims0}) ->
+    JWKs = case erlang:is_pid(JWK) of
+               false ->
+                   [JWK];
+               true ->
+                   {ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK),
+                   JWKs0
+           end,
+    VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo),
+    case verify(JWT, JWKs, VerifyClaims) of
+        ok -> ok;
+        {error, invalid_signature} -> ignore;
+        {error, {claims, _}} -> {stop, bad_password}
+    end.
+
+destroy(#{jwk := Connector}) when is_pid(Connector) ->
+    _ = emqx_authn_jwks_connector:stop(Connector),
+    ok;
+destroy(_) ->
+    ok.
+
+%%--------------------------------------------------------------------
+%% Internal functions
+%%--------------------------------------------------------------------
+
+create(#{verify_claims := VerifyClaims} = Config) ->
+    create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
+
+create2(#{use_jwks := false,
+          algorithm := 'hmac-based',
+          secret := Secret0,
+          secret_base64_encoded := Base64Encoded,
+          verify_claims := VerifyClaims}) ->
+    Secret = case Base64Encoded of
+                 true ->
+                     base64:decode(Secret0);
+                 false ->
+                     Secret0
+             end,
+    JWK = jose_jwk:from_oct(Secret),
+    {ok, #{jwk => JWK,
+           verify_claims => VerifyClaims}};
+
+create2(#{use_jwks := false,
+          algorithm := 'public-key',
+          certificate := Certificate,
+          verify_claims := VerifyClaims}) ->
+    JWK = jose_jwk:from_pem_file(Certificate),
+    {ok, #{jwk => JWK,
+           verify_claims => VerifyClaims}};
+
+create2(#{use_jwks := true,
+          verify_claims := VerifyClaims} = Config) ->
+    case emqx_authn_jwks_connector:start_link(Config) of
+        {ok, Connector} ->
+            {ok, #{jwk => Connector,
+                   verify_claims => VerifyClaims}};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+replace_placeholder(L, Variables) ->
+    replace_placeholder(L, Variables, []).
+
+replace_placeholder([], _Variables, Acc) ->
+    Acc;
+replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) ->
+    Value = maps:get(PL, Variables),
+    replace_placeholder(More, Variables, [{Name, Value} | Acc]);
+replace_placeholder([{Name, Value} | More], Variables, Acc) ->
+    replace_placeholder(More, Variables, [{Name, Value} | Acc]).
+
+verify(_JWS, [], _VerifyClaims) ->
+    {error, invalid_signature};
+verify(JWS, [JWK | More], VerifyClaims) ->
+    try jose_jws:verify(JWK, JWS) of
+        {true, Payload, _JWS} ->
+            Claims = emqx_json:decode(Payload, [return_maps]),
+            verify_claims(Claims, VerifyClaims);
+        {false, _, _} ->
+            verify(JWS, More, VerifyClaims)
+    catch
+        _:_Reason:_Stacktrace ->
+            %% TODO: Add log
+            {error, invalid_signature}
+    end.
+
+verify_claims(Claims, VerifyClaims0) ->
+    Now = os:system_time(seconds),
+    VerifyClaims = [{<<"exp">>, fun(ExpireTime) ->
+                                    Now < ExpireTime
+                                end},
+                    {<<"iat">>, fun(IssueAt) ->
+                                    IssueAt =< Now
+                                end},
+                    {<<"nbf">>, fun(NotBefore) ->
+                                    NotBefore =< Now
+                                end}] ++ VerifyClaims0,
+    do_verify_claims(Claims, VerifyClaims).
+
+do_verify_claims(_Claims, []) ->
+    ok;
+do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) ->
+    case maps:take(Name, Claims) of
+        error ->
+            do_verify_claims(Claims, More);
+        {Value, NClaims} ->
+            case Fun(Value) of
+                true ->
+                    do_verify_claims(NClaims, More);
+                _ ->
+                    {error, {claims, {Name, Value}}}
+            end
+    end;
+do_verify_claims(Claims, [{Name, Value} | More]) ->
+    case maps:take(Name, Claims) of
+        error ->
+            {error, {missing_claim, Name}};
+        {Value, NClaims} ->
+            do_verify_claims(NClaims, More);
+        {Value0, _} ->
+            {error, {claims, {Name, Value0}}}
+    end.
+
+check_verify_claims([]) ->
+    false;
+check_verify_claims([{Name, Expected} | More]) ->
+    check_claim_name(Name) andalso
+    check_claim_expected(Expected) andalso
+    check_verify_claims(More).
+
+check_claim_name(exp) ->
+    false;
+check_claim_name(iat) ->
+    false;
+check_claim_name(nbf) ->
+    false;
+check_claim_name(_) ->
+    true.
+
+check_claim_expected(Expected) ->
+    try handle_placeholder(Expected) of
+        _ -> true
+    catch
+        _:_ ->
+            false
+    end.
+
+handle_verify_claims(VerifyClaims) ->
+    handle_verify_claims(VerifyClaims, []).
+
+handle_verify_claims([], Acc) ->
+    Acc;
+handle_verify_claims([{Name, Expected0} | More], Acc) ->
+    Expected = handle_placeholder(Expected0),
+    handle_verify_claims(More, [{Name, Expected} | Acc]).
+
+handle_placeholder(Placeholder0) ->
+    case re:run(Placeholder0, "^\\$\\{[a-z0-9\\-]+\\}$", [{capture, all}]) of
+        {match, [{Offset, Length}]} ->
+            Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3),
+            Placeholder2 = validate_placeholder(Placeholder1),
+            {placeholder, Placeholder2};
+        nomatch ->
+            Placeholder0
+    end.
+
+validate_placeholder(<<"mqtt-clientid">>) ->
+    clientid;
+validate_placeholder(<<"mqtt-username">>) ->
+    username.

+ 69 - 38
apps/emqx_authentication/src/emqx_authentication_mnesia.erl

@@ -14,9 +14,14 @@
 %% limitations under the License.
 %%--------------------------------------------------------------------
 
--module(emqx_authentication_mnesia).
+-module(emqx_authn_mnesia).
 
--include("emqx_authentication.hrl").
+-include("emqx_authn.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([ structs/0, fields/1 ]).
 
 -export([ create/3
         , update/4
@@ -32,29 +37,10 @@
         , list_users/1
         ]).
 
-%% TODO: support bcrypt
--service_type(#{
-    name => mnesia,
-    params_spec => #{
-        user_id_type => #{
-            order => 1,
-            type => string,
-            enum => [<<"username">>, <<"clientid">>, <<"ip">>, <<"common name">>, <<"issuer">>],
-            default => <<"username">>
-        },
-        password_hash_algorithm => #{
-            order => 2,
-            type => string,
-            enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>],
-            default => <<"sha256">>
-        },
-        salt_rounds => #{
-            order => 3,
-            type => number,
-            default => 10
-        }
-    }
-}).
+-type user_id_type() :: clientid | username.
+
+-type user_group() :: {chain_id(), authenticator_name()}.
+-type user_id() :: binary().
 
 -record(user_info,
         { user_id :: {user_group(), user_id()}
@@ -62,8 +48,7 @@
         , salt :: binary()
         }).
 
--type(user_group() :: {chain_id(), service_name()}).
--type(user_id() :: binary()).
+-reflect_type([ user_id_type/0 ]).
 
 -export([mnesia/1]).
 
@@ -73,7 +58,6 @@
 -define(TAB, mnesia_basic_auth).
 
 -rlog_shard({?AUTH_SHARD, ?TAB}).
-
 %%------------------------------------------------------------------------------
 %% Mnesia bootstrap
 %%------------------------------------------------------------------------------
@@ -90,18 +74,61 @@ mnesia(boot) ->
 mnesia(copy) ->
     ok = ekka_mnesia:copy_table(?TAB, disc_copies).
 
-create(ChainID, ServiceName, #{<<"user_id_type">> := Type,
-                               <<"password_hash_algorithm">> := Algorithm,
-                               <<"salt_rounds">> := SaltRounds}) ->
-    Algorithm =:= <<"bcrypt">> andalso ({ok, _} = application:ensure_all_started(bcrypt)),
-    State = #{user_group => {ChainID, ServiceName},
-              user_id_type => binary_to_atom(Type, utf8),
-              password_hash_algorithm => binary_to_atom(Algorithm, utf8),
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+structs() -> [config].
+
+fields(config) ->
+    [ {user_id_type, fun user_id_type/1}
+    , {password_hash_algorithm, fun password_hash_algorithm/1}
+    ];
+
+fields(bcrypt) ->
+    [ {name, {enum, [bcrypt]}}
+    , {salt_rounds, fun salt_rounds/1}
+    ];
+
+fields(other_algorithms) ->
+    [ {name, {enum, [plain, md5, sha, sha256, sha512]}}
+    ].
+
+user_id_type(type) -> user_id_type();
+user_id_type(default) -> clientid;
+user_id_type(_) -> undefined.
+
+password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]};
+password_hash_algorithm(default) -> sha256;
+password_hash_algorithm(_) -> undefined.
+
+salt_rounds(type) -> integer();
+salt_rounds(default) -> 10;
+salt_rounds(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+create(ChainID, AuthenticatorName, #{user_id_type := Type,
+                                     password_hash_algorithm := #{name := bcrypt,
+                                                                  salt_rounds := SaltRounds}}) ->
+    {ok, _} = application:ensure_all_started(bcrypt),
+    State = #{user_group => {ChainID, AuthenticatorName},
+              user_id_type => Type,
+              password_hash_algorithm => bcrypt,
               salt_rounds => SaltRounds},
+    {ok, State};
+
+create(ChainID, AuthenticatorName, #{user_id_type := Type,
+                                     password_hash_algorithm := #{name := Name}}) ->
+    State = #{user_group => {ChainID, AuthenticatorName},
+              user_id_type => Type,
+              password_hash_algorithm => Name},
     {ok, State}.
 
-update(ChainID, ServiceName, Params, _State) ->
-    create(ChainID, ServiceName, Params).
+update(ChainID, AuthenticatorName, Config, _State) ->
+    create(ChainID, AuthenticatorName, Config).
 
 authenticate(ClientInfo = #{password := Password},
              #{user_group := UserGroup,
@@ -111,7 +138,11 @@ authenticate(ClientInfo = #{password := Password},
     case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
         [] ->
             ignore;
-        [#user_info{password_hash = PasswordHash, salt = Salt}] ->
+        [#user_info{password_hash = PasswordHash, salt = Salt0}] ->
+            Salt = case Algorithm of
+                       bcrypt -> PasswordHash;
+                       _ -> Salt0
+                   end,
             case PasswordHash =:= hash(Algorithm, Password, Salt) of
                 true -> ok;
                 false -> {stop, bad_password}

+ 160 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl

@@ -0,0 +1,160 @@
+%%--------------------------------------------------------------------
+%% 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_authn_mysql).
+
+-include("emqx_authn.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([ structs/0, fields/1 ]).
+
+-export([ create/3
+        , update/4
+        , authenticate/2
+        , destroy/1
+        ]).
+
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+%% Config:
+%% host
+%% port
+%% username
+%% password
+%% database
+%% pool_size
+%% connect_timeout
+%% enable_ssl
+%% ssl_opts
+%%   cacertfile
+%%   certfile
+%%   keyfile
+%%   verify
+%%   servce_name_indication
+%%   tls_versions
+%%   ciphers
+%% password_hash_algorithm
+%% salt_position
+%% query
+%% query_timeout
+structs() -> [config].
+
+fields(config) ->
+    [ {password_hash_algorithm, fun password_hash_algorithm/1}
+    , {salt_position,           {enum, [prefix, suffix]}}
+    , {query,                   fun query/1}
+    , {query_timeout,           fun query_timeout/1}
+    ].
+
+password_hash_algorithm(type) -> string();
+password_hash_algorithm(_) -> undefined.
+
+query(type) -> string();
+query(nullable) -> false;
+query(_) -> undefined.
+
+query_timeout(type) -> integer();
+query_timeout(defualt) -> 5000;
+query_timeout(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+create(ChainID, ServiceName, #{query := Query0,
+                               password_hash_algorithm := Algorithm} = Config) ->
+    {Query, PlaceHolders} = parse_query(Query0),
+    ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])),
+    State = #{query => Query,
+              placeholders => PlaceHolders,
+              password_hash_algorithm => Algorithm},
+    case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of
+        {ok, _} ->
+            {ok, State#{resource_id => ResourceID}};
+        {error, already_created} ->
+            {ok, State#{resource_id => ResourceID}};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) ->
+    case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of
+        {ok, _} -> {ok, State};
+        {error, Reason} -> {error, Reason}
+    end.
+
+authenticate(#{password := Password} = ClientInfo,
+             #{resource_id := ResourceID,
+               placeholders := PlaceHolders,
+               query := Query,
+               query_timeout := Timeout} = State) ->
+    Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo),
+    case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of
+        {ok, _Columns, []} -> ignore;
+        {ok, Columns, Rows} ->
+            %% TODO: Support superuser
+            Selected = maps:from_list(lists:zip(Columns, Rows)),
+            check_password(Password, Selected, State);
+        {error, _Reason} ->
+            ignore
+    end.
+
+destroy(#{resource_id := ResourceID}) ->
+    _ = emqx_resource:remove_local(ResourceID),
+    ok.
+    
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+check_password(undefined, _Algorithm, _Selected) ->
+    {stop, bad_password};
+check_password(Password,
+               #{password_hash := Hash},
+               #{password_hash_algorithm := bcrypt}) ->
+    {ok, Hash0} = bcrypt:hashpw(Password, Hash),
+    case list_to_binary(Hash0) =:= Hash of
+        true -> ok;
+        false -> {stop, bad_password}
+    end;
+check_password(Password,
+               #{password_hash := Hash} = Selected,
+               #{password_hash_algorithm := Algorithm,
+                 salt_position := SaltPosition}) ->
+    Salt = maps:get(salt, Selected, <<>>),
+    Hash0 = case SaltPosition of
+                prefix -> emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
+                suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
+            end,
+    case Hash0 =:= Hash of
+        true -> ok;
+        false -> {stop, bad_password}
+    end.
+
+%% TODO: Support prepare
+parse_query(Query) ->
+    case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of
+        {match, Captured} ->
+            PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
+            NQuery = re:replace(Query, "'\\$\\{[a-z0-9\\_]+\\}'", "?", [global, {return, binary}]),
+            {NQuery, PlaceHolders};
+        nomatch ->
+            {Query, []}
+    end.

+ 155 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl

@@ -0,0 +1,155 @@
+%%--------------------------------------------------------------------
+%% 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_authn_pgsql).
+
+-include("emqx_authn.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([ structs/0, fields/1 ]).
+
+-export([ create/3
+        , update/4
+        , authenticate/2
+        , destroy/1
+        ]).
+
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+%% Config:
+%% host
+%% port
+%% username
+%% password
+%% database
+%% pool_size
+%% connect_timeout
+%% enable_ssl
+%% cacertfile
+%% certfile
+%% keyfile
+%% verify
+%% servce_name_indication
+%% tls_versions
+%% ciphers
+%% password_hash_algorithm
+%% salt_position
+%% query
+structs() -> [config].
+
+fields(config) ->
+    [ {password_hash_algorithm, fun password_hash_algorithm/1}
+    , {salt_position, {enum, [prefix, suffix]}}
+    , {query, fun query/1}
+    ].
+
+password_hash_algorithm(type) -> string();
+password_hash_algorithm(_) -> undefined.
+
+query(type) -> string();
+query(nullable) -> false;
+query(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+create(ChainID, ServiceName, #{query := Query0,
+                               password_hash_algorithm := Algorithm} = Config) ->
+    {Query, PlaceHolders} = parse_query(Query0),
+    ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])),
+    State = #{query => Query,
+              placeholders => PlaceHolders,
+              password_hash_algorithm => Algorithm},
+    case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of
+        {ok, _} ->
+            {ok, State#{resource_id => ResourceID}};
+        {error, already_created} ->
+            {ok, State#{resource_id => ResourceID}};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) ->
+    case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of
+        {ok, _} -> {ok, State};
+        {error, Reason} -> {error, Reason}
+    end.
+
+authenticate(#{password := Password} = ClientInfo,
+             #{resource_id := ResourceID,
+               query := Query,
+               placeholders := PlaceHolders} = State) ->
+    Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo),
+    case emqx_resource:query(ResourceID, {sql, Query, Params}) of
+        {ok, _Columns, []} -> ignore;
+        {ok, Columns, Rows} ->
+            %% TODO: Support superuser
+            Selected = maps:from_list(lists:zip(Columns, Rows)),
+            check_password(Password, Selected, State);
+        {error, _Reason} ->
+            ignore
+    end.
+
+destroy(#{resource_id := ResourceID}) ->
+    _ = emqx_resource:remove_local(ResourceID),
+    ok.
+    
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+check_password(undefined, _Algorithm, _Selected) ->
+    {stop, bad_password};
+check_password(Password,
+               #{password_hash := Hash},
+               #{password_hash_algorithm := bcrypt}) ->
+    {ok, Hash0} = bcrypt:hashpw(Password, Hash),
+    case list_to_binary(Hash0) =:= Hash of
+        true -> ok;
+        false -> {stop, bad_password}
+    end;
+check_password(Password,
+               #{password_hash := Hash} = Selected,
+               #{password_hash_algorithm := Algorithm,
+                 salt_position := SaltPosition}) ->
+    Salt = maps:get(salt, Selected, <<>>),
+    Hash0 = case SaltPosition of
+                prefix -> emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
+                suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
+            end,
+    case Hash0 =:= Hash of
+        true -> ok;
+        false -> {stop, bad_password}
+    end.
+
+%% TODO: Support prepare
+parse_query(Query) ->
+    case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of
+        {match, Captured} ->
+            PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
+            Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))],
+            NQuery = lists:foldl(fun({PlaceHolder, Replacement}, Query0) ->
+                                     re:replace(Query0, <<"'\\", PlaceHolder/binary, "'">>, Replacement, [{return, binary}])
+                                 end, Query, lists:zip(PlaceHolders, Replacements)),
+            {NQuery, PlaceHolders};
+        nomatch ->
+            {Query, []}
+    end.

+ 15 - 0
apps/emqx_authn/test/data/private_key.pem

@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZNF0IeaTmJcBooxsY6Ze8PGFS
+QJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVDeYsu15Htxpqig1L/+MphbZlF
+ClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8GNp+/j2yir1Z9E2xAgMBAAEC
+gYBX7HsLncMWexux6nddbl0nWwyhyPZcvgvT4TjHTPAfhNdOtfQyZCUdbv5+mqip
+j6O8BE7ar2TMz5FgvVrF+O97LkYHNmZk0q3xtZlCYXp4BQqD6Wq65H5U4fAomalK
+xm7HsTCSVXx5CvnZK/JbkPw18QsgwrSHEFs+4Pf2noH+FQJBAL/bpPrkDOB476Iy
+RGnuCckUN1pdCU+UINC8oOWGNwsG6EE5ywlIWRXHtp4VMksG6mCLNJwGUAv2zWIs
+iEjZVfsCQQCVxOciTajTtYO5bPjkXoZoe4VKKXWMYv9AXXVCjq0ff/LjrnKJjbRm
+aoKQGhzjKHk5rgd9+Ydl6FnJw5K4B9dDAkEAtaHfQpZ7ildzpf4ovpBYO0EkViwW
+EHyPxI2PVTwHCC1126o3CYawr+nufSJcBqN5aAThvYRMa8cvEW5PZ4g52QJALF5L
+tt7Yz/crEciVp1nVaaiGISVNHIzLX28QaOpJoVZPR2ILrnJbaifNjBEgU69O0maa
+85fzo54E03/rvDcebwJAfTMgIyzFQK/ESnM43bUCI/Y5XAeKFBiN1YhCioNR4Hj7
+Lkw2RdrrPC9LV+gVJK0b7VUqR5odjdj7PN6SipuXNw==
+-----END RSA PRIVATE KEY-----

+ 6 - 0
apps/emqx_authn/test/data/public_key.pem

@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZ
+NF0IeaTmJcBooxsY6Ze8PGFSQJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVD
+eYsu15Htxpqig1L/+MphbZlFClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8
+GNp+/j2yir1Z9E2xAgMBAAE=
+-----END PUBLIC KEY-----

apps/emqx_authentication/test/data/user-credentials.csv → apps/emqx_authn/test/data/user-credentials.csv


apps/emqx_authentication/test/data/user-credentials.json → apps/emqx_authn/test/data/user-credentials.json


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

@@ -0,0 +1,142 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-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_authn_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.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], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')),
+    emqx_ct_helpers:stop_apps([emqx_authn]),
+    ok.
+
+set_special_configs(emqx_authn) ->
+    application:set_env(emqx, plugins_etc_dir,
+                        emqx_ct_helpers:deps_path(emqx_authn, "test")),
+    Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}},
+    ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)),
+    ok;
+set_special_configs(_App) ->
+    ok.
+
+t_chain(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?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_binding(_) ->
+    Listener1 = <<"listener1">>,
+    Listener2 = <<"listener2">>,
+    ChainID = <<"mychain">>,
+
+    ?assertEqual({error, {not_found, {chain, ChainID}}}, ?AUTH:bind(ChainID, [Listener1])),
+
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1])),
+    ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener2])),
+    ?assertEqual({error, {already_bound, [Listener1]}}, ?AUTH:bind(ChainID, [Listener1])),
+    {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID),
+    ?assertEqual(2, length(Listeners)),
+    ?assertMatch({ok, #{simple := ChainID}}, ?AUTH:list_bound_chains(Listener1)),
+
+    ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1])),
+    ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener2])),
+    ?assertEqual({error, {not_found, [Listener1]}}, ?AUTH:unbind(ChainID, [Listener1])),
+
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ok.
+
+t_binding2(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    Listener1 = <<"listener1">>,
+    Listener2 = <<"listener2">>,
+
+    ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1, Listener2])),
+    {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID),
+    ?assertEqual(2, length(Listeners)),
+    ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1, Listener2])),
+    ?assertMatch({ok, #{listeners := []}}, ?AUTH:list_bindings(ChainID)),
+
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ok.
+
+t_authenticator(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)),
+
+    AuthenticatorName1 = <<"myauthenticator1">>,
+    AuthenticatorConfig1 = #{name => AuthenticatorName1,
+                             type => 'built-in-database',
+                             config => #{
+                                 user_id_type => username,
+                                 password_hash_algorithm => #{
+                                     name => sha256
+                                 }}},
+    ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)),
+    ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName1)),
+    ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)),
+    ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)),
+
+    AuthenticatorName2 = <<"myauthenticator2">>,
+    AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2},
+    ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)),
+    ?assertMatch({ok, #{id := ChainID, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(ChainID)),
+    ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName2)),
+    ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)),
+
+    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)),
+    ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)),
+    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(ChainID, AuthenticatorName2)),
+    ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)),
+    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 1)),
+    ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)),
+    ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 3)),
+    ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 0)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName1)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName2)),
+    ?assertEqual({ok, []}, ?AUTH:list_authenticators(ChainID)),
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ok.

+ 182 - 0
apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl

@@ -0,0 +1,182 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-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_authn_jwt_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-define(AUTH, emqx_authn).
+
+all() ->
+    emqx_ct:all(?MODULE).
+
+init_per_suite(Config) ->
+    emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')),
+    emqx_ct_helpers:stop_apps([emqx_authn]),
+    ok.
+
+set_special_configs(emqx_authn) ->
+    application:set_env(emqx, plugins_etc_dir,
+                        emqx_ct_helpers:deps_path(emqx_authn, "test")),
+    Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}},
+    ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)),
+    ok;
+set_special_configs(_App) ->
+    ok.
+
+t_jwt_authenticator(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    AuthenticatorName = <<"myauthenticator">>,
+    Config = #{use_jwks => false,
+               algorithm => 'hmac-based',
+               secret => <<"abcdef">>,
+               secret_base64_encoded => false,
+               verify_claims => []},
+    AuthenticatorConfig = #{name => AuthenticatorName,
+                            type => jwt,
+                            config => Config},
+    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
+
+    ListenerID = <<"listener1">>,
+    ?AUTH:bind(ChainID, [ListenerID]),
+
+    Payload = #{<<"username">> => <<"myuser">>},
+    JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
+    ClientInfo = #{listener_id => ListenerID,
+			       username => <<"myuser">>,
+			       password => JWS},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
+
+    BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
+    ClientInfo2 = ClientInfo#{password => BadJWS},
+    ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)),
+
+    %% secret_base64_encoded
+    Config2 = Config#{secret => base64:encode(<<"abcdef">>),
+                      secret_base64_encoded => true},
+    ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config2)),
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
+
+    Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
+    ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config3)),
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
+    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>})),
+
+    %% Expiration
+    Payload3 = #{ <<"username">> => <<"myuser">>
+                , <<"exp">> => erlang:system_time(second) - 60},
+    JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
+    ClientInfo3 = ClientInfo#{password => JWS3},
+    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)),
+
+    Payload4 = #{ <<"username">> => <<"myuser">>
+                , <<"exp">> => erlang:system_time(second) + 60},
+    JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
+    ClientInfo4 = ClientInfo#{password => JWS4},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)),
+
+    %% Issued At
+    Payload5 = #{ <<"username">> => <<"myuser">>
+                , <<"iat">> => erlang:system_time(second) - 60},
+    JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
+    ClientInfo5 = ClientInfo#{password => JWS5},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo5)),
+
+    Payload6 = #{ <<"username">> => <<"myuser">>
+                , <<"iat">> => erlang:system_time(second) + 60},
+    JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
+    ClientInfo6 = ClientInfo#{password => JWS6},
+    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo6)),
+
+    %% Not Before
+    Payload7 = #{ <<"username">> => <<"myuser">>
+                , <<"nbf">> => erlang:system_time(second) - 60},
+    JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
+    ClientInfo7 = ClientInfo#{password => JWS7},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo7)),
+
+    Payload8 = #{ <<"username">> => <<"myuser">>
+                , <<"nbf">> => erlang:system_time(second) + 60},
+    JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
+    ClientInfo8 = ClientInfo#{password => JWS8},
+    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo8)),
+
+    ?AUTH:unbind([ListenerID], ChainID),
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ok.
+
+t_jwt_authenticator2(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    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 = #{use_jwks => false,
+               algorithm => 'public-key',
+               certificate => PublicKey,
+               verify_claims => []},
+    AuthenticatorConfig = #{name => AuthenticatorName,
+                            type => jwt,
+                            config => Config},
+    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
+
+    ListenerID = <<"listener1">>,
+    ?AUTH:bind(ChainID, [ListenerID]),
+
+    Payload = #{<<"username">> => <<"myuser">>},
+    JWS = generate_jws('public-key', Payload, PrivateKey),
+    ClientInfo = #{listener_id => ListenerID,
+			       username => <<"myuser">>,
+			       password => JWS},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
+    ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})),
+
+    ?AUTH:unbind([ListenerID], ChainID),
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    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.

+ 187 - 0
apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl

@@ -0,0 +1,187 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-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_authn_mnesia_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-define(AUTH, emqx_authn).
+
+all() ->
+    emqx_ct:all(?MODULE).
+
+init_per_suite(Config) ->
+    emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')),
+    emqx_ct_helpers:stop_apps([emqx_authn]),
+    ok.
+
+set_special_configs(emqx_authn) ->
+    application:set_env(emqx, plugins_etc_dir,
+                        emqx_ct_helpers:deps_path(emqx_authn, "test")),
+    Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}},
+    ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)),
+    ok;
+set_special_configs(_App) ->
+    ok.
+
+t_mnesia_authenticator(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    AuthenticatorName = <<"myauthenticator">>,
+    AuthenticatorConfig = #{name => AuthenticatorName,
+                            type => 'built-in-database',
+                            config => #{
+                                user_id_type => username,
+                                password_hash_algorithm => #{
+                                    name => sha256
+                                }}},
+    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
+
+    UserInfo = #{<<"user_id">> => <<"myuser">>,
+                 <<"password">> => <<"mypass">>},
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)),
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
+
+    ListenerID = <<"listener1">>,
+    ?AUTH:bind(ChainID, [ListenerID]),
+
+    ClientInfo = #{listener_id => ListenerID,
+			       username => <<"myuser">>,
+			       password => <<"mypass">>},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
+    ClientInfo2 = ClientInfo#{username => <<"baduser">>},
+    ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)),
+    ClientInfo3 = ClientInfo#{password => <<"badpass">>},
+    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)),
+
+    UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>},
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)),
+    ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)),
+
+    ?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)),
+    ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
+
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)),
+    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName)),
+    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
+    ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
+
+    ?AUTH:unbind([ListenerID], ChainID),
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ?assertEqual([], ets:tab2list(mnesia_basic_auth)),
+    ok.
+
+t_import(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    AuthenticatorName = <<"myauthenticator">>,
+    AuthenticatorConfig = #{name => AuthenticatorName,
+                            type => 'built-in-database',
+                            config => #{
+                                user_id_type => username,
+                                password_hash_algorithm => #{
+                                    name => sha256
+                                }}},
+    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
+
+    Dir = code:lib_dir(emqx_authn, test),
+    ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))),
+    ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))),
+    ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser1">>)),
+    ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser3">>)),
+
+    ListenerID = <<"listener1">>,
+    ?AUTH:bind(ChainID, [ListenerID]),
+
+    ClientInfo1 = #{listener_id => ListenerID,
+			        username => <<"myuser1">>,
+			        password => <<"mypassword1">>},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)),
+    ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
+                               password => <<"mypassword3">>},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)),
+
+    ?AUTH:unbind([ListenerID], ChainID),
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ok.
+
+t_multi_mnesia_authenticator(_) ->
+    ChainID = <<"mychain">>,
+    Chain = #{id => ChainID,
+              type => simple},
+    ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
+
+    AuthenticatorName1 = <<"myauthenticator1">>,
+    AuthenticatorConfig1 = #{name => AuthenticatorName1,
+                             type => 'built-in-database',
+                             config => #{
+                                 user_id_type => username,
+                                 password_hash_algorithm => #{
+                                     name => sha256
+                                 }}},
+    AuthenticatorName2 = <<"myauthenticator2">>,
+    AuthenticatorConfig2 = #{name => AuthenticatorName2,
+                             type => 'built-in-database',
+                             config => #{
+                                 user_id_type => clientid,
+                                 password_hash_algorithm => #{
+                                     name => sha256
+                                 }}},
+    ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)),
+    ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)),
+
+    ?assertEqual({ok, #{user_id => <<"myuser">>}},
+                 ?AUTH:add_user(ChainID, AuthenticatorName1,
+                                #{<<"user_id">> => <<"myuser">>,
+                                  <<"password">> => <<"mypass1">>})),
+    ?assertEqual({ok, #{user_id => <<"myclient">>}},
+                 ?AUTH:add_user(ChainID, AuthenticatorName2,
+                                #{<<"user_id">> => <<"myclient">>,
+                                  <<"password">> => <<"mypass2">>})),
+
+    ListenerID = <<"listener1">>,
+    ?AUTH:bind(ChainID, [ListenerID]),
+
+    ClientInfo1 = #{listener_id => ListenerID,
+			        username => <<"myuser">>,
+                    clientid => <<"myclient">>,
+			        password => <<"mypass1">>},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)),
+    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)),
+
+    ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)),
+    ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
+    ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)),
+
+    ?AUTH:unbind([ListenerID], ChainID),
+    ?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
+    ok.

+ 2 - 1
rebar.config.erl

@@ -248,6 +248,7 @@ relx_apps(ReleaseType) ->
     , {mnesia, load}
     , {ekka, load}
     , {emqx_plugin_libs, load}
+    , emqx_authn
     , emqx_authz
     , observer_cli
     , emqx_http_lib
@@ -285,7 +286,6 @@ relx_plugin_apps(ReleaseType) ->
     , emqx_sn
     , emqx_coap
     , emqx_stomp
-    , emqx_authentication
     , emqx_statsd
     , emqx_rule_actions
     ]
@@ -373,6 +373,7 @@ emqx_etc_overlay_common() ->
      {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"},
      {"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"},
      {"{{base_dir}}/lib/emqx_telemetry/etc/emqx_telemetry.conf", "etc/plugins/emqx_telemetry.conf"},
+     {"{{base_dir}}/lib/emqx_authn/etc/emqx_authn.conf", "etc/plugins/emqx_authn.conf"},
      {"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"},
      %% TODO: check why it has to end with .paho
      %% and why it is put to etc/plugins dir