From 6a8e35ce3ae909e0b1eaa49fe387e6b626fee935 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 14 Jul 2021 16:53:52 +0800 Subject: [PATCH] feat(one authn): merge simple authn and enhanced authn --- apps/emqx/src/emqx_access_control.erl | 33 +- apps/emqx/src/emqx_channel.erl | 194 +++++----- apps/emqx/src/emqx_reason_codes.erl | 7 +- apps/emqx_authn/etc/emqx_authn.conf | 32 +- apps/emqx_authn/include/emqx_authn.hrl | 13 +- apps/emqx_authn/src/emqx_authn.erl | 168 +++------ apps/emqx_authn/src/emqx_authn_api.erl | 339 ++++-------------- apps/emqx_authn/src/emqx_authn_app.erl | 45 +-- apps/emqx_authn/src/emqx_authn_schema.erl | 105 ++---- apps/emqx_authn/src/emqx_authn_utils.erl | 5 + .../emqx_enhanced_authn_mnesia.erl | 17 - .../emqx_enhanced_authn_scram_mnesia.erl | 240 +++++++++++++ .../src/simple_authn/emqx_authn_http.erl | 48 +-- .../emqx_authn_jwks_connector.erl | 5 +- .../src/simple_authn/emqx_authn_jwt.erl | 74 ++-- .../src/simple_authn/emqx_authn_mnesia.erl | 28 +- .../src/simple_authn/emqx_authn_mysql.erl | 15 +- .../src/simple_authn/emqx_authn_pgsql.erl | 19 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 100 ++---- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 70 ++-- .../test/emqx_authn_mnesia_SUITE.erl | 122 +++---- .../emqx_exproto/src/emqx_exproto_channel.erl | 17 +- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 12 +- apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl | 9 +- rebar.config | 1 + 25 files changed, 742 insertions(+), 976 deletions(-) delete mode 100644 apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl create mode 100644 apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 8da3277a9..b1c38bc41 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -18,27 +18,22 @@ -include("emqx.hrl"). --export([authenticate/1]). - --export([ authorize/3 +-export([ authenticate/1 + , authorize/3 ]). --type(result() :: #{auth_result := emqx_types:auth_result(), - anonymous := boolean() - }). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- --spec(authenticate(emqx_types:clientinfo()) -> {ok, result()} | {error, term()}). -authenticate(ClientInfo = #{zone := Zone}) -> - AuthResult = default_auth_result(Zone), - case emqx_zone:get_env(Zone, bypass_auth_plugins, false) of +-spec(authenticate(emqx_types:clientinfo()) -> + ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). +authenticate(Credential = #{zone := Zone}) -> + case emqx_zone:get_env(Zone, bypass_authentication, false) of true -> - return_auth_result(AuthResult); + ok; false -> - return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult)) + run_hooks('client.authenticate', [Credential], ok) end. %% @doc Check ACL @@ -65,18 +60,6 @@ do_authorize(ClientInfo, PubSub, Topic) -> _Other -> deny end. -default_auth_result(Zone) -> - case emqx_zone:get_env(Zone, allow_anonymous, false) of - true -> #{auth_result => success, anonymous => true}; - false -> #{auth_result => not_authorized, anonymous => false} - end. - -compile({inline, [run_hooks/3]}). run_hooks(Name, Args, Acc) -> ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc). - --compile({inline, [return_auth_result/1]}). -return_auth_result(Result = #{auth_result := success}) -> - {ok, Result}; -return_auth_result(Result) -> - {error, maps:get(auth_result, Result, unknown_error)}. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 99f8cb5df..911a44283 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -98,7 +98,7 @@ -type(channel() :: #channel{}). --type(conn_state() :: idle | connecting | connected | disconnected). +-type(conn_state() :: idle | connecting | connected | reauthenticating | disconnected). -type(reply() :: {outgoing, emqx_types:packet()} | {outgoing, [emqx_types:packet()]} @@ -272,7 +272,8 @@ take_ws_cookie(ClientInfo, ConnInfo) -> | {ok, replies(), channel()} | {shutdown, Reason :: term(), channel()} | {shutdown, Reason :: term(), replies(), channel()}). -handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = connected}) -> +handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> @@ -281,56 +282,64 @@ handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> fun check_connect/2, fun enrich_client/2, fun set_log_meta/2, - fun check_banned/2, - fun auth_connect/2 + fun check_banned/2 ], ConnPkt, Channel#channel{conn_state = connecting}) of {ok, NConnPkt, NChannel = #channel{clientinfo = ClientInfo}} -> NChannel1 = NChannel#channel{ will_msg = emqx_packet:will_msg(NConnPkt), alias_maximum = init_alias_maximum(NConnPkt, ClientInfo) }, - case enhanced_auth(?CONNECT_PACKET(NConnPkt), NChannel1) of + case authenticate(?CONNECT_PACKET(NConnPkt), NChannel1) of {ok, Properties, NChannel2} -> process_connect(Properties, ensure_connected(NChannel2)); {continue, Properties, NChannel2} -> handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, Properties}, NChannel2); - {error, ReasonCode, NChannel2} -> - handle_out(connack, ReasonCode, NChannel2) + {error, ReasonCode} -> + handle_out(connack, ReasonCode, NChannel1) end; {error, ReasonCode, NChannel} -> handle_out(connack, ReasonCode, NChannel) end; -handle_in(Packet = ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, _Properties), +handle_in(Packet = ?AUTH_PACKET(ReasonCode, _Properties), Channel = #channel{conn_state = ConnState}) -> - case enhanced_auth(Packet, Channel) of - {ok, NProperties, NChannel} -> + try + case {ReasonCode, ConnState} of + {?RC_CONTINUE_AUTHENTICATION, connecting} -> ok; + {?RC_CONTINUE_AUTHENTICATION, reauthenticating} -> ok; + {?RC_RE_AUTHENTICATE, connected} -> ok; + _ -> error(protocol_error) + end, + case authenticate(Packet, Channel) of + {ok, NProperties, NChannel} -> + case ConnState of + connecting -> + process_connect(NProperties, ensure_connected(NChannel)); + _ -> + handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel#channel{conn_state = connected}) + end; + {continue, NProperties, NChannel} -> + handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel#channel{conn_state = reauthenticating}); + {error, NReasonCode} -> + case ConnState of + connecting -> + handle_out(connack, NReasonCode, Channel); + _ -> + handle_out(disconnect, NReasonCode, Channel) + end + end + catch + _Class:_Reason -> case ConnState of connecting -> - process_connect(NProperties, ensure_connected(NChannel)); - connected -> - handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel); + handle_out(connack, ?RC_PROTOCOL_ERROR, Channel); _ -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel) - end; - {continue, NProperties, NChannel} -> - handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel); - {error, NReasonCode, NChannel} -> - handle_out(connack, NReasonCode, NChannel) + end end; -handle_in(Packet = ?AUTH_PACKET(?RC_RE_AUTHENTICATE, _Properties), - Channel = #channel{conn_state = connected}) -> - case enhanced_auth(Packet, Channel) of - {ok, NProperties, NChannel} -> - handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel); - {continue, NProperties, NChannel} -> - handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel); - {error, NReasonCode, NChannel} -> - handle_out(disconnect, NReasonCode, NChannel) - end; - -handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState}) when ConnState =/= connected -> +handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState}) + when ConnState =/= connected andalso ConnState =/= reauthenticating -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); handle_in(Packet = ?PUBLISH_PACKET(_QoS), Channel) -> @@ -469,9 +478,11 @@ handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connec handle_in({frame_error, Reason}, Channel = #channel{conn_state = connecting}) -> shutdown(Reason, ?CONNACK_PACKET(?RC_MALFORMED_PACKET), Channel); -handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connected}) -> +handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, {?RC_PACKET_TOO_LARGE, frame_too_large}, Channel); -handle_in({frame_error, Reason}, Channel = #channel{conn_state = connected}) -> +handle_in({frame_error, Reason}, Channel = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel); handle_in({frame_error, Reason}, Channel = #channel{conn_state = disconnected}) -> @@ -967,8 +978,9 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting}) shutdown(Reason, Channel); handle_info({sock_closed, Reason}, Channel = - #channel{conn_state = connected, - clientinfo = ClientInfo = #{zone := Zone}}) -> + #channel{conn_state = ConnState, + clientinfo = ClientInfo = #{zone := Zone}}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> emqx_zone:enable_flapping_detect(Zone) andalso emqx_flapping:detect(ClientInfo), Channel1 = ensure_disconnected(Reason, mabye_publish_will_msg(Channel)), @@ -1241,75 +1253,60 @@ check_banned(_ConnPkt, #channel{clientinfo = ClientInfo = #{zone := Zone}}) -> end. %%-------------------------------------------------------------------- -%% Auth Connect +%% Authenticate -auth_connect(#mqtt_packet_connect{password = Password}, +authenticate(?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5, + properties = #{'Authentication-Method' := AuthMethod} = Properties}), + #channel{clientinfo = ClientInfo, + auth_cache = AuthCache} = Channel) -> + AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), + do_authenticate(ClientInfo#{auth_method => AuthMethod, + auth_data => AuthData, + auth_cache => AuthCache}, Channel); + +authenticate(?CONNECT_PACKET(#mqtt_packet_connect{password = Password}), #channel{clientinfo = ClientInfo} = Channel) -> - #{clientid := ClientId, - username := Username} = ClientInfo, - case emqx_access_control:authenticate(ClientInfo#{password => Password}) of - {ok, AuthResult} -> - is_anonymous(AuthResult) andalso - emqx_metrics:inc('client.auth.anonymous'), - NClientInfo = maps:merge(ClientInfo, AuthResult), - {ok, Channel#channel{clientinfo = NClientInfo}}; - {error, Reason} -> - ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", - [ClientId, Username, Reason]), - {error, emqx_reason_codes:connack_error(Reason)} + do_authenticate(ClientInfo#{password => Password}, Channel); + +authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties), + #channel{clientinfo = ClientInfo, + conninfo = #{conn_props := ConnProps}, + auth_cache = AuthCache} = Channel) -> + case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of + AuthMethod -> + AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), + do_authenticate(ClientInfo#{auth_method => AuthMethod, + auth_data => AuthData, + auth_cache => AuthCache}, Channel); + _ -> + {error, ?RC_BAD_AUTHENTICATION_METHOD} end. -is_anonymous(#{anonymous := true}) -> true; -is_anonymous(_AuthResult) -> false. - -%%-------------------------------------------------------------------- -%% Enhanced Authentication - -enhanced_auth(?CONNECT_PACKET(#mqtt_packet_connect{ - proto_ver = Protover, - properties = Properties - }), Channel) -> - case Protover of - ?MQTT_PROTO_V5 -> - AuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined), - AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), - do_enhanced_auth(AuthMethod, AuthData, Channel); - _ -> - {ok, #{}, Channel} +do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> + Properties = #{'Authentication-Method' => AuthMethod}, + case emqx_access_control:authenticate(Credential) of + ok -> + {ok, Properties, Channel#channel{auth_cache = #{}}}; + {ok, AuthData} -> + {ok, Properties#{'Authentication-Data' => AuthData}, + Channel#channel{auth_cache = #{}}}; + {continue, AuthCache} -> + {continue, Properties, Channel#channel{auth_cache = AuthCache}}; + {continue, AuthData, AuthCache} -> + {continue, Properties#{'Authentication-Data' => AuthData}, + Channel#channel{auth_cache = AuthCache}}; + {error, Reason} -> + {error, emqx_reason_codes:connack_error(Reason)} end; -enhanced_auth(?AUTH_PACKET(_ReasonCode, Properties), Channel = #channel{conninfo = ConnInfo}) -> - AuthMethod = emqx_mqtt_props:get('Authentication-Method', - emqx_mqtt_props:get(conn_props, ConnInfo, #{}), - undefined - ), - NAuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined), - AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), - case NAuthMethod =:= undefined orelse NAuthMethod =/= AuthMethod of - true -> - {error, emqx_reason_codes:connack_error(bad_authentication_method), Channel}; - false -> - do_enhanced_auth(AuthMethod, AuthData, Channel) - end. - -do_enhanced_auth(undefined, undefined, Channel) -> - {ok, #{}, Channel}; -do_enhanced_auth(undefined, _AuthData, Channel) -> - {error, emqx_reason_codes:connack_error(not_authorized), Channel}; -do_enhanced_auth(_AuthMethod, undefined, Channel) -> - {error, emqx_reason_codes:connack_error(not_authorized), Channel}; -do_enhanced_auth(AuthMethod, AuthData, Channel = #channel{auth_cache = Cache}) -> - case run_hooks('client.enhanced_authenticate', [AuthMethod, AuthData], Cache) of - {ok, NAuthData, NCache} -> - NProperties = #{'Authentication-Method' => AuthMethod, - 'Authentication-Data' => NAuthData}, - {ok, NProperties, Channel#channel{auth_cache = NCache}}; - {continue, NAuthData, NCache} -> - NProperties = #{'Authentication-Method' => AuthMethod, - 'Authentication-Data' => NAuthData}, - {continue, NProperties, Channel#channel{auth_cache = NCache}}; - _ -> - {error, emqx_reason_codes:connack_error(not_authorized), Channel} +do_authenticate(Credential, Channel) -> + case emqx_access_control:authenticate(Credential) of + ok -> + {ok, #{}, Channel}; + {error, Reason} -> + {error, emqx_reason_codes:connack_error(Reason)} end. %%-------------------------------------------------------------------- @@ -1703,7 +1700,8 @@ shutdown(Reason, Reply, Packet, Channel) -> {shutdown, Reason, Reply, Packet, Channel}. disconnect_and_shutdown(Reason, Reply, Channel = ?IS_MQTT_V5 - = #channel{conn_state = connected}) -> + = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> shutdown(Reason, Reply, ?DISCONNECT_PACKET(reason_code(Reason)), Channel); disconnect_and_shutdown(Reason, Reply, Channel) -> diff --git a/apps/emqx/src/emqx_reason_codes.erl b/apps/emqx/src/emqx_reason_codes.erl index 893084b9d..b98fc1263 100644 --- a/apps/emqx/src/emqx_reason_codes.erl +++ b/apps/emqx/src/emqx_reason_codes.erl @@ -170,16 +170,11 @@ frame_error(frame_too_large) -> ?RC_PACKET_TOO_LARGE; frame_error(_) -> ?RC_MALFORMED_PACKET. connack_error(protocol_error) -> ?RC_PROTOCOL_ERROR; -connack_error(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID; connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD; -connack_error(bad_clientid_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD; -connack_error(username_or_password_undefined) -> ?RC_BAD_USER_NAME_OR_PASSWORD; -connack_error(password_error) -> ?RC_BAD_USER_NAME_OR_PASSWORD; connack_error(not_authorized) -> ?RC_NOT_AUTHORIZED; connack_error(server_unavailable) -> ?RC_SERVER_UNAVAILABLE; connack_error(server_busy) -> ?RC_SERVER_BUSY; connack_error(banned) -> ?RC_BANNED; connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD; -%% TODO: ??? -connack_error(_) -> ?RC_NOT_AUTHORIZED. +connack_error(_) -> ?RC_UNSPECIFIED_ERROR. diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index ecd49d5a5..5194445b8 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,26 +1,12 @@ emqx_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"] - # } + authenticators: [ + # { + # name: "authenticator1" + # mechanism: password-based + # config: { + # server_type: built-in-database + # user_id_type: clientid + # } + # } ] } diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index 46c1cf7ca..bb353348f 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -15,16 +15,15 @@ %%-------------------------------------------------------------------- -define(APP, emqx_authn). +-define(CHAIN, <<"mqtt">>). -type chain_id() :: binary(). --type authn_type() :: simple | enhanced. -type authenticator_name() :: binary(). --type authenticator_type() :: mnesia | jwt | mysql | postgresql. --type listener_id() :: binary(). +-type mechanism() :: 'password-based' | jwt | scram. -record(authenticator, { name :: authenticator_name() - , type :: authenticator_type() + , mechanism :: mechanism() , provider :: module() , config :: map() , state :: map() @@ -32,16 +31,10 @@ -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)). diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 24bdb21e7..611b47cc2 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -22,16 +22,12 @@ , disable/0 ]). --export([authenticate/1]). +-export([authenticate/2]). -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 @@ -55,10 +51,8 @@ -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 @@ -75,13 +69,6 @@ mnesia(boot) -> {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() -> @@ -94,39 +81,35 @@ 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 +authenticate(Credential, _AuthResult) -> + case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of + [#chain{authenticators = Authenticators}] -> + do_authenticate(Authenticators, Credential); + [] -> + {stop, {error, not_authorized}} 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} + {stop, {error, not_authorized}}; +do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], Credential) -> + case Provider:authenticate(Credential, State) of + ignore -> + do_authenticate(More, Credential); + Result -> + %% ok + %% {ok, AuthData} + %% {continue, AuthCache} + %% {continue, AuthData, AuthCache} + %% {error, Reason} + {stop, Result} end. -create_chain(#{id := ID, - type := Type}) -> +create_chain(#{id := ID}) -> 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), @@ -160,85 +143,20 @@ 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, + mechanism := Mechanism, config := Config}) -> UpdateFun = - fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) -> + fun(Chain = #chain{authenticators = Authenticators}) -> case lists:keymember(Name, 1, Authenticators) of true -> {error, {already_exists, {authenticator, Name}}}; false -> - Provider = authenticator_provider(AuthNType, Type), + Provider = authenticator_provider(Mechanism, Config), case Provider:create(ChainID, Name, Config) of {ok, State} -> Authenticator = #authenticator{name = Name, - type = Type, + mechanism = Mechanism, provider = Provider, config = Config, state = State}, @@ -367,12 +285,18 @@ list_users(ChainID, AuthenticatorName) -> %% 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. +authenticator_provider('password-based', #{server_type := 'built-in-database'}) -> + emqx_authn_mnesia; +authenticator_provider('password-based', #{server_type := 'mysql'}) -> + emqx_authn_mysql; +authenticator_provider('password-based', #{server_type := 'pgsql'}) -> + emqx_authn_pgsql; +authenticator_provider('password-based', #{server_type := 'http-server'}) -> + emqx_authn_http; +authenticator_provider(jwt, _) -> + emqx_authn_jwt; +authenticator_provider(scram, #{server_type := 'built-in-database'}) -> + emqx_enhanced_authn_scram_mnesia. do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> Provider:destroy(State). @@ -429,13 +353,13 @@ update_chain(ChainID, UpdateFun) -> 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. +% 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) -> @@ -457,11 +381,9 @@ call_authenticator(ChainID, AuthenticatorName, Func, Args) -> end. serialize_chain(#chain{id = ID, - type = Type, authenticators = Authenticators, created_at = CreatedAt}) -> #{id => ID, - type => Type, authenticators => serialize_authenticators(Authenticators), created_at => CreatedAt}. @@ -474,10 +396,10 @@ serialize_authenticators(Authenticators) -> [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. serialize_authenticator(#authenticator{name = Name, - type = Type, + mechanism = Mechanism, config = Config}) -> #{name => Name, - type => Type, + mechanism => Mechanism, config => Config}. trans(Fun) -> diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c24b790a3..c875cc717 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -18,15 +18,7 @@ -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 +-export([ create_authenticator/2 , delete_authenticator/2 , update_authenticator/2 , lookup_authenticator/2 @@ -40,135 +32,79 @@ , list_users/2 ]). --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", + path => "/authentication/authenticators", func => create_authenticator, - descr => "Create authenticator to chain" + descr => "Create authenticator" }). -rest_api(#{name => delete_authenticator, method => 'DELETE', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + path => "/authentication/authenticators/:bin:name", func => delete_authenticator, - descr => "Delete authenticator from chain" + descr => "Delete authenticator" }). -rest_api(#{name => update_authenticator, method => 'PUT', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + path => "/authentication/authenticators/:bin:name", func => update_authenticator, - descr => "Update authenticator in chain" + descr => "Update authenticator" }). -rest_api(#{name => lookup_authenticator, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + path => "/authentication/authenticators/:bin:name", func => lookup_authenticator, - descr => "Lookup authenticator in chain" + descr => "Lookup authenticator" }). -rest_api(#{name => list_authenticators, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators", + path => "/authentication/authenticators", func => list_authenticators, - descr => "List authenticators in chain" + descr => "List authenticators" }). -rest_api(#{name => move_authenticator, method => 'POST', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position", + path => "/authentication/authenticators/:bin: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", + path => "/authentication/authenticators/:bin: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", + path => "/authentication/authenticators/:bin: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", + path => "/authentication/authenticators/:bin: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", + path => "/authentication/authenticators/:bin: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", + path => "/authentication/authenticators/:bin:name/users/:bin:user_id", func => lookup_user, descr => "Lookup user" }). @@ -176,129 +112,24 @@ %% TODO: Support pagination -rest_api(#{name => list_users, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + path => "/authentication/authenticators/:bin: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; +do_create_authenticator(_Binding, Authenticator0) -> + Config = #{<<"emqx_authn">> => #{ + <<"authenticators">> => [Authenticator0] + }}, + #{emqx_authn := #{authenticators := [Authenticator1]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_authenticator(?CHAIN, Authenticator1) of + {ok, Authenticator2} -> + return({ok, Authenticator2}); {error, Reason} -> return(serialize_error(Reason)) end. @@ -306,9 +137,8 @@ do_create_authenticator(#{id := ChainID}, Authenticator0) -> 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 +do_delete_authenticator(#{name := Name}, _Params) -> + case emqx_authn:delete_authenticator(?CHAIN, Name) of ok -> return(ok); {error, Reason} -> @@ -320,36 +150,26 @@ 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; +do_update_authenticator(#{name := Name}, NewConfig0) -> + case emqx_authn:lookup_authenticator(?CHAIN, Name) of + {ok, #{mechanism := Mechanism}} -> + Authenticator = #{<<"name">> => Name, + <<"mechanism">> => Mechanism, + <<"config">> => NewConfig0}, + Config = #{<<"emqx_authn">> => #{ + <<"authenticators">> => [Authenticator] + }}, + #{ + emqx_authn := #{ + authenticators := [#{ + config := NewConfig1 + }] + } + } = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:update_authenticator(?CHAIN, Name, NewConfig1) of + {ok, NAuthenticator} -> + return({ok, NAuthenticator}); {error, Reason} -> return(serialize_error(Reason)) end; @@ -360,9 +180,8 @@ do_update_authenticator(#{id := ChainID, 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 +do_lookup_authenticator(#{name := Name}, _Params) -> + case emqx_authn:lookup_authenticator(?CHAIN, Name) of {ok, Authenticator} -> return({ok, Authenticator}); {error, Reason} -> @@ -372,8 +191,8 @@ do_lookup_authenticator(#{id := ChainID, 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 +do_list_authenticators(_Binding, _Params) -> + case emqx_authn:list_authenticators(?CHAIN) of {ok, Authenticators} -> return({ok, Authenticators}); {error, Reason} -> @@ -383,25 +202,22 @@ do_list_authenticators(#{id := ChainID}, _Params) -> 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 +do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the front">>}) -> + case emqx_authn:move_authenticator_to_the_front(?CHAIN, Name) 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 +do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the end">>}) -> + case emqx_authn:move_authenticator_to_the_end(?CHAIN, Name) 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 +do_move_authenticator(#{name := Name}, #{<<"position">> := N}) when is_number(N) -> + case emqx_authn:move_authenticator_to_the_nth(?CHAIN, Name, N) of ok -> return(ok); {error, Reason} -> @@ -413,9 +229,9 @@ do_move_authenticator(_Binding, _Params) -> import_users(Binding, Params) -> do_import_users(uri_decode(Binding), maps:from_list(Params)). -do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName}, +do_import_users(#{name := Name}, #{<<"filename">> := Filename}) -> - case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of + case emqx_authn:import_users(?CHAIN, Name, Filename) of ok -> return(ok); {error, Reason} -> @@ -428,9 +244,8 @@ do_import_users(_Binding, Params) -> 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 +do_add_user(#{name := Name}, UserInfo) -> + case emqx_authn:add_user(?CHAIN, Name, UserInfo) of {ok, User} -> return({ok, User}); {error, Reason} -> @@ -440,10 +255,9 @@ do_add_user(#{id := ChainID, delete_user(Binding, Params) -> do_delete_user(uri_decode(Binding), maps:from_list(Params)). -do_delete_user(#{id := ChainID, - authenticator_name := AuthenticatorName, +do_delete_user(#{name := Name, user_id := UserID}, _Params) -> - case emqx_authn:delete_user(ChainID, AuthenticatorName, UserID) of + case emqx_authn:delete_user(?CHAIN, Name, UserID) of ok -> return(ok); {error, Reason} -> @@ -453,10 +267,9 @@ do_delete_user(#{id := ChainID, update_user(Binding, Params) -> do_update_user(uri_decode(Binding), maps:from_list(Params)). -do_update_user(#{id := ChainID, - authenticator_name := AuthenticatorName, +do_update_user(#{name := Name, user_id := UserID}, NewUserInfo) -> - case emqx_authn:update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) of + case emqx_authn:update_user(?CHAIN, Name, UserID, NewUserInfo) of {ok, User} -> return({ok, User}); {error, Reason} -> @@ -466,10 +279,9 @@ do_update_user(#{id := ChainID, lookup_user(Binding, Params) -> do_lookup_user(uri_decode(Binding), maps:from_list(Params)). -do_lookup_user(#{id := ChainID, - authenticator_name := AuthenticatorName, +do_lookup_user(#{name := Name, user_id := UserID}, _Params) -> - case emqx_authn:lookup_user(ChainID, AuthenticatorName, UserID) of + case emqx_authn:lookup_user(?CHAIN, Name, UserID) of {ok, User} -> return({ok, User}); {error, Reason} -> @@ -479,9 +291,8 @@ do_lookup_user(#{id := ChainID, 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 +do_list_users(#{name := Name}, _Params) -> + case emqx_authn:list_users(?CHAIN, Name) of {ok, Users} -> return({ok, Users}); {error, Reason} -> @@ -526,11 +337,7 @@ serialize_error(_) -> {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. serialize_type(authenticator) -> - "Authenticator"; -serialize_type(chain) -> - "Chain"; -serialize_type(authenticator_type) -> - "Authenticator type". + "Authenticator". get_missed_params(Actual, Expected) -> Keys = lists:foldl(fun(Key, Acc) -> diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 033c760af..a78fa54f1 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -38,40 +38,19 @@ stop(_State) -> ok. initialize() -> - #{chains := Chains, - bindings := Bindings} = emqx_config:get([authn], #{chains => [], bindings => []}), - initialize_chains(Chains), - initialize_bindings(Bindings). + #{authenticators := Authenticators} = emqx_config:get([emqx_authn], #{authenticators => []}), + initialize(Authenticators). -initialize_chains([]) -> +initialize(Authenticators) -> + {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), + initialize_authenticators(Authenticators). + +initialize_authenticators([]) -> ok; -initialize_chains([#{id := ChainID, - type := Type, - authenticators := Authenticators} | More]) -> - case emqx_authn:create_chain(#{id => ChainID, - type => Type}) of +initialize_authenticators([#{name := Name} = Authenticator | More]) -> + case emqx_authn:create_authenticator(?CHAIN, Authenticator) of {ok, _} -> - initialize_authenticators(ChainID, Authenticators), - initialize_chains(More); + initialize_authenticators(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. + ?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason]) + end. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index d9bf72910..8b844ab69 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -21,94 +21,55 @@ -behaviour(hocon_schema). --export([structs/0, fields/1]). +-export([ structs/0 + , fields/1 + ]). --reflect_type([ chain_id/0 - , authenticator_name/0 +-reflect_type([ authenticator_name/0 ]). structs() -> ["emqx_authn"]. fields("emqx_authn") -> - [ {chains, fun chains/1} - , {bindings, fun bindings/1}]; + [ {authenticators, fun authenticators/1} ]; -fields('simple-chain') -> - [ {id, fun chain_id/1} - , {type, {enum, [simple]}} - , {authenticators, fun simple_authenticators/1} +fields('password-based') -> + [ {name, fun authenticator_name/1} + , {mechanism, {enum, ['password-based']}} + , {config, hoconsc:t(hoconsc:union( + [ hoconsc:ref(emqx_authn_mnesia, config) + , hoconsc:ref(emqx_authn_mysql, config) + , hoconsc:ref(emqx_authn_pgsql, config) + , hoconsc:ref(emqx_authn_http, get) + , hoconsc:ref(emqx_authn_http, post) + ]))} ]; -% 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))} + [ {name, fun authenticator_name/1} + , {mechanism, {enum, [jwt]}} + , {config, hoconsc:t(hoconsc:union( + [ hoconsc:ref(emqx_authn_jwt, 'hmac-based') + , hoconsc:ref(emqx_authn_jwt, 'public-key') + , hoconsc:ref(emqx_authn_jwt, 'jwks') + ]))} ]; -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))} +fields(scram) -> + [ {name, fun authenticator_name/1} + , {mechanism, {enum, [scram]}} + , {config, hoconsc:t(hoconsc:union( + [ hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) + ]))} ]. -chains(type) -> hoconsc:array({union, [hoconsc:ref(?MODULE, '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(?MODULE, 'built-in-database') +authenticators(type) -> + hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based') , hoconsc:ref(?MODULE, jwt) - , hoconsc:ref(?MODULE, mysql) - , hoconsc:ref(?MODULE, 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. + , hoconsc:ref(?MODULE, scram)]}); +authenticators(default) -> []; +authenticators(_) -> undefined. authenticator_name(type) -> authenticator_name(); authenticator_name(nullable) -> false; authenticator_name(_) -> undefined. - -bindings(type) -> hoconsc:array(hoconsc:ref(?MODULE, binding)); -bindings(default) -> []; -bindings(_) -> undefined. - -listeners(type) -> hoconsc:array(binary()); -listeners(default) -> []; -listeners(_) -> undefined. diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl index 98e27e76c..2a91584f0 100644 --- a/apps/emqx_authn/src/emqx_authn_utils.erl +++ b/apps/emqx_authn/src/emqx_authn_utils.erl @@ -17,6 +17,7 @@ -module(emqx_authn_utils). -export([ replace_placeholder/2 + , gen_salt/0 ]). %%------------------------------------------------------------------------------ @@ -41,6 +42,10 @@ replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Da replace_placeholder([_ | More], Data, Acc) -> replace_placeholder(More, Data, [null | Acc]). +gen_salt() -> + <> = crypto:strong_rand_bytes(16), + iolist_to_binary(io_lib:format("~32.16.0b", [X])). + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl deleted file mode 100644 index 207e93495..000000000 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl +++ /dev/null @@ -1,17 +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_enhanced_authn_mnesia). diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl new file mode 100644 index 000000000..d1d564bf3 --- /dev/null +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -0,0 +1,240 @@ +%%-------------------------------------------------------------------- +%% 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_enhanced_authn_scram_mnesia). + +-include("emqx_authn.hrl"). +-include_lib("esasl/include/esasl_scram.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +-export([ add_user/2 + , delete_user/2 + , update_user/3 + , lookup_user/2 + , list_users/1 + ]). + +-define(TAB, ?MODULE). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-rlog_shard({?AUTH_SHARD, ?TAB}). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot | copy) -> ok). +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {disc_copies, [node()]}, + {record_name, scram_user_credentail}, + {attributes, record_info(fields, scram_user_credentail)}, + {storage_properties, [{ets, [{read_concurrency, true}]}]}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB, disc_copies). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {server_type, fun server_type/1} + , {algorithm, fun algorithm/1} + , {iteration_count, fun iteration_count/1} + ]. + +server_type(type) -> hoconsc:enum(['built-in-database']); +server_type(default) -> 'built-in-database'; +server_type(_) -> undefined. + +algorithm(type) -> hoconsc:enum([sha256, sha256]); +algorithm(default) -> sha256; +algorithm(_) -> undefined. + +iteration_count(type) -> non_neg_integer(); +iteration_count(default) -> 4096; +iteration_count(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, Authenticator, #{algorithm := Algorithm, + iteration_count := IterationCount}) -> + State = #{user_group => {ChainID, Authenticator}, + algorithm => Algorithm, + iteration_count => IterationCount}, + {ok, State}. + +update(_ChainID, _Authenticator, _Config, _State) -> + {error, update_not_suppored}. + +authenticate(#{auth_method := AuthMethod, + auth_data := AuthData, + auth_cache := AuthCache}, State) -> + case ensure_auth_method(AuthMethod, State) of + true -> + case AuthCache of + #{next_step := client_final} -> + check_client_final_message(AuthData, AuthCache, State); + _ -> + check_client_first_message(AuthData, AuthCache, State) + end; + false -> + ignore + end; +authenticate(_Credential, _State) -> + ignore. + +destroy(#{user_group := UserGroup}) -> + trans( + fun() -> + MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun(UserCredential) -> + mnesia:delete_object(?TAB, UserCredential, write) + end, mnesia:select(?TAB, MatchSpec, write)) + end). + +%% TODO: binary to atom +add_user(#{<<"user_id">> := UserID, + <<"password">> := Password}, #{user_group := UserGroup} = State) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + add_user(UserID, Password, State), + {ok, #{user_id => UserID}}; + [_] -> + {error, already_exist} + end + end). + +delete_user(UserID, #{user_group := UserGroup}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + {error, not_found}; + [_] -> + mnesia:delete(?TAB, {UserGroup, UserID}, write) + end + end). + +update_user(UserID, #{<<"password">> := Password}, + #{user_group := UserGroup} = State) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + {error, not_found}; + [_] -> + add_user(UserID, Password, State), + {ok, #{user_id => UserID}} + end + end). + +lookup_user(UserID, #{user_group := UserGroup}) -> + case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of + [#scram_user_credentail{user_id = {_, UserID}}] -> + {ok, #{user_id => UserID}}; + [] -> + {error, not_found} + end. + +%% TODO: Support Pagination +list_users(#{user_group := UserGroup}) -> + Users = [#{user_id => UserID} || + #scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], + {ok, Users}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +ensure_auth_method('SCRAM-SHA-256', #{algorithm := sha256}) -> + true; +ensure_auth_method('SCRAM-SHA-512', #{algorithm := sha512}) -> + true; +ensure_auth_method(_, _) -> + false. + +check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> + LookupFun = fun(Username) -> + lookup_user2(Username, State) + end, + case esasl_scram:check_client_first_message( + Bin, + #{iteration_count => IterationCount, + lookup => LookupFun} + ) of + {cotinue, ServerFirstMessage, Cache} -> + {cotinue, ServerFirstMessage, Cache}; + {error, _Reason} -> + {error, not_authorized} + end. + +check_client_final_message(Bin, Cache, #{algorithm := Alg}) -> + case esasl_scram:check_client_final_message( + Bin, + Cache#{algorithm => Alg} + ) of + {ok, ServerFinalMessage} -> + {ok, ServerFinalMessage}; + {error, _Reason} -> + {error, not_authorized} + end. + +add_user(UserID, Password, State) -> + UserCredential = esasl_scram:generate_user_credential(UserID, Password, State), + mnesia:write(?TAB, UserCredential, write). + +lookup_user2(UserID, #{user_group := UserGroup}) -> + case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of + [#scram_user_credentail{} = UserCredential] -> + {ok, UserCredential}; + [] -> + {error, not_found} + end. + +%% TODO: Move to emqx_authn_utils.erl +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 692ff924e..0b36cf350 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -46,10 +46,9 @@ structs() -> [""]. fields("") -> - [ {config, #{type => hoconsc:union( - [ hoconsc:ref(?MODULE, get) - , hoconsc:ref(?MODULE, post) - ])}} + [ {config, {union, [ hoconsc:t(get) + , hoconsc:t(post) + ]}} ]; fields(get) -> @@ -64,7 +63,8 @@ fields(post) -> ] ++ common_fields(). common_fields() -> - [ {url, fun url/1} + [ {server_type, {enum, ['http-server']}} + , {url, fun url/1} , {accept, fun accept/1} , {headers, fun headers/1} , {form_data, fun form_data/1} @@ -142,10 +142,12 @@ update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = Stat {error, Reason} -> {error, Reason} end. -authenticate(ClientInfo, #{resource_id := ResourceID, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(Credential, #{resource_id := ResourceID, method := Method, request_timeout := RequestTimeout} = State) -> - Request = generate_request(ClientInfo, State), + Request = generate_request(Credential, State), case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of {ok, 204, _Headers} -> ok; {ok, 200, Headers, Body} -> @@ -154,8 +156,8 @@ authenticate(ClientInfo, #{resource_id := ResourceID, {ok, _NBody} -> %% TODO: Return by user property ok; - {error, Reason} -> - {stop, Reason} + {error, _Reason} -> + ok end; {error, _Reason} -> ignore @@ -208,13 +210,13 @@ generate_base_url(#{scheme := Scheme, port := Port}) -> iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])). -generate_request(ClientInfo, #{method := Method, +generate_request(Credential, #{method := Method, path := Path, base_query := BaseQuery, content_type := ContentType, headers := Headers, form_data := FormData0}) -> - FormData = replace_placeholders(FormData0, ClientInfo), + FormData = replace_placeholders(FormData0, Credential), case Method of get -> NPath = append_query(Path, BaseQuery ++ FormData), @@ -225,9 +227,9 @@ generate_request(ClientInfo, #{method := Method, {NPath, Headers, Body} end. -replace_placeholders(FormData0, ClientInfo) -> +replace_placeholders(FormData0, Credential) -> FormData = lists:map(fun({K, V0}) -> - case replace_placeholder(V0, ClientInfo) of + case replace_placeholder(V0, Credential) of undefined -> {K, undefined}; V -> {K, bin(V)} end @@ -236,16 +238,16 @@ replace_placeholders(FormData0, ClientInfo) -> V =/= undefined end, FormData). -replace_placeholder(<<"${mqtt-username}">>, ClientInfo) -> - maps:get(username, ClientInfo, undefined); -replace_placeholder(<<"${mqtt-clientid}">>, ClientInfo) -> - maps:get(clientid, ClientInfo, undefined); -replace_placeholder(<<"${ip-address}">>, ClientInfo) -> - maps:get(peerhost, ClientInfo, undefined); -replace_placeholder(<<"${cert-subject}">>, ClientInfo) -> - maps:get(dn, ClientInfo, undefined); -replace_placeholder(<<"${cert-common-name}">>, ClientInfo) -> - maps:get(cn, ClientInfo, undefined); +replace_placeholder(<<"${mqtt-username}">>, Credential) -> + maps:get(username, Credential, undefined); +replace_placeholder(<<"${mqtt-clientid}">>, Credential) -> + maps:get(clientid, Credential, undefined); +replace_placeholder(<<"${ip-address}">>, Credential) -> + maps:get(peerhost, Credential, undefined); +replace_placeholder(<<"${cert-subject}">>, Credential) -> + maps:get(dn, Credential, undefined); +replace_placeholder(<<"${cert-common-name}">>, Credential) -> + maps:get(cn, Credential, undefined); replace_placeholder(Constant, _) -> Constant. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl index 95e4b3d6d..d6e977be6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl @@ -132,10 +132,7 @@ handle_options(#{endpoint := Endpoint, refresh_interval => limit_refresh_interval(RefreshInterval0), ssl_opts => maps:to_list(SSLOpts), jwks => [], - request_id => undefined}; - -handle_options(#{enable_ssl := false} = Opts) -> - handle_options(Opts#{ssl_opts => #{}}). + request_id => undefined}. refresh_jwks(#{endpoint := Endpoint, ssl_opts := SSLOpts} = State) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 8fae45ff4..2605a682d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -35,21 +35,14 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +structs() -> [""]. 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') - ]}]; + [ {config, {union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + ]}} + ]; fields('hmac-based') -> [ {use_jwks, {enum, [false]}} @@ -67,35 +60,35 @@ fields('public-key') -> ]; fields('jwks') -> - [ {enable_ssl, {enum, [false]}} - ] ++ jwks_fields(); + [ {use_jwks, {enum, [true]}} + , {endpoint, fun endpoint/1} + , {refresh_interval, fun refresh_interval/1} + , {verify_claims, fun verify_claims/1} + , {ssl, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, ssl_enable) + , hoconsc:ref(?MODULE, ssl_disable) + ]), + default => #{<<"enable">> => false}}} + ]; -fields('jwks-using-ssl') -> - [ {enable_ssl, {enum, [true]}} - , {ssl_opts, fun ssl_opts/1} - ] ++ jwks_fields(); - -fields(ssl_opts) -> - [ {cacertfile, fun cacertfile/1} +fields(ssl_enable) -> + [ {enable, #{type => true}} + , {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(ssl_disable) -> + [ {enable, #{type => false}} ]; + 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. @@ -109,9 +102,9 @@ certificate(_) -> undefined. endpoint(type) -> string(); endpoint(_) -> undefined. -ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); -ssl_opts(default) -> []; -ssl_opts(_) -> undefined. +% ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); +% ssl_opts(default) -> []; +% ssl_opts(_) -> undefined. refresh_interval(type) -> integer(); refresh_interval(default) -> 300; @@ -169,7 +162,9 @@ update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Conn update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> create(Config). -authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(Credential = #{password := JWT}, #{jwk := JWK, verify_claims := VerifyClaims0}) -> JWKs = case erlang:is_pid(JWK) of false -> @@ -178,11 +173,11 @@ authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, {ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK), JWKs0 end, - VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), + VerifyClaims = replace_placeholder(VerifyClaims0, Credential), case verify(JWT, JWKs, VerifyClaims) of ok -> ok; {error, invalid_signature} -> ignore; - {error, {claims, _}} -> {stop, bad_password} + {error, {claims, _}} -> {error, bad_username_or_password} end. destroy(#{jwk := Connector}) when is_pid(Connector) -> @@ -222,8 +217,13 @@ create2(#{use_jwks := false, verify_claims => VerifyClaims}}; create2(#{use_jwks := true, - verify_claims := VerifyClaims} = Config) -> - case emqx_authn_jwks_connector:start_link(Config) of + verify_claims := VerifyClaims, + ssl := #{enable := Enable} = SSL} = Config) -> + SSLOpts = case Enable of + true -> maps:without(enable, SSL); + false -> #{} + end, + case emqx_authn_jwks_connector:start_link(Config#{ssl_opts => SSLOpts}) of {ok, Connector} -> {ok, #{jwk => Connector, verify_claims => VerifyClaims}}; diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 26b20c517..4b1bcbb76 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -55,7 +55,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --define(TAB, mnesia_basic_auth). +-define(TAB, ?MODULE). -rlog_shard({?AUTH_SHARD, ?TAB}). %%------------------------------------------------------------------------------ @@ -81,7 +81,8 @@ mnesia(copy) -> structs() -> [config]. fields(config) -> - [ {user_id_type, fun user_id_type/1} + [ {server_type, {enum, ['built-in-database']}} + , {user_id_type, fun user_id_type/1} , {password_hash_algorithm, fun password_hash_algorithm/1} ]; @@ -95,11 +96,11 @@ fields(other_algorithms) -> ]. user_id_type(type) -> user_id_type(); -user_id_type(default) -> clientid; +user_id_type(default) -> username; user_id_type(_) -> undefined. password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; -password_hash_algorithm(default) -> sha256; +password_hash_algorithm(default) -> #{<<"name">> => sha256}; password_hash_algorithm(_) -> undefined. salt_rounds(type) -> integer(); @@ -130,11 +131,13 @@ create(ChainID, AuthenticatorName, #{user_id_type := Type, update(ChainID, AuthenticatorName, Config, _State) -> create(ChainID, AuthenticatorName, Config). -authenticate(ClientInfo = #{password := Password}, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(#{password := Password} = Credential, #{user_group := UserGroup, user_id_type := Type, password_hash_algorithm := Algorithm}) -> - UserID = get_user_identity(ClientInfo, Type), + UserID = get_user_identity(Credential, Type), case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; @@ -145,7 +148,7 @@ authenticate(ClientInfo = #{password := Password}, end, case PasswordHash =:= hash(Algorithm, Password, Salt) of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end end. @@ -330,8 +333,7 @@ gen_salt(#{password_hash_algorithm := bcrypt, {ok, Salt} = bcrypt:gen_salt(Rounds), Salt; gen_salt(_) -> - <> = crypto:strong_rand_bytes(16), - iolist_to_binary(io_lib:format("~32.16.0b", [X])). + emqx_authn_utils:gen_salt(). hash(bcrypt, Password, Salt) -> {ok, Hash} = bcrypt:hashpw(Password, Salt), @@ -343,10 +345,10 @@ insert_user(UserGroup, UserID, PasswordHash) -> insert_user(UserGroup, UserID, PasswordHash, <<>>). insert_user(UserGroup, UserID, PasswordHash, Salt) -> - Credential = #user_info{user_id = {UserGroup, UserID}, - password_hash = PasswordHash, - salt = Salt}, - mnesia:write(?TAB, Credential, write). + UserInfo = #user_info{user_id = {UserGroup, UserID}, + password_hash = PasswordHash, + salt = Salt}, + mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> mnesia:delete_object(?TAB, UserInfo, write). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index cc4445eaf..c76ece1c4 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -36,7 +36,8 @@ structs() -> [config]. fields(config) -> - [ {password_hash_algorithm, fun password_hash_algorithm/1} + [ {server_type, {enum, [mysql]}} + , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} , {query_timeout, fun query_timeout/1} @@ -81,12 +82,14 @@ update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = Stat {error, Reason} -> {error, Reason} end. -authenticate(#{password := Password} = ClientInfo, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(#{password := Password} = Credential, #{resource_id := ResourceID, placeholders := PlaceHolders, query := Query, query_timeout := Timeout} = State) -> - Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential), case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> @@ -106,14 +109,14 @@ destroy(#{resource_id := ResourceID}) -> %%------------------------------------------------------------------------------ check_password(undefined, _Algorithm, _Selected) -> - {stop, bad_password}; + {error, bad_username_or_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} + false -> {error, bad_username_or_password} end; check_password(Password, #{password_hash := Hash} = Selected, @@ -126,7 +129,7 @@ check_password(Password, end, case Hash0 =:= Hash of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end. %% TODO: Support prepare diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index c9046c606..700298c46 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -36,9 +36,10 @@ structs() -> [config]. fields(config) -> - [ {password_hash_algorithm, fun password_hash_algorithm/1} - , {salt_position, {enum, [prefix, suffix]}} - , {query, fun query/1} + [ {server_type, {enum, [pgsql]}} + , {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} ] ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). @@ -75,11 +76,13 @@ update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> {error, Reason} -> {error, Reason} end. -authenticate(#{password := Password} = ClientInfo, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(#{password := Password} = Credential, #{resource_id := ResourceID, query := Query, placeholders := PlaceHolders} = State) -> - Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential), case emqx_resource:query(ResourceID, {sql, Query, Params}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> @@ -99,14 +102,14 @@ destroy(#{resource_id := ResourceID}) -> %%------------------------------------------------------------------------------ check_password(undefined, _Algorithm, _Selected) -> - {stop, bad_password}; + {error, bad_username_or_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} + false -> {error, bad_username_or_password} end; check_password(Password, #{password_hash := Hash} = Selected, @@ -119,7 +122,7 @@ check_password(Password, end, case Hash0 =:= Hash of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end. %% TODO: Support prepare diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 17c08cc70..827eb49ab 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include("emqx_authn.hrl"). + -define(AUTH, emqx_authn). all() -> @@ -40,16 +42,17 @@ end_per_suite(_) -> set_special_configs(emqx_authn) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authn, "test")), - Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}}, 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(_) -> + ?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)), + ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, + Chain = #{id => ChainID}, ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), @@ -57,86 +60,37 @@ t_chain(_) -> ?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', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', 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)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)), + ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(?CHAIN, 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, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), + ?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), - ?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)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 27f34f936..008deca3d 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include("emqx_authn.hrl"). + -define(AUTH, emqx_authn). all() -> @@ -39,18 +41,13 @@ end_per_suite(_) -> set_special_configs(emqx_authn) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authn, "test")), - Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}}, 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', @@ -58,84 +55,74 @@ t_jwt_authenticator(_) -> secret_base64_encoded => false, verify_claims => []}, AuthenticatorConfig = #{name => AuthenticatorName, - type => jwt, + mechanism => jwt, config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), - - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), ClientInfo2 = ClientInfo#{password => BadJWS}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), %% 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)), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config2)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), 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">>})), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config3)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), %% Expiration Payload3 = #{ <<"username">> => <<"myuser">> , <<"exp">> => erlang:system_time(second) - 60}, JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), ClientInfo3 = ClientInfo#{password => JWS3}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), 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)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), %% 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)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), 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)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)), %% 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)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), 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)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), 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"])), @@ -145,23 +132,18 @@ t_jwt_authenticator2(_) -> certificate => PublicKey, verify_claims => []}, AuthenticatorConfig = #{name => AuthenticatorName, - type => jwt, + mechanism => jwt, config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), - - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('public-key', Payload, PrivateKey), - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ok. generate_jws('hmac-based', Payload, Secret) -> diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index abc7ad149..75dd497ae 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include("emqx_authn.hrl"). + -define(AUTH, emqx_authn). all() -> @@ -39,149 +41,125 @@ end_per_suite(_) -> set_special_configs(emqx_authn) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authn, "test")), - Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}}, 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)), + ct:pal("11111 ~p~n", [?AUTH:list_authenticators(<<"mqtt">>)]), + AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, 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">>)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), - - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)), ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), - ?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, 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">>)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertEqual([], ets:tab2list(mnesia_basic_auth)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), 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', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, 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">>)), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)), - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), - - ClientInfo1 = #{listener_id => ListenerID, - username => <<"myuser1">>, + ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, password => <<"mypassword3">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), - - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), 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', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorConfig2 = #{name => AuthenticatorName2, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', 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, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(ChainID, AuthenticatorName1, + ?AUTH:add_user(?CHAIN, AuthenticatorName1, #{<<"user_id">> => <<"myuser">>, <<"password">> => <<"mypass1">>})), ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(ChainID, AuthenticatorName2, + ?AUTH:add_user(?CHAIN, AuthenticatorName2, #{<<"user_id">> => <<"myclient">>, <<"password">> => <<"mypass2">>})), - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), - - ClientInfo1 = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo1 = #{username => <<"myuser">>, clientid => <<"myclient">>, password => <<"mypass1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), ok. diff --git a/apps/emqx_exproto/src/emqx_exproto_channel.erl b/apps/emqx_exproto/src/emqx_exproto_channel.erl index d45f445ab..5b66cacbc 100644 --- a/apps/emqx_exproto/src/emqx_exproto_channel.erl +++ b/apps/emqx_exproto/src/emqx_exproto_channel.erl @@ -260,25 +260,21 @@ handle_call({auth, ClientInfo0, Password}, Channel = #channel{conninfo = ConnInfo, clientinfo = ClientInfo}) -> ClientInfo1 = enrich_clientinfo(ClientInfo0, ClientInfo), - NConnInfo = enrich_conninfo(ClientInfo1, ConnInfo), + ConnInfo1 = enrich_conninfo(ClientInfo1, ConnInfo), - Channel1 = Channel#channel{conninfo = NConnInfo, + Channel1 = Channel#channel{conninfo = ConnInfo1, clientinfo = ClientInfo1}, #{clientid := ClientId, username := Username} = ClientInfo1, case emqx_access_control:authenticate(ClientInfo1#{password => Password}) of - {ok, AuthResult} -> + ok -> emqx_logger:set_metadata_clientid(ClientId), - is_anonymous(AuthResult) andalso - emqx_metrics:inc('client.auth.anonymous'), - NClientInfo = maps:merge(ClientInfo1, AuthResult), - NChannel = Channel1#channel{clientinfo = NClientInfo}, - case emqx_cm:open_session(true, NClientInfo, NConnInfo) of + case emqx_cm:open_session(true, ClientInfo1, ConnInfo1) of {ok, _Session} -> ?LOG(debug, "Client ~s (Username: '~s') authorized successfully!", [ClientId, Username]), - {reply, ok, [{event, connected}], ensure_connected(NChannel)}; + {reply, ok, [{event, connected}], ensure_connected(Channel1)}; {error, Reason} -> ?LOG(warning, "Client ~s (Username: '~s') open session failed for ~0p", [ClientId, Username, Reason]), @@ -393,9 +389,6 @@ terminate(Reason, Channel) -> Req = #{reason => stringfy(Reason)}, try_dispatch(on_socket_closed, wrap(Req), Channel). -is_anonymous(#{anonymous := true}) -> true; -is_anonymous(_AuthResult) -> false. - %%-------------------------------------------------------------------- %% Sub/UnSub %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 5dadf2aca..f43620996 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -33,7 +33,7 @@ %% Gateway ID , type := gateway_type() %% Autenticator - , auth := allow_anonymous | emqx_authentication:chain_id() + , auth := emqx_authn:chain_id() %% The ConnectionManager PID , cm := pid() }. @@ -66,19 +66,19 @@ -spec authenticate(context(), emqx_types:clientinfo()) -> {ok, emqx_types:clientinfo()} | {error, any()}. -authenticate(_Ctx = #{auth := allow_anonymous}, ClientInfo) -> - {ok, ClientInfo#{anonymous => true}}; authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> ClientInfo = ClientInfo0#{ zone => undefined, chain_id => ChainId }, case emqx_access_control:authenticate(ClientInfo) of - {ok, AuthResult} -> - {ok, mountpoint(maps:merge(ClientInfo, AuthResult))}; + ok -> + {ok, mountpoint(ClientInfo)}; {error, Reason} -> {error, Reason} - end. + end; +authenticate(_Ctx, ClientInfo) -> + {ok, ClientInfo}. %% @doc Register the session to the cluster. %% diff --git a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl index 34c72dcca..51c278b56 100644 --- a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl +++ b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl @@ -86,15 +86,14 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> ClientInfo = clientinfo(Lwm2mState), _ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined), case emqx_access_control:authenticate(ClientInfo) of - {ok, AuthResult} -> + ok -> _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined), - ClientInfo1 = maps:merge(ClientInfo, AuthResult), Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683), - ClientInfo2 = maps:put(sockport, Sockport, ClientInfo1), + ClientInfo1 = maps:put(sockport, Sockport, ClientInfo), Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(), - mountpoint = maps:get(mountpoint, ClientInfo2)}, - run_hooks('client.connected', [ClientInfo2, conninfo(Lwm2mState1)]), + mountpoint = maps:get(mountpoint, ClientInfo1)}, + run_hooks('client.connected', [ClientInfo1, conninfo(Lwm2mState1)]), erlang:send(CoapPid, post_init), erlang:send_after(2000, CoapPid, auto_observe), diff --git a/rebar.config b/rebar.config index 2a58dc142..0c741e907 100644 --- a/rebar.config +++ b/rebar.config @@ -63,6 +63,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} + , {esasl, {git, "https://github.com/emqx/esasl", {branch, "refactor/sasl"}}} ]}. {xref_ignores,