diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 41e74018e..b7731fdb5 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -18,23 +18,18 @@ -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, listener := Listener}) -> - AuthResult = default_auth_result(Zone, Listener), - return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult)). +-spec(authenticate(emqx_types:clientinfo()) -> + ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). +authenticate(Credential = #{zone := Zone, listener := Listener}) -> + run_hooks('client.authenticate', [Credential], ok) %% @doc Check ACL -spec(authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) @@ -60,18 +55,6 @@ do_authorize(ClientInfo, PubSub, Topic) -> _Other -> deny end. -default_auth_result(Zone, Listener) -> - case emqx_config:get_listener_conf(Zone, Listener, [auth, enable]) of - false -> #{auth_result => success, anonymous => true}; - true -> #{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 5660863a7..c10b937e5 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -102,7 +102,7 @@ -type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). --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()]} @@ -283,65 +283,77 @@ 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(), Channel = #channel{conn_state = connecting}) -> + handle_out(connack, ?RC_PROTOCOL_ERROR, Channel); + handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> case pipeline([fun enrich_conninfo/2, fun run_conn_hooks/2, 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) -> @@ -481,9 +493,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}) -> @@ -980,8 +994,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, listener := Listener}}) -> + #channel{conn_state = ConnState, + clientinfo = ClientInfo = #{zone := Zone listener := Listener}}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> emqx_config:get_listener_conf(Zone, Listener, [flapping_detect, enable]) andalso emqx_flapping:detect(ClientInfo), Channel1 = ensure_disconnected(Reason, mabye_publish_will_msg(Channel)), @@ -1254,75 +1269,60 @@ check_banned(_ConnPkt, #channel{clientinfo = ClientInfo}) -> 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. %%-------------------------------------------------------------------- @@ -1723,7 +1723,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/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index 054f839a3..f5bf35acf 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -33,10 +33,7 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_authenticate(_) -> - toggle_auth(true), - ?assertMatch({error, _}, emqx_access_control:authenticate(clientinfo())), - toggle_auth(false), - ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). + ?assertMatch(ok, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 6b468c480..87b93a8e7 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -181,7 +181,7 @@ init_per_suite(Config) -> %% Access Control Meck ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, - fun(_) -> {ok, #{auth_result => success}} end), + fun(_) -> ok end), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), @@ -268,35 +268,40 @@ t_handle_in_unexpected_packet(_) -> {ok, [{outgoing, Packet}, {close, protocol_error}], Channel} = emqx_channel:handle_in(?PUBLISH_PACKET(?QOS_0), Channel). -t_handle_in_connect_auth_failed(_) -> - ConnPkt = #mqtt_packet_connect{ - proto_name = <<"MQTT">>, - proto_ver = ?MQTT_PROTO_V5, - is_bridge = false, - clean_start = true, - keepalive = 30, - properties = #{ - 'Authentication-Method' => <<"failed_auth_method">>, - 'Authentication-Data' => <<"failed_auth_data">> - }, - clientid = <<"clientid">>, - username = <<"username">> - }, - {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} = - emqx_channel:handle_in(?CONNECT_PACKET(ConnPkt), channel(#{conn_state => idle})). +% t_handle_in_connect_auth_failed(_) -> +% ConnPkt = #mqtt_packet_connect{ +% proto_name = <<"MQTT">>, +% proto_ver = ?MQTT_PROTO_V5, +% is_bridge = false, +% clean_start = true, +% keepalive = 30, +% properties = #{ +% 'Authentication-Method' => <<"failed_auth_method">>, +% 'Authentication-Data' => <<"failed_auth_data">> +% }, +% clientid = <<"clientid">>, +% username = <<"username">> +% }, +% {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} = +% emqx_channel:handle_in(?CONNECT_PACKET(ConnPkt), channel(#{conn_state => idle})). t_handle_in_continue_auth(_) -> Properties = #{ 'Authentication-Method' => <<"failed_auth_method">>, 'Authentication-Data' => <<"failed_auth_data">> }, - {shutdown, bad_authentication_method, ?CONNACK_PACKET(?RC_BAD_AUTHENTICATION_METHOD), _} = - emqx_channel:handle_in(?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION,Properties), channel()), - {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} = + + Channel1 = channel(#{conn_state => connected}), + {ok, [{outgoing, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR)}, {close, protocol_error}], Channel1} = + emqx_channel:handle_in(?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, Properties), Channel1), + + Channel2 = channel(#{conn_state => connecting}), + ConnInfo = emqx_channel:info(conninfo, Channel2), + Channel3 = emqx_channel:set_field(conninfo, ConnInfo#{conn_props => Properties}, Channel2), + + {ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS)}], _} = emqx_channel:handle_in( - ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION,Properties), - channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => Properties}}) - ). + ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, Properties), Channel3). t_handle_in_re_auth(_) -> Properties = #{ @@ -315,10 +320,14 @@ t_handle_in_re_auth(_) -> ?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => undefined}}) ), - {ok, [{outgoing, ?DISCONNECT_PACKET(?RC_NOT_AUTHORIZED)}, {close, not_authorized}], _} = + + Channel1 = channel(), + ConnInfo = emqx_channel:info(conninfo, Channel1), + Channel2 = emqx_channel:set_field(conninfo, ConnInfo#{conn_props => Properties}, Channel1), + + {ok, ?AUTH_PACKET(?RC_SUCCESS), _} = emqx_channel:handle_in( - ?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), - channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => Properties}}) + ?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), Channel2 ). t_handle_in_qos0_publish(_) -> @@ -494,8 +503,8 @@ t_handle_in_disconnect(_) -> t_handle_in_auth(_) -> Channel = channel(#{conn_state => connected}), - Packet = ?DISCONNECT_PACKET(?RC_IMPLEMENTATION_SPECIFIC_ERROR), - {ok, [{outgoing, Packet}, {close, implementation_specific_error}], Channel} = + Packet = ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), + {ok, [{outgoing, Packet}, {close, protocol_error}], Channel} = emqx_channel:handle_in(?AUTH_PACKET(), Channel). t_handle_in_frame_error(_) -> @@ -809,7 +818,7 @@ t_enrich_client(_) -> {ok, _ConnPkt, _Chan} = emqx_channel:enrich_client(connpkt(), channel()). t_auth_connect(_) -> - {ok, _Chan} = emqx_channel:auth_connect(connpkt(), channel()). + {ok, _, _Chan} = emqx_channel:authenticate(?CONNECT_PACKET(connpkt()), channel()). t_process_alias(_) -> Publish = #mqtt_packet_publish{topic_name = <<>>, properties = #{'Topic-Alias' => 1}}, diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index ecd49d5a5..3e69ae46d 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,26 +1,13 @@ 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"] - # } + enable: false + 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/rebar.config b/apps/emqx_authn/rebar.config index 73696b033..32b5a43e0 100644 --- a/apps/emqx_authn/rebar.config +++ b/apps/emqx_authn/rebar.config @@ -1,4 +1,6 @@ -{deps, []}. +{deps, [ + {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} +]}. {edoc_opts, [{preprocess, true}]}. {erl_opts, [warn_unused_vars, diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index c997582ec..3b89d2a99 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {modules, []}, {registered, [emqx_authn_sup, emqx_authn_registry]}, - {applications, [kernel,stdlib]}, + {applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql,jose]}, {mod, {emqx_authn_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, 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..225969cd2 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -38,40 +38,22 @@ stop(_State) -> ok. initialize() -> - #{chains := Chains, - bindings := Bindings} = emqx_config:get([authn], #{chains => [], bindings => []}), - initialize_chains(Chains), - initialize_bindings(Bindings). + AuthNConfig = emqx_config:get([emqx_authn], #{enable => false, + authenticators => []}), + initialize(AuthNConfig). -initialize_chains([]) -> +initialize(#{enable := Enable, authenticators := Authenticators}) -> + {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), + initialize_authenticators(Authenticators), + Enable =:= true andalso emqx_authn:enable(), + ok. + +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..7ed5a9999 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -21,94 +21,61 @@ -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}]; - -fields('simple-chain') -> - [ {id, fun chain_id/1} - , {type, {enum, [simple]}} - , {authenticators, fun simple_authenticators/1} + [ {enable, fun enable/1} + , {authenticators, fun authenticators/1} ]; -% fields('enhanced-chain') -> -% [ {id, fun chain_id/1} -% , {type, {enum, [enhanced]}} -% , {authenticators, fun enhanced_authenticators/1} -% ]; - -fields(binding) -> - [ {chain_id, fun chain_id/1} - , {listeners, fun listeners/1} +fields('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('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. +enable(type) -> boolean(); +enable(defualt) -> false; +enable(_) -> 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..14240b578 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} @@ -72,7 +72,7 @@ common_fields() -> ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)). validations() -> - [ {check_ssl_opts, fun emqx_connector_http:check_ssl_opts/1} ]. + [ {check_ssl_opts, fun check_ssl_opts/1} ]. url(type) -> binary(); url(nullable) -> false; @@ -108,26 +108,30 @@ create(ChainID, AuthenticatorName, #{method := Method, url := URL, accept := Accept, - content_type := ContentType, headers := Headers, form_data := FormData, request_timeout := RequestTimeout} = Config) -> - NHeaders = maps:merge(#{<<"accept">> => atom_to_binary(Accept, utf8), - <<"content-type">> => atom_to_binary(ContentType, utf8)}, Headers), + ContentType = maps:get(content_type, Config, undefined), + DefaultHeader0 = case ContentType of + undefined -> #{}; + _ -> #{<<"content-type">> => atom_to_binary(ContentType, utf8)} + end, + DefaultHeader = DefaultHeader0#{<<"accept">> => atom_to_binary(Accept, utf8)}, + NHeaders = maps:to_list(maps:merge(DefaultHeader, maps:from_list(Headers))), NFormData = preprocess_form_data(FormData), #{path := Path, query := Query} = URIMap = parse_url(URL), BaseURL = generate_base_url(URIMap), State = #{method => Method, path => Path, - base_query => cow_qs:parse_qs(Query), + base_query => cow_qs:parse_qs(list_to_binary(Query)), accept => Accept, content_type => ContentType, headers => NHeaders, form_data => NFormData, request_timeout => RequestTimeout}, ResourceID = <>, - case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url := BaseURL}) of + case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url => BaseURL}) of {ok, _} -> {ok, State#{resource_id => ResourceID}}; {error, already_created} -> @@ -142,10 +146,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 +160,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 @@ -190,6 +196,9 @@ check_form_data(FormData) -> false end. +check_ssl_opts(Conf) -> + emqx_connector_http:check_ssl_opts("url", Conf). + preprocess_form_data(FormData) -> KVs = binary:split(FormData, [<<"&">>], [global]), [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs]. @@ -208,13 +217,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 +234,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 +245,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..437dac72d 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,10 +102,6 @@ certificate(_) -> undefined. endpoint(type) -> string(); endpoint(_) -> undefined. -ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); -ssl_opts(default) -> []; -ssl_opts(_) -> undefined. - refresh_interval(type) -> integer(); refresh_interval(default) -> 300; refresh_interval(validator) -> [fun(I) -> I > 0 end]; @@ -169,7 +158,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 +169,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 +213,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..b93e32e3d 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,46 @@ 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. + +t_authenticate(_) -> + ?assertEqual(false, emqx_zone:get_env(external, bypass_auth_plugins, false)), + ClientInfo = #{zone => external, + username => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + emqx_authn:enable(), + ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)). 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..fe7d244cd 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,131 @@ 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)), - 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]), + ?assertEqual(false, emqx_zone:get_env(external, bypass_auth_plugins, false)), - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{zone => external, + username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?AUTH:enable(), + ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), + ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), + ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), 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_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index 3d09d0f92..c98b63cc2 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -113,7 +113,7 @@ init({ClientId, Username, Password, Channel}) -> password = Password}, _ = run_hooks('client.connect', [conninfo(State0)], undefined), case emqx_access_control:authenticate(clientinfo(State0)) of - {ok, _AuthResult} -> + ok -> ok = emqx_cm:discard_session(ClientId), _ = run_hooks('client.connack', [conninfo(State0), success], undefined), diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 9bfbb9277..cbeff37eb 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -4,7 +4,6 @@ ]}. {deps, [ - {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}, {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, {epgsql, {git, "https://github.com/epgsql/epgsql", {tag, "4.4.0"}}}, diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index bef5c26f3..94940a065 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -32,7 +32,7 @@ , fields/1 , validations/0]). --export([ check_ssl_opts/1 ]). +-export([ check_ssl_opts/2 ]). -type connect_timeout() :: non_neg_integer() | infinity. -type pool_type() :: random | hash. @@ -57,7 +57,7 @@ fields(config) -> , {pool_type, fun pool_type/1} , {pool_size, fun pool_size/1} , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), - nullable => true}} + default => #{}}} ]; fields(ssl_opts) -> @@ -107,8 +107,9 @@ keyfile(type) -> string(); keyfile(nullable) -> true; keyfile(_) -> undefined. +%% TODO: certfile is required certfile(type) -> string(); -certfile(nullable) -> false; +certfile(nullable) -> true; certfile(_) -> undefined. verify(type) -> boolean(); @@ -178,7 +179,7 @@ on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery, #{pool_name : end, Result. -on_health_check(_InstId, #{server := {Host, Port}} = State) -> +on_health_check(_InstId, #{host := Host, port := Port} = State) -> case gen_tcp:connect(Host, Port, emqx_misc:ipv6_probe([]), 3000) of {ok, Sock} -> gen_tcp:close(Sock), @@ -199,13 +200,16 @@ check_base_url(URL) -> end. check_ssl_opts(Conf) -> - URL = hocon_schema:get_value("url", Conf), + check_ssl_opts("base_url", Conf). + +check_ssl_opts(URLFrom, Conf) -> + URL = hocon_schema:get_value(URLFrom, Conf), {ok, #{scheme := Scheme}} = emqx_http_lib:uri_parse(URL), SSLOpts = hocon_schema:get_value("ssl_opts", Conf), - case {Scheme, SSLOpts} of - {http, undefined} -> true; + case {Scheme, maps:size(SSLOpts)} of + {http, 0} -> true; {http, _} -> false; - {https, undefined} -> false; + {https, 0} -> false; {https, _} -> true end. 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 cacc21a9d..ab18d29be 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/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index 178e4b04d..aae681fd5 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -101,10 +101,10 @@ authorize_appid(Req) -> {basic, AppId, AppSecret} -> case emqx_mgmt_auth:is_authorized(AppId, AppSecret) of true -> ok; - false -> {401} + false -> {401, #{<<"WWW-Authenticate">> => <<"Basic Realm=\"minirest-server\"">>}, <<"UNAUTHORIZED">>} end; _ -> - {401} + {401, #{<<"WWW-Authenticate">> => <<"Basic Realm=\"minirest-server\"">>}, <<"UNAUTHORIZED">>} end. format(Port) when is_integer(Port) -> diff --git a/rebar.config b/rebar.config index 268e89965..68a199d56 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.10.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} + , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}} ]}. {xref_ignores,