diff --git a/apps/emqx_auth_http/include/emqx_auth_http.hrl b/apps/emqx_auth_http/include/emqx_auth_http.hrl index c0bfa2177..439087e9c 100644 --- a/apps/emqx_auth_http/include/emqx_auth_http.hrl +++ b/apps/emqx_auth_http/include/emqx_auth_http.hrl @@ -31,4 +31,6 @@ -define(AUTHN_TYPE, {?AUTHN_MECHANISM, ?AUTHN_BACKEND}). -define(AUTHN_TYPE_SCRAM, {?AUTHN_MECHANISM_SCRAM, ?AUTHN_BACKEND}). +-define(AUTHN_DATA_FIELDS, [is_superuser, client_attrs, expire_at, acl]). + -endif. diff --git a/apps/emqx_auth_http/src/emqx_auth_http_app.erl b/apps/emqx_auth_http/src/emqx_auth_http_app.erl index 3d8ae0dad..8b7d08c4e 100644 --- a/apps/emqx_auth_http/src/emqx_auth_http_app.erl +++ b/apps/emqx_auth_http/src/emqx_auth_http_app.erl @@ -25,7 +25,7 @@ start(_StartType, _StartArgs) -> ok = emqx_authz:register_source(?AUTHZ_TYPE, emqx_authz_http), ok = emqx_authn:register_provider(?AUTHN_TYPE, emqx_authn_http), - ok = emqx_authn:register_provider(?AUTHN_TYPE_SCRAM, emqx_authn_scram_http), + ok = emqx_authn:register_provider(?AUTHN_TYPE_SCRAM, emqx_authn_scram_restapi), {ok, Sup} = emqx_auth_http_sup:start_link(), {ok, Sup}. diff --git a/apps/emqx_auth_http/src/emqx_authn_http.erl b/apps/emqx_auth_http/src/emqx_authn_http.erl index b294de24f..edaa0ee5a 100644 --- a/apps/emqx_auth_http/src/emqx_authn_http.erl +++ b/apps/emqx_auth_http/src/emqx_authn_http.erl @@ -32,7 +32,9 @@ with_validated_config/2, generate_request/2, request_for_log/2, - response_for_log/1 + response_for_log/1, + extract_auth_data/2, + safely_parse_body/2 ]). %%------------------------------------------------------------------------------ @@ -209,34 +211,14 @@ handle_response(Headers, Body) -> case safely_parse_body(ContentType, Body) of {ok, NBody} -> body_to_auth_data(NBody); - {error, Reason} -> - ?TRACE_AUTHN_PROVIDER( - error, - "parse_http_response_failed", - #{content_type => ContentType, body => Body, reason => Reason} - ), + {error, _Reason} -> ignore end. body_to_auth_data(Body) -> case maps:get(<<"result">>, Body, <<"ignore">>) of <<"allow">> -> - IsSuperuser = emqx_authn_utils:is_superuser(Body), - Attrs = emqx_authn_utils:client_attrs(Body), - try - ExpireAt = expire_at(Body), - ACL = acl(ExpireAt, Body), - Result = merge_maps([ExpireAt, IsSuperuser, ACL, Attrs]), - {ok, Result} - catch - throw:{bad_acl_rule, Reason} -> - %% it's a invalid token, so ok to log - ?TRACE_AUTHN_PROVIDER("bad_acl_rule", Reason#{http_body => Body}), - {error, bad_username_or_password}; - throw:Reason -> - ?TRACE_AUTHN_PROVIDER("bad_response_body", Reason#{http_body => Body}), - {error, bad_username_or_password} - end; + extract_auth_data(http, Body); <<"deny">> -> {error, not_authorized}; <<"ignore">> -> @@ -245,6 +227,24 @@ body_to_auth_data(Body) -> ignore end. +extract_auth_data(Source, Body) -> + IsSuperuser = emqx_authn_utils:is_superuser(Body), + Attrs = emqx_authn_utils:client_attrs(Body), + try + ExpireAt = expire_at(Body), + ACL = acl(ExpireAt, Source, Body), + Result = merge_maps([ExpireAt, IsSuperuser, ACL, Attrs]), + {ok, Result} + catch + throw:{bad_acl_rule, Reason} -> + %% it's a invalid token, so ok to log + ?TRACE_AUTHN_PROVIDER("bad_acl_rule", Reason#{http_body => Body}), + {error, bad_username_or_password}; + throw:Reason -> + ?TRACE_AUTHN_PROVIDER("bad_response_body", Reason#{http_body => Body}), + {error, bad_username_or_password} + end. + merge_maps([]) -> #{}; merge_maps([Map | Maps]) -> maps:merge(Map, merge_maps(Maps)). @@ -283,40 +283,43 @@ expire_sec(#{<<"expire_at">> := _}) -> expire_sec(_) -> undefined. -acl(#{expire_at := ExpireTimeMs}, #{<<"acl">> := Rules}) -> +acl(#{expire_at := ExpireTimeMs}, Source, #{<<"acl">> := Rules}) -> #{ acl => #{ - source_for_logging => http, + source_for_logging => Source, rules => emqx_authz_rule_raw:parse_and_compile_rules(Rules), %% It's seconds level precision (like JWT) for authz %% see emqx_authz_client_info:check/1 expire => erlang:convert_time_unit(ExpireTimeMs, millisecond, second) } }; -acl(_NoExpire, #{<<"acl">> := Rules}) -> +acl(_NoExpire, Source, #{<<"acl">> := Rules}) -> #{ acl => #{ - source_for_logging => http, + source_for_logging => Source, rules => emqx_authz_rule_raw:parse_and_compile_rules(Rules) } }; -acl(_, _) -> +acl(_, _, _) -> #{}. safely_parse_body(ContentType, Body) -> try parse_body(ContentType, Body) catch - _Class:_Reason -> + _Class:Reason -> + ?TRACE_AUTHN_PROVIDER( + error, + "parse_http_response_failed", + #{content_type => ContentType, body => Body, reason => Reason} + ), {error, invalid_body} end. parse_body(<<"application/json", _/binary>>, Body) -> {ok, emqx_utils_json:decode(Body, [return_maps])}; parse_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) -> - Flags = [<<"result">>, <<"is_superuser">>], - RawMap = maps:from_list(cow_qs:parse_qs(Body)), - NBody = maps:with(Flags, RawMap), + NBody = maps:from_list(cow_qs:parse_qs(Body)), {ok, NBody}; parse_body(ContentType, _) -> {error, {unsupported_content_type, ContentType}}. diff --git a/apps/emqx_auth_http/src/emqx_authn_scram_http.erl b/apps/emqx_auth_http/src/emqx_authn_scram_restapi.erl similarity index 72% rename from apps/emqx_auth_http/src/emqx_authn_scram_http.erl rename to apps/emqx_auth_http/src/emqx_authn_scram_restapi.erl index 0e6190b4b..abb91f130 100644 --- a/apps/emqx_auth_http/src/emqx_authn_scram_http.erl +++ b/apps/emqx_auth_http/src/emqx_authn_scram_restapi.erl @@ -2,10 +2,19 @@ %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- --module(emqx_authn_scram_http). +%% Note: +%% This is not an implementation of the RFC 7804: +%% Salted Challenge Response HTTP Authentication Mechanism. +%% This backend is an implementation of scram, +%% which uses an external web resource as a source of user information. --include_lib("emqx_auth/include/emqx_authn.hrl"). +-module(emqx_authn_scram_restapi). + +-feature(maybe_expr, enable). + +-include("emqx_auth_http.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("emqx_auth/include/emqx_authn.hrl"). -behaviour(emqx_authn_provider). @@ -22,10 +31,6 @@ <<"salt">> ]). --define(OPTIONAL_USER_INFO_KEYS, [ - <<"is_superuser">> -]). - %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ @@ -72,7 +77,9 @@ authenticate( reason => Reason }) end, - emqx_utils_scram:authenticate(AuthMethod, AuthData, AuthCache, RetrieveFun, OnErrFun, State); + emqx_utils_scram:authenticate( + AuthMethod, AuthData, AuthCache, State, RetrieveFun, OnErrFun, ?AUTHN_DATA_FIELDS + ); authenticate(_Credential, _State) -> ignore. @@ -95,7 +102,7 @@ retrieve( ) -> Request = emqx_authn_http:generate_request(Credential#{username := Username}, State), Response = emqx_resource:simple_sync_query(ResourceId, {Method, Request, RequestTimeout}), - ?TRACE_AUTHN_PROVIDER("scram_http_response", #{ + ?TRACE_AUTHN_PROVIDER("scram_restapi_response", #{ request => emqx_authn_http:request_for_log(Credential, State), response => emqx_authn_http:response_for_log(Response), resource => ResourceId @@ -113,16 +120,11 @@ retrieve( handle_response(Headers, Body) -> ContentType = proplists:get_value(<<"content-type">>, Headers), - case safely_parse_body(ContentType, Body) of - {ok, NBody} -> - body_to_user_info(NBody); - {error, Reason} = Error -> - ?TRACE_AUTHN_PROVIDER( - error, - "parse_scram_http_response_failed", - #{content_type => ContentType, body => Body, reason => Reason} - ), - Error + maybe + {ok, NBody} ?= emqx_authn_http:safely_parse_body(ContentType, Body), + {ok, UserInfo} ?= body_to_user_info(NBody), + {ok, AuthData} ?= emqx_authn_http:extract_auth_data(scram_restapi, NBody), + {ok, maps:merge(AuthData, UserInfo)} end. body_to_user_info(Body) -> @@ -131,26 +133,16 @@ body_to_user_info(Body) -> true -> case safely_convert_hex(Required0) of {ok, Required} -> - UserInfo0 = maps:merge(Required, maps:with(?OPTIONAL_USER_INFO_KEYS, Body)), - UserInfo1 = emqx_utils_maps:safe_atom_key_map(UserInfo0), - UserInfo = maps:merge(#{is_superuser => false}, UserInfo1), - {ok, UserInfo}; + {ok, emqx_utils_maps:safe_atom_key_map(Required)}; Error -> + ?TRACE_AUTHN_PROVIDER("decode_keys_failed", #{http_body => Body}), Error end; _ -> - ?TRACE_AUTHN_PROVIDER("bad_response_body", #{http_body => Body}), + ?TRACE_AUTHN_PROVIDER("missing_requried_keys", #{http_body => Body}), {error, bad_response} end. -safely_parse_body(ContentType, Body) -> - try - parse_body(ContentType, Body) - catch - _Class:_Reason -> - {error, invalid_body} - end. - safely_convert_hex(Required) -> try {ok, @@ -165,15 +157,5 @@ safely_convert_hex(Required) -> {error, Reason} end. -parse_body(<<"application/json", _/binary>>, Body) -> - {ok, emqx_utils_json:decode(Body, [return_maps])}; -parse_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) -> - Flags = ?REQUIRED_USER_INFO_KEYS ++ ?OPTIONAL_USER_INFO_KEYS, - RawMap = maps:from_list(cow_qs:parse_qs(Body)), - NBody = maps:with(Flags, RawMap), - {ok, NBody}; -parse_body(ContentType, _) -> - {error, {unsupported_content_type, ContentType}}. - merge_scram_conf(Conf, State) -> maps:merge(maps:with([algorithm, iteration_count], Conf), State). diff --git a/apps/emqx_auth_http/src/emqx_authn_scram_http_schema.erl b/apps/emqx_auth_http/src/emqx_authn_scram_restapi_schema.erl similarity index 88% rename from apps/emqx_auth_http/src/emqx_authn_scram_http_schema.erl rename to apps/emqx_auth_http/src/emqx_authn_scram_restapi_schema.erl index ca43fe3a6..bf3398abb 100644 --- a/apps/emqx_auth_http/src/emqx_authn_scram_http_schema.erl +++ b/apps/emqx_auth_http/src/emqx_authn_scram_restapi_schema.erl @@ -2,7 +2,7 @@ %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- --module(emqx_authn_scram_http_schema). +-module(emqx_authn_scram_restapi_schema). -behaviour(emqx_authn_schema). @@ -22,16 +22,16 @@ namespace() -> "authn". refs() -> - [?R_REF(scram_http_get), ?R_REF(scram_http_post)]. + [?R_REF(scram_restapi_get), ?R_REF(scram_restapi_post)]. select_union_member( #{<<"mechanism">> := ?AUTHN_MECHANISM_SCRAM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIN} = Value ) -> case maps:get(<<"method">>, Value, undefined) of <<"get">> -> - [?R_REF(scram_http_get)]; + [?R_REF(scram_restapi_get)]; <<"post">> -> - [?R_REF(scramm_http_post)]; + [?R_REF(scram_restapi_post)]; Else -> throw(#{ reason => "unknown_http_method", @@ -43,20 +43,20 @@ select_union_member( select_union_member(_Value) -> undefined. -fields(scram_http_get) -> +fields(scram_restapi_get) -> [ {method, #{type => get, required => true, desc => ?DESC(emqx_authn_http_schema, method)}}, {headers, fun emqx_authn_http_schema:headers_no_content_type/1} ] ++ common_fields(); -fields(scram_http_post) -> +fields(scram_restapi_post) -> [ {method, #{type => post, required => true, desc => ?DESC(emqx_authn_http_schema, method)}}, {headers, fun emqx_authn_http_schema:headers/1} ] ++ common_fields(). -desc(scram_http_get) -> +desc(scram_restapi_get) -> ?DESC(emqx_authn_http_schema, get); -desc(scram_http_post) -> +desc(scram_restapi_post) -> ?DESC(emqx_authn_http_schema, post); desc(_) -> undefined. diff --git a/apps/emqx_auth_http/test/emqx_authn_scram_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_scram_restapi_SUITE.erl similarity index 81% rename from apps/emqx_auth_http/test/emqx_authn_scram_http_SUITE.erl rename to apps/emqx_auth_http/test/emqx_authn_scram_restapi_SUITE.erl index b00212cb1..7963cf1e3 100644 --- a/apps/emqx_auth_http/test/emqx_authn_scram_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_scram_restapi_SUITE.erl @@ -2,7 +2,7 @@ %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- --module(emqx_authn_scram_http_SUITE). +-module(emqx_authn_scram_restapi_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -21,6 +21,9 @@ -define(ALGORITHM_STR, <<"sha512">>). -define(ITERATION_COUNT, 4096). +-define(T_ACL_USERNAME, <<"username">>). +-define(T_ACL_PASSWORD, <<"password">>). + -include_lib("emqx/include/emqx_placeholder.hrl"). all() -> @@ -54,11 +57,11 @@ init_per_testcase(_Case, Config) -> [authentication], ?GLOBAL ), - {ok, _} = emqx_authn_scram_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH), + {ok, _} = emqx_authn_scram_restapi_test_server:start_link(?HTTP_PORT, ?HTTP_PATH), Config. end_per_testcase(_Case, _Config) -> - ok = emqx_authn_scram_http_test_server:stop(). + ok = emqx_authn_scram_restapi_test_server:stop(). %%------------------------------------------------------------------------------ %% Tests @@ -72,7 +75,9 @@ t_create(_Config) -> {create_authenticator, ?GLOBAL, AuthConfig} ), - {ok, [#{provider := emqx_authn_scram_http}]} = emqx_authn_chains:list_authenticators(?GLOBAL). + {ok, [#{provider := emqx_authn_scram_restapi}]} = emqx_authn_chains:list_authenticators( + ?GLOBAL + ). t_create_invalid(_Config) -> AuthConfig = raw_config(), @@ -118,59 +123,8 @@ t_authenticate(_Config) -> ok = emqx_config:put([mqtt, idle_timeout], 500), - {ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883), - - ClientFirstMessage = esasl_scram:client_first_message(Username), - - ConnectPacket = ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - properties = #{ - 'Authentication-Method' => <<"SCRAM-SHA-512">>, - 'Authentication-Data' => ClientFirstMessage - } - } - ), - - ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), - - %% Intentional sleep to trigger idle timeout for the connection not yet authenticated - ok = ct:sleep(1000), - - ?AUTH_PACKET( - ?RC_CONTINUE_AUTHENTICATION, - #{'Authentication-Data' := ServerFirstMessage} - ) = receive_packet(), - - {continue, ClientFinalMessage, ClientCache} = - esasl_scram:check_server_first_message( - ServerFirstMessage, - #{ - client_first_message => ClientFirstMessage, - password => Password, - algorithm => ?ALGORITHM - } - ), - - AuthContinuePacket = ?AUTH_PACKET( - ?RC_CONTINUE_AUTHENTICATION, - #{ - 'Authentication-Method' => <<"SCRAM-SHA-512">>, - 'Authentication-Data' => ClientFinalMessage - } - ), - - ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket), - - ?CONNACK_PACKET( - ?RC_SUCCESS, - _, - #{'Authentication-Data' := ServerFinalMessage} - ) = receive_packet(), - - ok = esasl_scram:check_server_final_message( - ServerFinalMessage, ClientCache#{algorithm => ?ALGORITHM} - ). + {ok, Pid} = create_connection(Username, Password), + emqx_authn_mqtt_test_client:stop(Pid). t_authenticate_bad_props(_Config) -> Username = <<"u">>, @@ -314,6 +268,47 @@ t_destroy(_Config) -> _ ) = receive_packet(). +t_acl(_Config) -> + init_auth(), + + ACL = emqx_authn_http_SUITE:acl_rules(), + set_user_handler(?T_ACL_USERNAME, ?T_ACL_PASSWORD, #{acl => ACL}), + {ok, Pid} = create_connection(?T_ACL_USERNAME, ?T_ACL_PASSWORD), + + Cases = [ + {allow, <<"http-authn-acl/#">>}, + {deny, <<"http-authn-acl/1">>}, + {deny, <<"t/#">>} + ], + + try + lists:foreach( + fun(Case) -> + test_acl(Case, Pid) + end, + Cases + ) + after + ok = emqx_authn_mqtt_test_client:stop(Pid) + end. + +t_auth_expire(_Config) -> + init_auth(), + + ExpireSec = 3, + WaitTime = timer:seconds(ExpireSec + 1), + ACL = emqx_authn_http_SUITE:acl_rules(), + + set_user_handler(?T_ACL_USERNAME, ?T_ACL_PASSWORD, #{ + acl => ACL, + expire_at => + erlang:system_time(second) + ExpireSec + }), + {ok, Pid} = create_connection(?T_ACL_USERNAME, ?T_ACL_PASSWORD), + + timer:sleep(WaitTime), + ?assertEqual(false, erlang:is_process_alive(Pid)). + t_is_superuser() -> State = init_auth(), ok = test_is_superuser(State, false), @@ -324,12 +319,12 @@ test_is_superuser(State, ExpectedIsSuperuser) -> Username = <<"u">>, Password = <<"p">>, - set_user_handler(Username, Password, ExpectedIsSuperuser), + set_user_handler(Username, Password, #{is_superuser => ExpectedIsSuperuser}), ClientFirstMessage = esasl_scram:client_first_message(Username), {continue, ServerFirstMessage, ServerCache} = - emqx_authn_scram_http:authenticate( + emqx_authn_scram_restapi:authenticate( #{ auth_method => <<"SCRAM-SHA-512">>, auth_data => ClientFirstMessage, @@ -349,7 +344,7 @@ test_is_superuser(State, ExpectedIsSuperuser) -> ), {ok, UserInfo1, ServerFinalMessage} = - emqx_authn_scram_http:authenticate( + emqx_authn_scram_restapi:authenticate( #{ auth_method => <<"SCRAM-SHA-512">>, auth_data => ClientFinalMessage, @@ -382,24 +377,25 @@ raw_config() -> }. set_user_handler(Username, Password) -> - set_user_handler(Username, Password, false). -set_user_handler(Username, Password, IsSuperuser) -> + set_user_handler(Username, Password, #{is_superuser => false}). +set_user_handler(Username, Password, Extra0) -> %% HTTP Server Handler = fun(Req0, State) -> #{ username := Username } = cowboy_req:match_qs([username], Req0), - UserInfo = make_user_info(Password, ?ALGORITHM, ?ITERATION_COUNT, IsSuperuser), + UserInfo = make_user_info(Password, ?ALGORITHM, ?ITERATION_COUNT), + Extra = maps:merge(#{is_superuser => false}, Extra0), Req = cowboy_req:reply( 200, #{<<"content-type">> => <<"application/json">>}, - emqx_utils_json:encode(UserInfo), + emqx_utils_json:encode(maps:merge(Extra, UserInfo)), Req0 ), {ok, Req, State} end, - ok = emqx_authn_scram_http_test_server:set_handler(Handler). + ok = emqx_authn_scram_restapi_test_server:set_handler(Handler). init_auth() -> init_auth(raw_config()). @@ -413,7 +409,7 @@ init_auth(Config) -> {ok, [#{state := State}]} = emqx_authn_chains:list_authenticators(?GLOBAL), State. -make_user_info(Password, Algorithm, IterationCount, IsSuperuser) -> +make_user_info(Password, Algorithm, IterationCount) -> {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info( Password, #{ @@ -424,8 +420,7 @@ make_user_info(Password, Algorithm, IterationCount, IsSuperuser) -> #{ stored_key => binary:encode_hex(StoredKey), server_key => binary:encode_hex(ServerKey), - salt => binary:encode_hex(Salt), - is_superuser => IsSuperuser + salt => binary:encode_hex(Salt) }. receive_packet() -> @@ -436,3 +431,79 @@ receive_packet() -> after 1000 -> ct:fail("Deliver timeout") end. + +create_connection(Username, Password) -> + {ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883), + + ClientFirstMessage = esasl_scram:client_first_message(Username), + + ConnectPacket = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5, + properties = #{ + 'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFirstMessage + } + } + ), + + ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), + + %% Intentional sleep to trigger idle timeout for the connection not yet authenticated + ok = ct:sleep(1000), + + ?AUTH_PACKET( + ?RC_CONTINUE_AUTHENTICATION, + #{'Authentication-Data' := ServerFirstMessage} + ) = receive_packet(), + + {continue, ClientFinalMessage, ClientCache} = + esasl_scram:check_server_first_message( + ServerFirstMessage, + #{ + client_first_message => ClientFirstMessage, + password => Password, + algorithm => ?ALGORITHM + } + ), + + AuthContinuePacket = ?AUTH_PACKET( + ?RC_CONTINUE_AUTHENTICATION, + #{ + 'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFinalMessage + } + ), + + ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket), + + ?CONNACK_PACKET( + ?RC_SUCCESS, + _, + #{'Authentication-Data' := ServerFinalMessage} + ) = receive_packet(), + + ok = esasl_scram:check_server_final_message( + ServerFinalMessage, ClientCache#{algorithm => ?ALGORITHM} + ), + {ok, Pid}. + +test_acl({allow, Topic}, C) -> + ?assertMatch( + [0], + send_subscribe(C, Topic) + ); +test_acl({deny, Topic}, C) -> + ?assertMatch( + [?RC_NOT_AUTHORIZED], + send_subscribe(C, Topic) + ). + +send_subscribe(Client, Topic) -> + TopicOpts = #{nl => 0, rap => 0, rh => 0, qos => 0}, + Packet = ?SUBSCRIBE_PACKET(1, [{Topic, TopicOpts}]), + emqx_authn_mqtt_test_client:send(Client, Packet), + timer:sleep(200), + + ?SUBACK_PACKET(1, ReasonCode) = receive_packet(), + ReasonCode. diff --git a/apps/emqx_auth_http/test/emqx_authn_scram_http_test_server.erl b/apps/emqx_auth_http/test/emqx_authn_scram_restapi_test_server.erl similarity index 98% rename from apps/emqx_auth_http/test/emqx_authn_scram_http_test_server.erl rename to apps/emqx_auth_http/test/emqx_authn_scram_restapi_test_server.erl index 5467df621..1e1432e0b 100644 --- a/apps/emqx_auth_http/test/emqx_authn_scram_http_test_server.erl +++ b/apps/emqx_auth_http/test/emqx_authn_scram_restapi_test_server.erl @@ -2,7 +2,7 @@ %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- --module(emqx_authn_scram_http_test_server). +-module(emqx_authn_scram_restapi_test_server). -behaviour(supervisor). -behaviour(cowboy_handler). diff --git a/apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia.erl b/apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia.erl index d59afea28..9880b71ee 100644 --- a/apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia.erl +++ b/apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia.erl @@ -141,7 +141,9 @@ authenticate( reason => Reason }) end, - emqx_utils_scram:authenticate(AuthMethod, AuthData, AuthCache, RetrieveFun, OnErrFun, State); + emqx_utils_scram:authenticate( + AuthMethod, AuthData, AuthCache, State, RetrieveFun, OnErrFun, [is_superuser] + ); authenticate(_Credential, _State) -> ignore. diff --git a/apps/emqx_conf/src/emqx_conf_schema_inject.erl b/apps/emqx_conf/src/emqx_conf_schema_inject.erl index 5c155bbf5..d94657325 100644 --- a/apps/emqx_conf/src/emqx_conf_schema_inject.erl +++ b/apps/emqx_conf/src/emqx_conf_schema_inject.erl @@ -51,7 +51,7 @@ authn_mods(ee) -> authn_mods(ce) ++ [ emqx_gcp_device_authn_schema, - emqx_authn_scram_http_schema + emqx_authn_scram_restapi_schema ]. authz() -> diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl index 0707c12aa..c79bc8e61 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl @@ -383,7 +383,7 @@ schema_authn() -> emqx_dashboard_swagger:schema_with_examples( emqx_authn_schema:authenticator_type_without([ emqx_authn_scram_mnesia_schema, - emqx_authn_scram_http_schema + emqx_authn_scram_restapi_schema ]), emqx_authn_api:authenticator_examples() ). diff --git a/apps/emqx_utils/src/emqx_utils_scram.erl b/apps/emqx_utils/src/emqx_utils_scram.erl index 9d0543703..cb11082fb 100644 --- a/apps/emqx_utils/src/emqx_utils_scram.erl +++ b/apps/emqx_utils/src/emqx_utils_scram.erl @@ -16,17 +16,17 @@ -module(emqx_utils_scram). --export([authenticate/6]). +-export([authenticate/7]). %%------------------------------------------------------------------------------ %% Authentication %%------------------------------------------------------------------------------ -authenticate(AuthMethod, AuthData, AuthCache, RetrieveFun, OnErrFun, Conf) -> +authenticate(AuthMethod, AuthData, AuthCache, Conf, RetrieveFun, OnErrFun, ResultKeys) -> case ensure_auth_method(AuthMethod, AuthData, Conf) of true -> case AuthCache of #{next_step := client_final} -> - check_client_final_message(AuthData, AuthCache, Conf, OnErrFun); + check_client_final_message(AuthData, AuthCache, Conf, OnErrFun, ResultKeys); _ -> check_client_first_message(AuthData, AuthCache, Conf, RetrieveFun, OnErrFun) end; @@ -64,9 +64,7 @@ check_client_first_message( {error, not_authorized} end. -check_client_final_message( - Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}, OnErrFun -) -> +check_client_final_message(Bin, Cache, #{algorithm := Alg}, OnErrFun, ResultKeys) -> case esasl_scram:check_client_final_message( Bin, @@ -74,7 +72,7 @@ check_client_final_message( ) of {ok, ServerFinalMessage} -> - {ok, #{is_superuser => IsSuperuser}, ServerFinalMessage}; + {ok, maps:with(ResultKeys, Cache), ServerFinalMessage}; {error, Reason} -> OnErrFun("check_client_final_message_error", Reason), {error, not_authorized} diff --git a/changes/ee/feat-13504.en.md b/changes/ee/feat-13504.en.md index c9b22f403..acea1241a 100644 --- a/changes/ee/feat-13504.en.md +++ b/changes/ee/feat-13504.en.md @@ -1 +1,5 @@ Added a HTTP backend for the authentication mechanism `scram`. + +Note: This is not an implementation of the RFC 7804: Salted Challenge Response HTTP Authentication Mechanism. + +This backend is an implementation of scram that uses an external web resource as a source of SCRAM authentication data, including stored key of the client, server key, and the salt. It support other authentication and authorization extension fields like HTTP auth backend, namely: `is_superuser`, `client_attrs`, `expire_at` and `acl`.