feat(authn): added a HTTP backend for the authentication mechanism scram
This commit is contained in:
parent
9e4a84cf76
commit
878b218692
|
@ -122,14 +122,6 @@ t_union_member_selector(_) ->
|
||||||
},
|
},
|
||||||
check(BadMechanism)
|
check(BadMechanism)
|
||||||
),
|
),
|
||||||
BadCombination = Base#{<<"mechanism">> => <<"scram">>, <<"backend">> => <<"http">>},
|
|
||||||
?assertThrow(
|
|
||||||
#{
|
|
||||||
reason := "unknown_mechanism",
|
|
||||||
expected := "password_based"
|
|
||||||
},
|
|
||||||
check(BadCombination)
|
|
||||||
),
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_http_auth_selector(_) ->
|
t_http_auth_selector(_) ->
|
||||||
|
|
|
@ -22,8 +22,13 @@
|
||||||
|
|
||||||
-define(AUTHN_MECHANISM, password_based).
|
-define(AUTHN_MECHANISM, password_based).
|
||||||
-define(AUTHN_MECHANISM_BIN, <<"password_based">>).
|
-define(AUTHN_MECHANISM_BIN, <<"password_based">>).
|
||||||
|
|
||||||
|
-define(AUTHN_MECHANISM_SCRAM, scram).
|
||||||
|
-define(AUTHN_MECHANISM_SCRAM_BIN, <<"scram">>).
|
||||||
|
|
||||||
-define(AUTHN_BACKEND, http).
|
-define(AUTHN_BACKEND, http).
|
||||||
-define(AUTHN_BACKEND_BIN, <<"http">>).
|
-define(AUTHN_BACKEND_BIN, <<"http">>).
|
||||||
-define(AUTHN_TYPE, {?AUTHN_MECHANISM, ?AUTHN_BACKEND}).
|
-define(AUTHN_TYPE, {?AUTHN_MECHANISM, ?AUTHN_BACKEND}).
|
||||||
|
-define(AUTHN_TYPE_SCRAM, {?AUTHN_MECHANISM_SCRAM, ?AUTHN_BACKEND}).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
|
@ -25,10 +25,12 @@
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
ok = emqx_authz:register_source(?AUTHZ_TYPE, emqx_authz_http),
|
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, emqx_authn_http),
|
||||||
|
ok = emqx_authn:register_provider(?AUTHN_TYPE_SCRAM, emqx_authn_scram_http),
|
||||||
{ok, Sup} = emqx_auth_http_sup:start_link(),
|
{ok, Sup} = emqx_auth_http_sup:start_link(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok = emqx_authn:deregister_provider(?AUTHN_TYPE),
|
ok = emqx_authn:deregister_provider(?AUTHN_TYPE),
|
||||||
|
ok = emqx_authn:deregister_provider(?AUTHN_TYPE_SCRAM),
|
||||||
ok = emqx_authz:unregister_source(?AUTHZ_TYPE),
|
ok = emqx_authz:unregister_source(?AUTHZ_TYPE),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -28,6 +28,13 @@
|
||||||
destroy/1
|
destroy/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
with_validated_config/2,
|
||||||
|
generate_request/2,
|
||||||
|
request_for_log/2,
|
||||||
|
response_for_log/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
namespace/0
|
namespace/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([url/1, headers/1, headers_no_content_type/1, request_timeout/1]).
|
||||||
|
|
||||||
-include("emqx_auth_http.hrl").
|
-include("emqx_auth_http.hrl").
|
||||||
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
@ -61,12 +63,6 @@ select_union_member(
|
||||||
got => Else
|
got => Else
|
||||||
})
|
})
|
||||||
end;
|
end;
|
||||||
select_union_member(#{<<"backend">> := ?AUTHN_BACKEND_BIN}) ->
|
|
||||||
throw(#{
|
|
||||||
reason => "unknown_mechanism",
|
|
||||||
expected => "password_based",
|
|
||||||
got => undefined
|
|
||||||
});
|
|
||||||
select_union_member(_Value) ->
|
select_union_member(_Value) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authn_scram_http).
|
||||||
|
|
||||||
|
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-behaviour(emqx_authn_provider).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
create/2,
|
||||||
|
update/2,
|
||||||
|
authenticate/2,
|
||||||
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(REQUIRED_USER_INFO_KEYS, [
|
||||||
|
<<"stored_key">>,
|
||||||
|
<<"server_key">>,
|
||||||
|
<<"salt">>
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(OPTIONAL_USER_INFO_KEYS, [
|
||||||
|
<<"is_superuser">>
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create(_AuthenticatorID, Config) ->
|
||||||
|
create(Config).
|
||||||
|
|
||||||
|
create(Config0) ->
|
||||||
|
emqx_authn_http:with_validated_config(Config0, fun(Config, State) ->
|
||||||
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
|
% {Config, State} = parse_config(Config0),
|
||||||
|
{ok, _Data} = emqx_authn_utils:create_resource(
|
||||||
|
ResourceId,
|
||||||
|
emqx_bridge_http_connector,
|
||||||
|
Config
|
||||||
|
),
|
||||||
|
{ok, merge_scram_conf(Config, State#{resource_id => ResourceId})}
|
||||||
|
end).
|
||||||
|
|
||||||
|
update(Config0, #{resource_id := ResourceId} = _State) ->
|
||||||
|
emqx_authn_http:with_validated_config(Config0, fun(Config, NState) ->
|
||||||
|
% {Config, NState} = parse_config(Config0),
|
||||||
|
case emqx_authn_utils:update_resource(emqx_bridge_http_connector, Config, ResourceId) of
|
||||||
|
{error, Reason} ->
|
||||||
|
error({load_config_error, Reason});
|
||||||
|
{ok, _} ->
|
||||||
|
{ok, merge_scram_conf(Config, NState#{resource_id => ResourceId})}
|
||||||
|
end
|
||||||
|
end).
|
||||||
|
|
||||||
|
authenticate(
|
||||||
|
#{
|
||||||
|
auth_method := AuthMethod,
|
||||||
|
auth_data := AuthData,
|
||||||
|
auth_cache := AuthCache
|
||||||
|
} = Credential,
|
||||||
|
State
|
||||||
|
) ->
|
||||||
|
RetrieveFun = fun(Username) ->
|
||||||
|
retrieve(Username, Credential, State)
|
||||||
|
end,
|
||||||
|
OnErrFun = fun(Msg, Reason) ->
|
||||||
|
?TRACE_AUTHN_PROVIDER(Msg, #{
|
||||||
|
reason => Reason
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
emqx_utils_scram:authenticate(AuthMethod, AuthData, AuthCache, RetrieveFun, OnErrFun, State);
|
||||||
|
authenticate(_Credential, _State) ->
|
||||||
|
ignore.
|
||||||
|
|
||||||
|
destroy(#{resource_id := ResourceId}) ->
|
||||||
|
_ = emqx_resource:remove_local(ResourceId),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
retrieve(
|
||||||
|
Username,
|
||||||
|
Credential,
|
||||||
|
#{
|
||||||
|
resource_id := ResourceId,
|
||||||
|
method := Method,
|
||||||
|
request_timeout := RequestTimeout
|
||||||
|
} = State
|
||||||
|
) ->
|
||||||
|
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", #{
|
||||||
|
request => emqx_authn_http:request_for_log(Credential, State),
|
||||||
|
response => emqx_authn_http:response_for_log(Response),
|
||||||
|
resource => ResourceId
|
||||||
|
}),
|
||||||
|
case Response of
|
||||||
|
{ok, 200, Headers, Body} ->
|
||||||
|
handle_response(Headers, Body);
|
||||||
|
{ok, _StatusCode, _Headers} ->
|
||||||
|
{error, bad_response};
|
||||||
|
{ok, _StatusCode, _Headers, _Body} ->
|
||||||
|
{error, bad_response};
|
||||||
|
{error, _Reason} = Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
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
|
||||||
|
end.
|
||||||
|
|
||||||
|
body_to_user_info(Body) ->
|
||||||
|
Required0 = maps:with(?REQUIRED_USER_INFO_KEYS, Body),
|
||||||
|
case maps:size(Required0) =:= erlang:length(?REQUIRED_USER_INFO_KEYS) of
|
||||||
|
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};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
?TRACE_AUTHN_PROVIDER("bad_response_body", #{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,
|
||||||
|
maps:map(
|
||||||
|
fun(_Key, Hex) ->
|
||||||
|
binary:decode_hex(Hex)
|
||||||
|
end,
|
||||||
|
Required
|
||||||
|
)}
|
||||||
|
catch
|
||||||
|
_Class:Reason ->
|
||||||
|
{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).
|
|
@ -0,0 +1,81 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authn_scram_http_schema).
|
||||||
|
|
||||||
|
-behaviour(emqx_authn_schema).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
fields/1,
|
||||||
|
validations/0,
|
||||||
|
desc/1,
|
||||||
|
refs/0,
|
||||||
|
select_union_member/1,
|
||||||
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include("emqx_auth_http.hrl").
|
||||||
|
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
|
namespace() -> "authn".
|
||||||
|
|
||||||
|
refs() ->
|
||||||
|
[?R_REF(scram_http_get), ?R_REF(scram_http_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)];
|
||||||
|
<<"post">> ->
|
||||||
|
[?R_REF(scramm_http_post)];
|
||||||
|
Else ->
|
||||||
|
throw(#{
|
||||||
|
reason => "unknown_http_method",
|
||||||
|
expected => "get | post",
|
||||||
|
field_name => method,
|
||||||
|
got => Else
|
||||||
|
})
|
||||||
|
end;
|
||||||
|
select_union_member(_Value) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
fields(scram_http_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) ->
|
||||||
|
[
|
||||||
|
{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(emqx_authn_http_schema, get);
|
||||||
|
desc(scram_http_post) ->
|
||||||
|
?DESC(emqx_authn_http_schema, post);
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
validations() ->
|
||||||
|
emqx_authn_http_schema:validations().
|
||||||
|
|
||||||
|
common_fields() ->
|
||||||
|
emqx_authn_schema:common_fields() ++
|
||||||
|
[
|
||||||
|
{mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM_SCRAM)},
|
||||||
|
{backend, emqx_authn_schema:backend(?AUTHN_BACKEND)},
|
||||||
|
{algorithm, fun emqx_authn_scram_mnesia_schema:algorithm/1},
|
||||||
|
{iteration_count, fun emqx_authn_scram_mnesia_schema:iteration_count/1},
|
||||||
|
{url, fun emqx_authn_http_schema:url/1},
|
||||||
|
{body,
|
||||||
|
hoconsc:mk(typerefl:alias("map", map([{fuzzy, term(), binary()}])), #{
|
||||||
|
required => false, desc => ?DESC(emqx_authn_http_schema, body)
|
||||||
|
})},
|
||||||
|
{request_timeout, fun emqx_authn_http_schema:request_timeout/1}
|
||||||
|
] ++
|
||||||
|
proplists:delete(pool_type, emqx_bridge_http_connector:fields(config)).
|
|
@ -0,0 +1,438 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authn_scram_http_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
||||||
|
|
||||||
|
-define(PATH, [authentication]).
|
||||||
|
|
||||||
|
-define(HTTP_PORT, 34333).
|
||||||
|
-define(HTTP_PATH, "/user/[...]").
|
||||||
|
-define(ALGORITHM, sha512).
|
||||||
|
-define(ALGORITHM_STR, <<"sha512">>).
|
||||||
|
-define(ITERATION_COUNT, 4096).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
case emqx_release:edition() of
|
||||||
|
ce ->
|
||||||
|
[];
|
||||||
|
_ ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE)
|
||||||
|
end.
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
Apps = emqx_cth_suite:start([cowboy, emqx, emqx_conf, emqx_auth, emqx_auth_http], #{
|
||||||
|
work_dir => ?config(priv_dir, Config)
|
||||||
|
}),
|
||||||
|
|
||||||
|
IdleTimeout = emqx_config:get([mqtt, idle_timeout]),
|
||||||
|
[{apps, Apps}, {idle_timeout, IdleTimeout} | Config].
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
ok = emqx_config:put([mqtt, idle_timeout], ?config(idle_timeout, Config)),
|
||||||
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
|
[authentication],
|
||||||
|
?GLOBAL
|
||||||
|
),
|
||||||
|
ok = emqx_cth_suite:stop(?config(apps, Config)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_per_testcase(_Case, Config) ->
|
||||||
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
|
[authentication],
|
||||||
|
?GLOBAL
|
||||||
|
),
|
||||||
|
{ok, _} = emqx_authn_scram_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_testcase(_Case, _Config) ->
|
||||||
|
ok = emqx_authn_scram_http_test_server:stop().
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Tests
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_create(_Config) ->
|
||||||
|
AuthConfig = raw_config(),
|
||||||
|
|
||||||
|
{ok, _} = emqx:update_config(
|
||||||
|
?PATH,
|
||||||
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
|
{ok, [#{provider := emqx_authn_scram_http}]} = emqx_authn_chains:list_authenticators(?GLOBAL).
|
||||||
|
|
||||||
|
t_create_invalid(_Config) ->
|
||||||
|
AuthConfig = raw_config(),
|
||||||
|
|
||||||
|
InvalidConfigs =
|
||||||
|
[
|
||||||
|
AuthConfig#{<<"headers">> => []},
|
||||||
|
AuthConfig#{<<"method">> => <<"delete">>},
|
||||||
|
AuthConfig#{<<"url">> => <<"localhost">>},
|
||||||
|
AuthConfig#{<<"url">> => <<"http://foo.com/xxx#fragment">>},
|
||||||
|
AuthConfig#{<<"url">> => <<"http://${foo}.com/xxx">>},
|
||||||
|
AuthConfig#{<<"url">> => <<"//foo.com/xxx">>},
|
||||||
|
AuthConfig#{<<"algorithm">> => <<"sha128">>}
|
||||||
|
],
|
||||||
|
|
||||||
|
lists:foreach(
|
||||||
|
fun(Config) ->
|
||||||
|
ct:pal("creating authenticator with invalid config: ~p", [Config]),
|
||||||
|
{error, _} =
|
||||||
|
try
|
||||||
|
emqx:update_config(
|
||||||
|
?PATH,
|
||||||
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
)
|
||||||
|
catch
|
||||||
|
throw:Error ->
|
||||||
|
{error, Error}
|
||||||
|
end,
|
||||||
|
?assertEqual(
|
||||||
|
{error, {not_found, {chain, ?GLOBAL}}},
|
||||||
|
emqx_authn_chains:list_authenticators(?GLOBAL)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
InvalidConfigs
|
||||||
|
).
|
||||||
|
|
||||||
|
t_authenticate(_Config) ->
|
||||||
|
Username = <<"u">>,
|
||||||
|
Password = <<"p">>,
|
||||||
|
|
||||||
|
set_user_handler(Username, Password),
|
||||||
|
init_auth(),
|
||||||
|
|
||||||
|
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}
|
||||||
|
).
|
||||||
|
|
||||||
|
t_authenticate_bad_props(_Config) ->
|
||||||
|
Username = <<"u">>,
|
||||||
|
Password = <<"p">>,
|
||||||
|
|
||||||
|
set_user_handler(Username, Password),
|
||||||
|
init_auth(),
|
||||||
|
|
||||||
|
{ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883),
|
||||||
|
|
||||||
|
ConnectPacket = ?CONNECT_PACKET(
|
||||||
|
#mqtt_packet_connect{
|
||||||
|
proto_ver = ?MQTT_PROTO_V5,
|
||||||
|
properties = #{
|
||||||
|
'Authentication-Method' => <<"SCRAM-SHA-512">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||||
|
|
||||||
|
?CONNACK_PACKET(?RC_NOT_AUTHORIZED) = receive_packet().
|
||||||
|
|
||||||
|
t_authenticate_bad_username(_Config) ->
|
||||||
|
Username = <<"u">>,
|
||||||
|
Password = <<"p">>,
|
||||||
|
|
||||||
|
set_user_handler(Username, Password),
|
||||||
|
init_auth(),
|
||||||
|
|
||||||
|
{ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883),
|
||||||
|
|
||||||
|
ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>),
|
||||||
|
|
||||||
|
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),
|
||||||
|
|
||||||
|
?CONNACK_PACKET(?RC_NOT_AUTHORIZED) = receive_packet().
|
||||||
|
|
||||||
|
t_authenticate_bad_password(_Config) ->
|
||||||
|
Username = <<"u">>,
|
||||||
|
Password = <<"p">>,
|
||||||
|
|
||||||
|
set_user_handler(Username, Password),
|
||||||
|
init_auth(),
|
||||||
|
|
||||||
|
{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),
|
||||||
|
|
||||||
|
?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 => <<"badpassword">>,
|
||||||
|
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_NOT_AUTHORIZED) = receive_packet().
|
||||||
|
|
||||||
|
t_destroy(_Config) ->
|
||||||
|
Username = <<"u">>,
|
||||||
|
Password = <<"p">>,
|
||||||
|
|
||||||
|
set_user_handler(Username, Password),
|
||||||
|
init_auth(),
|
||||||
|
|
||||||
|
ok = emqx_config:put([mqtt, idle_timeout], 500),
|
||||||
|
|
||||||
|
{ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883),
|
||||||
|
|
||||||
|
ConnectPacket = ?CONNECT_PACKET(
|
||||||
|
#mqtt_packet_connect{
|
||||||
|
proto_ver = ?MQTT_PROTO_V5,
|
||||||
|
properties = #{
|
||||||
|
'Authentication-Method' => <<"SCRAM-SHA-512">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||||
|
|
||||||
|
ok = ct:sleep(1000),
|
||||||
|
|
||||||
|
?CONNACK_PACKET(?RC_NOT_AUTHORIZED) = receive_packet(),
|
||||||
|
|
||||||
|
%% emqx_authn_mqtt_test_client:stop(Pid),
|
||||||
|
|
||||||
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
|
[authentication],
|
||||||
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
|
{ok, Pid2} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883),
|
||||||
|
|
||||||
|
ok = emqx_authn_mqtt_test_client:send(Pid2, ConnectPacket),
|
||||||
|
|
||||||
|
ok = ct:sleep(1000),
|
||||||
|
|
||||||
|
?CONNACK_PACKET(
|
||||||
|
?RC_SUCCESS,
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) = receive_packet().
|
||||||
|
|
||||||
|
t_is_superuser() ->
|
||||||
|
State = init_auth(),
|
||||||
|
ok = test_is_superuser(State, false),
|
||||||
|
ok = test_is_superuser(State, true),
|
||||||
|
ok = test_is_superuser(State, false).
|
||||||
|
|
||||||
|
test_is_superuser(State, ExpectedIsSuperuser) ->
|
||||||
|
Username = <<"u">>,
|
||||||
|
Password = <<"p">>,
|
||||||
|
|
||||||
|
set_user_handler(Username, Password, ExpectedIsSuperuser),
|
||||||
|
|
||||||
|
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
||||||
|
|
||||||
|
{continue, ServerFirstMessage, ServerCache} =
|
||||||
|
emqx_authn_scram_http:authenticate(
|
||||||
|
#{
|
||||||
|
auth_method => <<"SCRAM-SHA-512">>,
|
||||||
|
auth_data => ClientFirstMessage,
|
||||||
|
auth_cache => #{}
|
||||||
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
|
{continue, ClientFinalMessage, ClientCache} =
|
||||||
|
esasl_scram:check_server_first_message(
|
||||||
|
ServerFirstMessage,
|
||||||
|
#{
|
||||||
|
client_first_message => ClientFirstMessage,
|
||||||
|
password => Password,
|
||||||
|
algorithm => ?ALGORITHM
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
{ok, UserInfo1, ServerFinalMessage} =
|
||||||
|
emqx_authn_scram_http:authenticate(
|
||||||
|
#{
|
||||||
|
auth_method => <<"SCRAM-SHA-512">>,
|
||||||
|
auth_data => ClientFinalMessage,
|
||||||
|
auth_cache => ServerCache
|
||||||
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = esasl_scram:check_server_final_message(
|
||||||
|
ServerFinalMessage, ClientCache#{algorithm => ?ALGORITHM}
|
||||||
|
),
|
||||||
|
|
||||||
|
?assertMatch(#{is_superuser := ExpectedIsSuperuser}, UserInfo1).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
raw_config() ->
|
||||||
|
#{
|
||||||
|
<<"mechanism">> => <<"scram">>,
|
||||||
|
<<"backend">> => <<"http">>,
|
||||||
|
<<"enable">> => <<"true">>,
|
||||||
|
<<"method">> => <<"get">>,
|
||||||
|
<<"url">> => <<"http://127.0.0.1:34333/user">>,
|
||||||
|
<<"body">> => #{<<"username">> => ?PH_USERNAME},
|
||||||
|
<<"headers">> => #{<<"X-Test-Header">> => <<"Test Value">>},
|
||||||
|
<<"algorithm">> => ?ALGORITHM_STR,
|
||||||
|
<<"iteration_count">> => ?ITERATION_COUNT
|
||||||
|
}.
|
||||||
|
|
||||||
|
set_user_handler(Username, Password) ->
|
||||||
|
set_user_handler(Username, Password, false).
|
||||||
|
set_user_handler(Username, Password, IsSuperuser) ->
|
||||||
|
%% HTTP Server
|
||||||
|
Handler = fun(Req0, State) ->
|
||||||
|
#{
|
||||||
|
username := Username
|
||||||
|
} = cowboy_req:match_qs([username], Req0),
|
||||||
|
|
||||||
|
UserInfo = make_user_info(Password, ?ALGORITHM, ?ITERATION_COUNT, IsSuperuser),
|
||||||
|
Req = cowboy_req:reply(
|
||||||
|
200,
|
||||||
|
#{<<"content-type">> => <<"application/json">>},
|
||||||
|
emqx_utils_json:encode(UserInfo),
|
||||||
|
Req0
|
||||||
|
),
|
||||||
|
{ok, Req, State}
|
||||||
|
end,
|
||||||
|
ok = emqx_authn_scram_http_test_server:set_handler(Handler).
|
||||||
|
|
||||||
|
init_auth() ->
|
||||||
|
init_auth(raw_config()).
|
||||||
|
|
||||||
|
init_auth(Config) ->
|
||||||
|
{ok, _} = emqx:update_config(
|
||||||
|
?PATH,
|
||||||
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
|
{ok, [#{state := State}]} = emqx_authn_chains:list_authenticators(?GLOBAL),
|
||||||
|
State.
|
||||||
|
|
||||||
|
make_user_info(Password, Algorithm, IterationCount, IsSuperuser) ->
|
||||||
|
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(
|
||||||
|
Password,
|
||||||
|
#{
|
||||||
|
algorithm => Algorithm,
|
||||||
|
iteration_count => IterationCount
|
||||||
|
}
|
||||||
|
),
|
||||||
|
#{
|
||||||
|
stored_key => binary:encode_hex(StoredKey),
|
||||||
|
server_key => binary:encode_hex(ServerKey),
|
||||||
|
salt => binary:encode_hex(Salt),
|
||||||
|
is_superuser => IsSuperuser
|
||||||
|
}.
|
||||||
|
|
||||||
|
receive_packet() ->
|
||||||
|
receive
|
||||||
|
{packet, Packet} ->
|
||||||
|
ct:pal("Delivered packet: ~p", [Packet]),
|
||||||
|
Packet
|
||||||
|
after 1000 ->
|
||||||
|
ct:fail("Deliver timeout")
|
||||||
|
end.
|
|
@ -0,0 +1,115 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authn_scram_http_test_server).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
|
||||||
|
% cowboy_server callbacks
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
% supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
% API
|
||||||
|
-export([
|
||||||
|
start_link/2,
|
||||||
|
start_link/3,
|
||||||
|
stop/0,
|
||||||
|
set_handler/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link(Port, Path) ->
|
||||||
|
start_link(Port, Path, false).
|
||||||
|
|
||||||
|
start_link(Port, Path, SSLOpts) ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, Path, SSLOpts]).
|
||||||
|
|
||||||
|
stop() ->
|
||||||
|
gen_server:stop(?MODULE).
|
||||||
|
|
||||||
|
set_handler(F) when is_function(F, 2) ->
|
||||||
|
true = ets:insert(?MODULE, {handler, F}),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% supervisor API
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([Port, Path, SSLOpts]) ->
|
||||||
|
Dispatch = cowboy_router:compile(
|
||||||
|
[
|
||||||
|
{'_', [{Path, ?MODULE, []}]}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
|
ProtoOpts = #{env => #{dispatch => Dispatch}},
|
||||||
|
|
||||||
|
Tab = ets:new(?MODULE, [set, named_table, public]),
|
||||||
|
ets:insert(Tab, {handler, fun default_handler/2}),
|
||||||
|
|
||||||
|
{Transport, TransOpts, CowboyModule} = transport_settings(Port, SSLOpts),
|
||||||
|
|
||||||
|
ChildSpec = ranch:child_spec(?MODULE, Transport, TransOpts, CowboyModule, ProtoOpts),
|
||||||
|
|
||||||
|
{ok, {#{}, [ChildSpec]}}.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% cowboy_server API
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
init(Req, State) ->
|
||||||
|
[{handler, Handler}] = ets:lookup(?MODULE, handler),
|
||||||
|
Handler(Req, State).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
transport_settings(Port, false) ->
|
||||||
|
TransOpts = #{
|
||||||
|
socket_opts => [{port, Port}],
|
||||||
|
connection_type => supervisor
|
||||||
|
},
|
||||||
|
{ranch_tcp, TransOpts, cowboy_clear};
|
||||||
|
transport_settings(Port, SSLOpts) ->
|
||||||
|
TransOpts = #{
|
||||||
|
socket_opts => [
|
||||||
|
{port, Port},
|
||||||
|
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
|
||||||
|
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|
||||||
|
| SSLOpts
|
||||||
|
],
|
||||||
|
connection_type => supervisor
|
||||||
|
},
|
||||||
|
{ranch_ssl, TransOpts, cowboy_tls}.
|
||||||
|
|
||||||
|
default_handler(Req0, State) ->
|
||||||
|
Req = cowboy_req:reply(
|
||||||
|
400,
|
||||||
|
#{<<"content-type">> => <<"text/plain">>},
|
||||||
|
<<"">>,
|
||||||
|
Req0
|
||||||
|
),
|
||||||
|
{ok, Req, State}.
|
||||||
|
|
||||||
|
make_user_info(Password, Algorithm, IterationCount) ->
|
||||||
|
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(
|
||||||
|
Password,
|
||||||
|
#{
|
||||||
|
algorithm => Algorithm,
|
||||||
|
iteration_count => IterationCount
|
||||||
|
}
|
||||||
|
),
|
||||||
|
#{
|
||||||
|
stored_key => StoredKey,
|
||||||
|
server_key => ServerKey,
|
||||||
|
salt => Salt,
|
||||||
|
is_superuser => false
|
||||||
|
}.
|
|
@ -133,17 +133,15 @@ authenticate(
|
||||||
},
|
},
|
||||||
State
|
State
|
||||||
) ->
|
) ->
|
||||||
case ensure_auth_method(AuthMethod, AuthData, State) of
|
RetrieveFun = fun(Username) ->
|
||||||
true ->
|
retrieve(Username, State)
|
||||||
case AuthCache of
|
end,
|
||||||
#{next_step := client_final} ->
|
OnErrFun = fun(Msg, Reason) ->
|
||||||
check_client_final_message(AuthData, AuthCache, State);
|
?TRACE_AUTHN_PROVIDER(Msg, #{
|
||||||
_ ->
|
reason => Reason
|
||||||
check_client_first_message(AuthData, AuthCache, State)
|
})
|
||||||
end;
|
end,
|
||||||
false ->
|
emqx_utils_scram:authenticate(AuthMethod, AuthData, AuthCache, RetrieveFun, OnErrFun, State);
|
||||||
ignore
|
|
||||||
end;
|
|
||||||
authenticate(_Credential, _State) ->
|
authenticate(_Credential, _State) ->
|
||||||
ignore.
|
ignore.
|
||||||
|
|
||||||
|
@ -257,55 +255,6 @@ run_fuzzy_filter(
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
ensure_auth_method(_AuthMethod, undefined, _State) ->
|
|
||||||
false;
|
|
||||||
ensure_auth_method(<<"SCRAM-SHA-256">>, _AuthData, #{algorithm := sha256}) ->
|
|
||||||
true;
|
|
||||||
ensure_auth_method(<<"SCRAM-SHA-512">>, _AuthData, #{algorithm := sha512}) ->
|
|
||||||
true;
|
|
||||||
ensure_auth_method(_AuthMethod, _AuthData, _State) ->
|
|
||||||
false.
|
|
||||||
|
|
||||||
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
|
|
||||||
RetrieveFun = fun(Username) ->
|
|
||||||
retrieve(Username, State)
|
|
||||||
end,
|
|
||||||
case
|
|
||||||
esasl_scram:check_client_first_message(
|
|
||||||
Bin,
|
|
||||||
#{
|
|
||||||
iteration_count => IterationCount,
|
|
||||||
retrieve => RetrieveFun
|
|
||||||
}
|
|
||||||
)
|
|
||||||
of
|
|
||||||
{continue, ServerFirstMessage, Cache} ->
|
|
||||||
{continue, ServerFirstMessage, Cache};
|
|
||||||
ignore ->
|
|
||||||
ignore;
|
|
||||||
{error, Reason} ->
|
|
||||||
?TRACE_AUTHN_PROVIDER("check_client_first_message_error", #{
|
|
||||||
reason => Reason
|
|
||||||
}),
|
|
||||||
{error, not_authorized}
|
|
||||||
end.
|
|
||||||
|
|
||||||
check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) ->
|
|
||||||
case
|
|
||||||
esasl_scram:check_client_final_message(
|
|
||||||
Bin,
|
|
||||||
Cache#{algorithm => Alg}
|
|
||||||
)
|
|
||||||
of
|
|
||||||
{ok, ServerFinalMessage} ->
|
|
||||||
{ok, #{is_superuser => IsSuperuser}, ServerFinalMessage};
|
|
||||||
{error, Reason} ->
|
|
||||||
?TRACE_AUTHN_PROVIDER("check_client_final_message_error", #{
|
|
||||||
reason => Reason
|
|
||||||
}),
|
|
||||||
{error, not_authorized}
|
|
||||||
end.
|
|
||||||
|
|
||||||
user_info_record(
|
user_info_record(
|
||||||
#{
|
#{
|
||||||
user_id := UserID,
|
user_id := UserID,
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
select_union_member/1
|
select_union_member/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([algorithm/1, iteration_count/1]).
|
||||||
|
|
||||||
namespace() -> "authn".
|
namespace() -> "authn".
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
|
@ -38,11 +40,6 @@ select_union_member(#{
|
||||||
<<"mechanism">> := ?AUTHN_MECHANISM_SCRAM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIN
|
<<"mechanism">> := ?AUTHN_MECHANISM_SCRAM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIN
|
||||||
}) ->
|
}) ->
|
||||||
refs();
|
refs();
|
||||||
select_union_member(#{<<"mechanism">> := ?AUTHN_MECHANISM_SCRAM_BIN}) ->
|
|
||||||
throw(#{
|
|
||||||
reason => "unknown_backend",
|
|
||||||
expected => ?AUTHN_BACKEND
|
|
||||||
});
|
|
||||||
select_union_member(_) ->
|
select_union_member(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,10 @@ authn_mods(ce) ->
|
||||||
];
|
];
|
||||||
authn_mods(ee) ->
|
authn_mods(ee) ->
|
||||||
authn_mods(ce) ++
|
authn_mods(ce) ++
|
||||||
[emqx_gcp_device_authn_schema].
|
[
|
||||||
|
emqx_gcp_device_authn_schema,
|
||||||
|
emqx_authn_scram_http_schema
|
||||||
|
].
|
||||||
|
|
||||||
authz() ->
|
authz() ->
|
||||||
[{emqx_authz_schema, authz_mods()}].
|
[{emqx_authz_schema, authz_mods()}].
|
||||||
|
|
|
@ -381,6 +381,9 @@ params_fuzzy_in_qs() ->
|
||||||
|
|
||||||
schema_authn() ->
|
schema_authn() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
emqx_authn_schema:authenticator_type_without([emqx_authn_scram_mnesia_schema]),
|
emqx_authn_schema:authenticator_type_without([
|
||||||
|
emqx_authn_scram_mnesia_schema,
|
||||||
|
emqx_authn_scram_http_schema
|
||||||
|
]),
|
||||||
emqx_authn_api:authenticator_examples()
|
emqx_authn_api:authenticator_examples()
|
||||||
).
|
).
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021-2024 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_utils_scram).
|
||||||
|
|
||||||
|
-export([authenticate/6]).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Authentication
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
authenticate(AuthMethod, AuthData, AuthCache, RetrieveFun, OnErrFun, Conf) ->
|
||||||
|
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_first_message(AuthData, AuthCache, Conf, RetrieveFun, OnErrFun)
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
ignore
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_auth_method(_AuthMethod, undefined, _Conf) ->
|
||||||
|
false;
|
||||||
|
ensure_auth_method(<<"SCRAM-SHA-256">>, _AuthData, #{algorithm := sha256}) ->
|
||||||
|
true;
|
||||||
|
ensure_auth_method(<<"SCRAM-SHA-512">>, _AuthData, #{algorithm := sha512}) ->
|
||||||
|
true;
|
||||||
|
ensure_auth_method(_AuthMethod, _AuthData, _Conf) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
check_client_first_message(
|
||||||
|
Bin, _Cache, #{iteration_count := IterationCount}, RetrieveFun, OnErrFun
|
||||||
|
) ->
|
||||||
|
case
|
||||||
|
esasl_scram:check_client_first_message(
|
||||||
|
Bin,
|
||||||
|
#{
|
||||||
|
iteration_count => IterationCount,
|
||||||
|
retrieve => RetrieveFun
|
||||||
|
}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{continue, ServerFirstMessage, Cache} ->
|
||||||
|
{continue, ServerFirstMessage, Cache};
|
||||||
|
ignore ->
|
||||||
|
ignore;
|
||||||
|
{error, Reason} ->
|
||||||
|
OnErrFun("check_client_first_message_error", Reason),
|
||||||
|
{error, not_authorized}
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_client_final_message(
|
||||||
|
Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}, OnErrFun
|
||||||
|
) ->
|
||||||
|
case
|
||||||
|
esasl_scram:check_client_final_message(
|
||||||
|
Bin,
|
||||||
|
Cache#{algorithm => Alg}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, ServerFinalMessage} ->
|
||||||
|
{ok, #{is_superuser => IsSuperuser}, ServerFinalMessage};
|
||||||
|
{error, Reason} ->
|
||||||
|
OnErrFun("check_client_final_message_error", Reason),
|
||||||
|
{error, not_authorized}
|
||||||
|
end.
|
Loading…
Reference in New Issue