Merge pull request #6384 from savonarola/refactor-password-hashing
refactor(authn): unify password hashing
This commit is contained in:
commit
b8a68d7a9f
|
@ -250,7 +250,7 @@ set_peercert_infos(Peercert, ClientInfo, Zone) ->
|
||||||
dn -> DN;
|
dn -> DN;
|
||||||
crt -> Peercert;
|
crt -> Peercert;
|
||||||
pem when is_binary(Peercert) -> base64:encode(Peercert);
|
pem when is_binary(Peercert) -> base64:encode(Peercert);
|
||||||
md5 when is_binary(Peercert) -> emqx_passwd:hash(md5, Peercert);
|
md5 when is_binary(Peercert) -> emqx_passwd:hash_data(md5, Peercert);
|
||||||
_ -> undefined
|
_ -> undefined
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
|
@ -17,81 +17,120 @@
|
||||||
-module(emqx_passwd).
|
-module(emqx_passwd).
|
||||||
|
|
||||||
-export([ hash/2
|
-export([ hash/2
|
||||||
, check_pass/2
|
, hash_data/2
|
||||||
|
, check_pass/3
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export_type([ password/0
|
||||||
|
, password_hash/0
|
||||||
|
, hash_type_simple/0
|
||||||
|
, hash_type/0
|
||||||
|
, salt_position/0
|
||||||
|
, salt/0]).
|
||||||
|
|
||||||
-include("logger.hrl").
|
-include("logger.hrl").
|
||||||
|
|
||||||
-type(hash_type() :: plain | md5 | sha | sha256 | sha512 | pbkdf2 | bcrypt).
|
-type(password() :: binary()).
|
||||||
|
-type(password_hash() :: binary()).
|
||||||
|
|
||||||
-export_type([hash_type/0]).
|
-type(hash_type_simple() :: plain | md5 | sha | sha256 | sha512).
|
||||||
|
-type(hash_type() :: hash_type_simple() | bcrypt | pbkdf2).
|
||||||
|
|
||||||
|
-type(salt_position() :: prefix | suffix).
|
||||||
|
-type(salt() :: binary()).
|
||||||
|
|
||||||
|
-type(pbkdf2_mac_fun() :: md4 | md5 | ripemd160 | sha | sha224 | sha256 | sha384 | sha512).
|
||||||
|
-type(pbkdf2_iterations() :: pos_integer()).
|
||||||
|
-type(pbkdf2_dk_length() :: pos_integer() | undefined).
|
||||||
|
|
||||||
|
-type(hash_params() ::
|
||||||
|
{bcrypt, salt()} |
|
||||||
|
{pbkdf2, pbkdf2_mac_fun(), salt(), pbkdf2_iterations(), pbkdf2_dk_length()} |
|
||||||
|
{hash_type_simple(), salt(), salt_position()}).
|
||||||
|
|
||||||
|
-export_type([pbkdf2_mac_fun/0]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec(check_pass(binary() | tuple(), binary() | tuple())
|
-spec(check_pass(hash_params(), password_hash(), password()) -> boolean()).
|
||||||
-> ok | {error, term()}).
|
check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password) ->
|
||||||
check_pass({PassHash, Password}, bcrypt) ->
|
case pbkdf2(MacFun, Password, Salt, Iterations, DKLength) of
|
||||||
try
|
{ok, HashPasswd} ->
|
||||||
Salt = binary:part(PassHash, {0, 29}),
|
compare_secure(hex(HashPasswd), PasswordHash);
|
||||||
check_pass(PassHash, emqx_passwd:hash(bcrypt, {Salt, Password}))
|
{error, _Reason}->
|
||||||
catch
|
false
|
||||||
error:badarg -> {error, incorrect_hash}
|
|
||||||
end;
|
end;
|
||||||
check_pass({PassHash, Password}, HashType) ->
|
check_pass({bcrypt, Salt}, PasswordHash, Password) ->
|
||||||
check_pass(PassHash, emqx_passwd:hash(HashType, Password));
|
case bcrypt:hashpw(Password, Salt) of
|
||||||
check_pass({PassHash, Salt, Password}, {pbkdf2, Macfun, Iterations, Dklen}) ->
|
{ok, HashPasswd} ->
|
||||||
check_pass(PassHash, emqx_passwd:hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}));
|
compare_secure(list_to_binary(HashPasswd), PasswordHash);
|
||||||
check_pass({PassHash, Salt, Password}, {salt, bcrypt}) ->
|
{error, _Reason}->
|
||||||
check_pass(PassHash, emqx_passwd:hash(bcrypt, {Salt, Password}));
|
false
|
||||||
check_pass({PassHash, Salt, Password}, {bcrypt, salt}) ->
|
end;
|
||||||
check_pass(PassHash, emqx_passwd:hash(bcrypt, {Salt, Password}));
|
check_pass({_SimpleHash, _Salt, _SaltPosition} = HashParams, PasswordHash, Password) ->
|
||||||
check_pass({PassHash, Salt, Password}, {salt, HashType}) ->
|
Hash = hash(HashParams, Password),
|
||||||
check_pass(PassHash, emqx_passwd:hash(HashType, <<Salt/binary, Password/binary>>));
|
compare_secure(Hash, PasswordHash).
|
||||||
check_pass({PassHash, Salt, Password}, {HashType, salt}) ->
|
|
||||||
check_pass(PassHash, emqx_passwd:hash(HashType, <<Password/binary, Salt/binary>>));
|
|
||||||
check_pass(PassHash, PassHash) -> ok;
|
|
||||||
check_pass(_Hash1, _Hash2) -> {error, password_error}.
|
|
||||||
|
|
||||||
-spec(hash(hash_type(), binary() | tuple()) -> binary()).
|
-spec(hash(hash_params(), password()) -> password_hash()).
|
||||||
hash(plain, Password) ->
|
hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password) ->
|
||||||
Password;
|
case pbkdf2(MacFun, Password, Salt, Iterations, DKLength) of
|
||||||
hash(md5, Password) ->
|
{ok, HashPasswd} ->
|
||||||
hexstring(crypto:hash(md5, Password));
|
hex(HashPasswd);
|
||||||
hash(sha, Password) ->
|
{error, Reason}->
|
||||||
hexstring(crypto:hash(sha, Password));
|
error(Reason)
|
||||||
hash(sha256, Password) ->
|
|
||||||
hexstring(crypto:hash(sha256, Password));
|
|
||||||
hash(sha512, Password) ->
|
|
||||||
hexstring(crypto:hash(sha512, Password));
|
|
||||||
hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) ->
|
|
||||||
case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of
|
|
||||||
{ok, Hexstring} ->
|
|
||||||
pbkdf2:to_hex(Hexstring);
|
|
||||||
{error, Reason} ->
|
|
||||||
?SLOG(error, #{msg => "pbkdf2_hash_error", reason => Reason}),
|
|
||||||
<<>>
|
|
||||||
end;
|
end;
|
||||||
hash(bcrypt, {Salt, Password}) ->
|
hash({bcrypt, Salt}, Password) ->
|
||||||
{ok, _} = application:ensure_all_started(bcrypt),
|
|
||||||
case bcrypt:hashpw(Password, Salt) of
|
case bcrypt:hashpw(Password, Salt) of
|
||||||
{ok, HashPasswd} ->
|
{ok, HashPasswd} ->
|
||||||
list_to_binary(HashPasswd);
|
list_to_binary(HashPasswd);
|
||||||
{error, Reason}->
|
{error, Reason}->
|
||||||
?SLOG(error, #{msg => "bcrypt_hash_error", reason => Reason}),
|
error(Reason)
|
||||||
<<>>
|
end;
|
||||||
|
hash({SimpleHash, Salt, prefix}, Password) when is_binary(Password), is_binary(Salt) ->
|
||||||
|
hash_data(SimpleHash, <<Salt/binary, Password/binary>>);
|
||||||
|
hash({SimpleHash, Salt, suffix}, Password) when is_binary(Password), is_binary(Salt) ->
|
||||||
|
hash_data(SimpleHash, <<Password/binary, Salt/binary>>).
|
||||||
|
|
||||||
|
|
||||||
|
-spec(hash_data(hash_type(), binary()) -> binary()).
|
||||||
|
hash_data(plain, Data) when is_binary(Data) ->
|
||||||
|
Data;
|
||||||
|
hash_data(md5, Data) when is_binary(Data) ->
|
||||||
|
hex(crypto:hash(md5, Data));
|
||||||
|
hash_data(sha, Data) when is_binary(Data) ->
|
||||||
|
hex(crypto:hash(sha, Data));
|
||||||
|
hash_data(sha256, Data) when is_binary(Data) ->
|
||||||
|
hex(crypto:hash(sha256, Data));
|
||||||
|
hash_data(sha512, Data) when is_binary(Data) ->
|
||||||
|
hex(crypto:hash(sha512, Data)).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
compare_secure(X, Y) when is_binary(X), is_binary(Y) ->
|
||||||
|
compare_secure(binary_to_list(X), binary_to_list(Y));
|
||||||
|
compare_secure(X, Y) when is_list(X), is_list(Y) ->
|
||||||
|
case length(X) == length(Y) of
|
||||||
|
true ->
|
||||||
|
compare_secure(X, Y, 0);
|
||||||
|
false ->
|
||||||
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
compare_secure([X | RestX], [Y | RestY], Result) ->
|
||||||
%% Internal funcs
|
compare_secure(RestX, RestY, (X bxor Y) bor Result);
|
||||||
%%--------------------------------------------------------------------
|
compare_secure([], [], Result) ->
|
||||||
|
Result == 0.
|
||||||
|
|
||||||
hexstring(<<X:128/big-unsigned-integer>>) ->
|
|
||||||
iolist_to_binary(io_lib:format("~32.16.0b", [X]));
|
pbkdf2(MacFun, Password, Salt, Iterations, undefined) ->
|
||||||
hexstring(<<X:160/big-unsigned-integer>>) ->
|
pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations);
|
||||||
iolist_to_binary(io_lib:format("~40.16.0b", [X]));
|
pbkdf2(MacFun, Password, Salt, Iterations, DKLength) ->
|
||||||
hexstring(<<X:256/big-unsigned-integer>>) ->
|
pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations, DKLength).
|
||||||
iolist_to_binary(io_lib:format("~64.16.0b", [X]));
|
|
||||||
hexstring(<<X:512/big-unsigned-integer>>) ->
|
|
||||||
iolist_to_binary(io_lib:format("~128.16.0b", [X])).
|
hex(X) when is_binary(X) ->
|
||||||
|
pbkdf2:to_hex(X).
|
||||||
|
|
|
@ -19,13 +19,85 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
||||||
all() -> [t_hash].
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
{ok, _} = application:ensure_all_started(bcrypt),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_hash_data(_) ->
|
||||||
|
Password = <<"password">>,
|
||||||
|
Password = emqx_passwd:hash_data(plain, Password),
|
||||||
|
|
||||||
|
<<"5f4dcc3b5aa765d61d8327deb882cf99">>
|
||||||
|
= emqx_passwd:hash_data(md5, Password),
|
||||||
|
|
||||||
|
<<"5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8">>
|
||||||
|
= emqx_passwd:hash_data(sha, Password),
|
||||||
|
|
||||||
|
<<"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8">>
|
||||||
|
= emqx_passwd:hash_data(sha256, Password),
|
||||||
|
|
||||||
|
Sha512 = iolist_to_binary(
|
||||||
|
[<<"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb9">>,
|
||||||
|
<<"80b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86">>]),
|
||||||
|
|
||||||
|
Sha512 = emqx_passwd:hash_data(sha512, Password).
|
||||||
|
|
||||||
t_hash(_) ->
|
t_hash(_) ->
|
||||||
Password = <<"password">>, Salt = <<"salt">>,
|
Password = <<"password">>,
|
||||||
_ = emqx_passwd:hash(plain, Password),
|
Salt = <<"salt">>,
|
||||||
_ = emqx_passwd:hash(md5, Password),
|
WrongPassword = <<"wrongpass">>,
|
||||||
_ = emqx_passwd:hash(sha, Password),
|
|
||||||
_ = emqx_passwd:hash(sha256, Password),
|
Md5 = <<"67a1e09bb1f83f5007dc119c14d663aa">>,
|
||||||
_ = emqx_passwd:hash(bcrypt, {Salt, Password}),
|
Md5 = emqx_passwd:hash({md5, Salt, prefix}, Password),
|
||||||
_ = emqx_passwd:hash(pbkdf2, {Salt, Password, sha256, 1000, 20}).
|
true = emqx_passwd:check_pass({md5, Salt, prefix}, Md5, Password),
|
||||||
|
false = emqx_passwd:check_pass({md5, Salt, prefix}, Md5, WrongPassword),
|
||||||
|
|
||||||
|
Sha = <<"59b3e8d637cf97edbe2384cf59cb7453dfe30789">>,
|
||||||
|
Sha = emqx_passwd:hash({sha, Salt, prefix}, Password),
|
||||||
|
true = emqx_passwd:check_pass({sha, Salt, prefix}, Sha, Password),
|
||||||
|
false = emqx_passwd:check_pass({sha, Salt, prefix}, Sha, WrongPassword),
|
||||||
|
|
||||||
|
Sha256 = <<"7a37b85c8918eac19a9089c0fa5a2ab4dce3f90528dcdeec108b23ddf3607b99">>,
|
||||||
|
Sha256 = emqx_passwd:hash({sha256, Salt, suffix}, Password),
|
||||||
|
true = emqx_passwd:check_pass({sha256, Salt, suffix}, Sha256, Password),
|
||||||
|
false = emqx_passwd:check_pass({sha256, Salt, suffix}, Sha256, WrongPassword),
|
||||||
|
|
||||||
|
Sha512 = iolist_to_binary(
|
||||||
|
[<<"fa6a2185b3e0a9a85ef41ffb67ef3c1fb6f74980f8ebf970e4e72e353ed9537d">>,
|
||||||
|
<<"593083c201dfd6e43e1c8a7aac2bc8dbb119c7dfb7d4b8f131111395bd70e97f">>]),
|
||||||
|
Sha512 = emqx_passwd:hash({sha512, Salt, suffix}, Password),
|
||||||
|
true = emqx_passwd:check_pass({sha512, Salt, suffix}, Sha512, Password),
|
||||||
|
false = emqx_passwd:check_pass({sha512, Salt, suffix}, Sha512, WrongPassword),
|
||||||
|
|
||||||
|
BcryptSalt = <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
Bcrypt = <<"$2b$12$wtY3h20mUjjmeaClpqZVvehyw7F.V78F3rbK2xDkCzRTMi6pmfUB6">>,
|
||||||
|
Bcrypt = emqx_passwd:hash({bcrypt, BcryptSalt}, Password),
|
||||||
|
true = emqx_passwd:check_pass({bcrypt, Bcrypt}, Bcrypt, Password),
|
||||||
|
false = emqx_passwd:check_pass({bcrypt, Bcrypt}, Bcrypt, WrongPassword),
|
||||||
|
false = emqx_passwd:check_pass({bcrypt, <<>>}, <<>>, WrongPassword),
|
||||||
|
|
||||||
|
%% Invalid salt, bcrypt fails
|
||||||
|
?assertException(error, _, emqx_passwd:hash({bcrypt, Salt}, Password)),
|
||||||
|
|
||||||
|
BadDKlen = 1 bsl 32,
|
||||||
|
Pbkdf2Salt = <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
|
Pbkdf2 = <<"01dbee7f4a9e243e988b62c73cda935d"
|
||||||
|
"a05378b93244ec8f48a99e61ad799d86">>,
|
||||||
|
Pbkdf2 = emqx_passwd:hash({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Password),
|
||||||
|
true = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Pbkdf2, Password),
|
||||||
|
false = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Pbkdf2, WrongPassword),
|
||||||
|
false = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, BadDKlen}, Pbkdf2, Password),
|
||||||
|
|
||||||
|
%% Invalid derived_length, pbkdf2 fails
|
||||||
|
?assertException(error, _, emqx_passwd:hash({pbkdf2, sha, Pbkdf2Salt, 2, BadDKlen}, Password)).
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authn_password_hashing).
|
||||||
|
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
-type(simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512).
|
||||||
|
-type(salt_position() :: prefix | suffix).
|
||||||
|
|
||||||
|
-type(simple_algorithm() :: #{name := simple_algorithm_name(),
|
||||||
|
salt_position := salt_position()}).
|
||||||
|
|
||||||
|
-type(bcrypt_algorithm() :: #{name := bcrypt}).
|
||||||
|
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}).
|
||||||
|
|
||||||
|
-type(pbkdf2_algorithm() :: #{name := pbkdf2,
|
||||||
|
mac_fun := emqx_passwd:pbkdf2_mac_fun(),
|
||||||
|
iterations := pos_integer()}).
|
||||||
|
|
||||||
|
-type(algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm()).
|
||||||
|
-type(algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw()).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Hocon Schema
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
|
-export([roots/0,
|
||||||
|
fields/1]).
|
||||||
|
|
||||||
|
-export([type_ro/1,
|
||||||
|
type_rw/1]).
|
||||||
|
|
||||||
|
-export([init/1,
|
||||||
|
gen_salt/1,
|
||||||
|
hash/2,
|
||||||
|
check_password/4]).
|
||||||
|
|
||||||
|
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
|
||||||
|
|
||||||
|
fields(bcrypt_rw) ->
|
||||||
|
fields(bcrypt) ++
|
||||||
|
[{salt_rounds, fun salt_rounds/1}];
|
||||||
|
|
||||||
|
fields(bcrypt) ->
|
||||||
|
[{name, {enum, [bcrypt]}}];
|
||||||
|
|
||||||
|
fields(pbkdf2) ->
|
||||||
|
[{name, {enum, [pbkdf2]}},
|
||||||
|
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
|
||||||
|
{iterations, integer()},
|
||||||
|
{dk_length, fun dk_length/1}];
|
||||||
|
|
||||||
|
fields(other_algorithms) ->
|
||||||
|
[{name, {enum, [plain, md5, sha, sha256, sha512]}},
|
||||||
|
{salt_position, fun salt_position/1}].
|
||||||
|
|
||||||
|
salt_position(type) -> {enum, [prefix, suffix]};
|
||||||
|
salt_position(default) -> prefix;
|
||||||
|
salt_position(_) -> undefined.
|
||||||
|
|
||||||
|
salt_rounds(type) -> integer();
|
||||||
|
salt_rounds(default) -> 10;
|
||||||
|
salt_rounds(_) -> undefined.
|
||||||
|
|
||||||
|
dk_length(type) -> integer();
|
||||||
|
dk_length(nullable) -> true;
|
||||||
|
dk_length(default) -> undefined;
|
||||||
|
dk_length(_) -> undefined.
|
||||||
|
|
||||||
|
type_rw(type) ->
|
||||||
|
hoconsc:union(rw_refs());
|
||||||
|
type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||||
|
type_rw(_) -> undefined.
|
||||||
|
|
||||||
|
type_ro(type) ->
|
||||||
|
hoconsc:union(ro_refs());
|
||||||
|
type_ro(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||||
|
type_ro(_) -> undefined.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(init(algorithm()) -> ok).
|
||||||
|
init(#{name := bcrypt}) ->
|
||||||
|
{ok, _} = application:ensure_all_started(bcrypt),
|
||||||
|
ok;
|
||||||
|
init(#{name := _Other}) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
|
||||||
|
-spec(gen_salt(algorithm_rw()) -> emqx_passwd:salt()).
|
||||||
|
gen_salt(#{name := plain}) ->
|
||||||
|
<<>>;
|
||||||
|
gen_salt(#{name := bcrypt,
|
||||||
|
salt_rounds := Rounds}) ->
|
||||||
|
{ok, Salt} = bcrypt:gen_salt(Rounds),
|
||||||
|
list_to_binary(Salt);
|
||||||
|
gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt ->
|
||||||
|
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
||||||
|
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
||||||
|
|
||||||
|
|
||||||
|
-spec(hash(algorithm_rw(), emqx_passwd:password()) -> {emqx_passwd:hash(), emqx_passwd:salt()}).
|
||||||
|
hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
|
||||||
|
Salt0 = gen_salt(Algorithm),
|
||||||
|
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
|
||||||
|
Salt = Hash,
|
||||||
|
{Hash, Salt};
|
||||||
|
hash(#{name := pbkdf2,
|
||||||
|
mac_fun := MacFun,
|
||||||
|
iterations := Iterations} = Algorithm, Password) ->
|
||||||
|
Salt = gen_salt(Algorithm),
|
||||||
|
DKLength = maps:get(dk_length, Algorithm, undefined),
|
||||||
|
Hash = emqx_passwd:hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password),
|
||||||
|
{Hash, Salt};
|
||||||
|
hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
|
||||||
|
Salt = gen_salt(Algorithm),
|
||||||
|
Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password),
|
||||||
|
{Hash, Salt}.
|
||||||
|
|
||||||
|
|
||||||
|
-spec(check_password(
|
||||||
|
algorithm(),
|
||||||
|
emqx_passwd:salt(),
|
||||||
|
emqx_passwd:hash(),
|
||||||
|
emqx_passwd:password()) -> boolean()).
|
||||||
|
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
|
||||||
|
emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password);
|
||||||
|
check_password(#{name := pbkdf2,
|
||||||
|
mac_fun := MacFun,
|
||||||
|
iterations := Iterations} = Algorithm,
|
||||||
|
Salt, PasswordHash, Password) ->
|
||||||
|
DKLength = maps:get(dk_length, Algorithm, undefined),
|
||||||
|
emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password);
|
||||||
|
check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) ->
|
||||||
|
emqx_passwd:check_pass({Other, Salt, SaltPosition}, PasswordHash, Password).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
rw_refs() ->
|
||||||
|
[hoconsc:ref(?MODULE, bcrypt_rw),
|
||||||
|
hoconsc:ref(?MODULE, pbkdf2),
|
||||||
|
hoconsc:ref(?MODULE, other_algorithms)].
|
||||||
|
|
||||||
|
ro_refs() ->
|
||||||
|
[hoconsc:ref(?MODULE, bcrypt),
|
||||||
|
hoconsc:ref(?MODULE, pbkdf2),
|
||||||
|
hoconsc:ref(?MODULE, other_algorithms)].
|
|
@ -18,12 +18,10 @@
|
||||||
|
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
|
|
||||||
-export([ replace_placeholders/2
|
-export([ check_password_from_selected_map/3
|
||||||
|
, replace_placeholders/2
|
||||||
, replace_placeholder/2
|
, replace_placeholder/2
|
||||||
, check_password/3
|
|
||||||
, is_superuser/1
|
, is_superuser/1
|
||||||
, hash/4
|
|
||||||
, gen_salt/0
|
|
||||||
, bin/1
|
, bin/1
|
||||||
, ensure_apps_started/1
|
, ensure_apps_started/1
|
||||||
, cleanup_resources/0
|
, cleanup_resources/0
|
||||||
|
@ -36,6 +34,17 @@
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
|
||||||
|
{error, bad_username_or_password};
|
||||||
|
check_password_from_selected_map(
|
||||||
|
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password) ->
|
||||||
|
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
||||||
|
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
||||||
|
true -> ok;
|
||||||
|
false ->
|
||||||
|
{error, bad_username_or_password}
|
||||||
|
end.
|
||||||
|
|
||||||
replace_placeholders(PlaceHolders, Data) ->
|
replace_placeholders(PlaceHolders, Data) ->
|
||||||
replace_placeholders(PlaceHolders, Data, []).
|
replace_placeholders(PlaceHolders, Data, []).
|
||||||
|
|
||||||
|
@ -64,27 +73,6 @@ replace_placeholder(?PH_CERT_CN_NAME, Credential) ->
|
||||||
replace_placeholder(Constant, _) ->
|
replace_placeholder(Constant, _) ->
|
||||||
Constant.
|
Constant.
|
||||||
|
|
||||||
check_password(undefined, _Selected, _State) ->
|
|
||||||
{error, bad_username_or_password};
|
|
||||||
check_password(Password,
|
|
||||||
#{<<"password_hash">> := Hash},
|
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
|
||||||
case emqx_passwd:hash(bcrypt, {Hash, Password}) of
|
|
||||||
Hash -> ok;
|
|
||||||
_ ->
|
|
||||||
{error, bad_username_or_password}
|
|
||||||
end;
|
|
||||||
check_password(Password,
|
|
||||||
#{<<"password_hash">> := Hash} = Selected,
|
|
||||||
#{password_hash_algorithm := Algorithm,
|
|
||||||
salt_position := SaltPosition}) ->
|
|
||||||
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
|
||||||
case hash(Algorithm, Password, Salt, SaltPosition) of
|
|
||||||
Hash -> ok;
|
|
||||||
_ ->
|
|
||||||
{error, bad_username_or_password}
|
|
||||||
end.
|
|
||||||
|
|
||||||
is_superuser(#{<<"is_superuser">> := <<"">>}) ->
|
is_superuser(#{<<"is_superuser">> := <<"">>}) ->
|
||||||
#{is_superuser => false};
|
#{is_superuser => false};
|
||||||
is_superuser(#{<<"is_superuser">> := <<"0">>}) ->
|
is_superuser(#{<<"is_superuser">> := <<"0">>}) ->
|
||||||
|
@ -108,15 +96,6 @@ ensure_apps_started(bcrypt) ->
|
||||||
ensure_apps_started(_) ->
|
ensure_apps_started(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
hash(Algorithm, Password, Salt, prefix) ->
|
|
||||||
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
|
||||||
hash(Algorithm, Password, Salt, suffix) ->
|
|
||||||
emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>).
|
|
||||||
|
|
||||||
gen_salt() ->
|
|
||||||
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
|
||||||
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
|
||||||
|
|
||||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||||
bin(L) when is_list(L) -> list_to_binary(L);
|
bin(L) when is_list(L) -> list_to_binary(L);
|
||||||
bin(X) -> X.
|
bin(X) -> X.
|
||||||
|
|
|
@ -91,31 +91,13 @@ fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
||||||
, {backend, emqx_authn_schema:backend('built-in-database')}
|
, {backend, emqx_authn_schema:backend('built-in-database')}
|
||||||
, {user_id_type, fun user_id_type/1}
|
, {user_id_type, fun user_id_type/1}
|
||||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
|
||||||
] ++ emqx_authn_schema:common_fields();
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
fields(bcrypt) ->
|
|
||||||
[ {name, {enum, [bcrypt]}}
|
|
||||||
, {salt_rounds, fun salt_rounds/1}
|
|
||||||
];
|
|
||||||
|
|
||||||
fields(other_algorithms) ->
|
|
||||||
[ {name, {enum, [plain, md5, sha, sha256, sha512]}}
|
|
||||||
].
|
|
||||||
|
|
||||||
user_id_type(type) -> user_id_type();
|
user_id_type(type) -> user_id_type();
|
||||||
user_id_type(default) -> <<"username">>;
|
user_id_type(default) -> <<"username">>;
|
||||||
user_id_type(_) -> undefined.
|
user_id_type(_) -> undefined.
|
||||||
|
|
||||||
password_hash_algorithm(type) -> hoconsc:union([hoconsc:ref(?MODULE, bcrypt),
|
|
||||||
hoconsc:ref(?MODULE, other_algorithms)]);
|
|
||||||
password_hash_algorithm(default) -> #{<<"name">> => sha256};
|
|
||||||
password_hash_algorithm(_) -> undefined.
|
|
||||||
|
|
||||||
salt_rounds(type) -> integer();
|
|
||||||
salt_rounds(default) -> 10;
|
|
||||||
salt_rounds(_) -> undefined.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -125,22 +107,11 @@ refs() ->
|
||||||
|
|
||||||
create(AuthenticatorID,
|
create(AuthenticatorID,
|
||||||
#{user_id_type := Type,
|
#{user_id_type := Type,
|
||||||
password_hash_algorithm := #{name := bcrypt,
|
password_hash_algorithm := Algorithm}) ->
|
||||||
salt_rounds := SaltRounds}}) ->
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
ok = emqx_authn_utils:ensure_apps_started(bcrypt),
|
|
||||||
State = #{user_group => AuthenticatorID,
|
State = #{user_group => AuthenticatorID,
|
||||||
user_id_type => Type,
|
user_id_type => Type,
|
||||||
password_hash_algorithm => bcrypt,
|
password_hash_algorithm => Algorithm},
|
||||||
salt_rounds => SaltRounds},
|
|
||||||
{ok, State};
|
|
||||||
|
|
||||||
create(AuthenticatorID,
|
|
||||||
#{user_id_type := Type,
|
|
||||||
password_hash_algorithm := #{name := Name}}) ->
|
|
||||||
ok = emqx_authn_utils:ensure_apps_started(Name),
|
|
||||||
State = #{user_group => AuthenticatorID,
|
|
||||||
user_id_type => Type,
|
|
||||||
password_hash_algorithm => Name},
|
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
update(Config, #{user_group := ID}) ->
|
update(Config, #{user_group := ID}) ->
|
||||||
|
@ -156,12 +127,9 @@ authenticate(#{password := Password} = Credential,
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[] ->
|
[] ->
|
||||||
ignore;
|
ignore;
|
||||||
[#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = IsSuperuser}] ->
|
[#user_info{password_hash = PasswordHash, salt = Salt, is_superuser = IsSuperuser}] ->
|
||||||
Salt = case Algorithm of
|
case emqx_authn_password_hashing:check_password(
|
||||||
bcrypt -> PasswordHash;
|
Algorithm, Salt, PasswordHash, Password) of
|
||||||
_ -> Salt0
|
|
||||||
end,
|
|
||||||
case PasswordHash =:= hash(Algorithm, Password, Salt) of
|
|
||||||
true -> {ok, #{is_superuser => IsSuperuser}};
|
true -> {ok, #{is_superuser => IsSuperuser}};
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end
|
end
|
||||||
|
@ -193,12 +161,13 @@ import_users(Filename0, State) ->
|
||||||
|
|
||||||
add_user(#{user_id := UserID,
|
add_user(#{user_id := UserID,
|
||||||
password := Password} = UserInfo,
|
password := Password} = UserInfo,
|
||||||
#{user_group := UserGroup} = State) ->
|
#{user_group := UserGroup,
|
||||||
|
password_hash_algorithm := Algorithm}) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
{PasswordHash, Salt} = hash(Password, State),
|
{PasswordHash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
|
||||||
IsSuperuser = maps:get(is_superuser, UserInfo, false),
|
IsSuperuser = maps:get(is_superuser, UserInfo, false),
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
||||||
{ok, #{user_id => UserID, is_superuser => IsSuperuser}};
|
{ok, #{user_id => UserID, is_superuser => IsSuperuser}};
|
||||||
|
@ -219,7 +188,8 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
end).
|
end).
|
||||||
|
|
||||||
update_user(UserID, UserInfo,
|
update_user(UserID, UserInfo,
|
||||||
#{user_group := UserGroup} = State) ->
|
#{user_group := UserGroup,
|
||||||
|
password_hash_algorithm := Algorithm}) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
|
@ -229,11 +199,12 @@ update_user(UserID, UserInfo,
|
||||||
, salt = Salt
|
, salt = Salt
|
||||||
, is_superuser = IsSuperuser}] ->
|
, is_superuser = IsSuperuser}] ->
|
||||||
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
|
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
|
||||||
{NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of
|
{NPasswordHash, NSalt} = case UserInfo of
|
||||||
undefined ->
|
#{password := Password} ->
|
||||||
{PasswordHash, Salt};
|
emqx_authn_password_hashing:hash(
|
||||||
Password ->
|
Algorithm, Password);
|
||||||
hash(Password, State)
|
#{} ->
|
||||||
|
{PasswordHash, Salt}
|
||||||
end,
|
end,
|
||||||
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
|
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
|
||||||
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
|
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
|
||||||
|
@ -349,26 +320,6 @@ get_user_info_by_seq([<<"false">> | More1], [<<"is_superuser">> | More2], Acc) -
|
||||||
get_user_info_by_seq(_, _, _) ->
|
get_user_info_by_seq(_, _, _) ->
|
||||||
{error, bad_format}.
|
{error, bad_format}.
|
||||||
|
|
||||||
gen_salt(#{password_hash_algorithm := plain}) ->
|
|
||||||
<<>>;
|
|
||||||
gen_salt(#{password_hash_algorithm := bcrypt,
|
|
||||||
salt_rounds := Rounds}) ->
|
|
||||||
{ok, Salt} = bcrypt:gen_salt(Rounds),
|
|
||||||
Salt;
|
|
||||||
gen_salt(_) ->
|
|
||||||
emqx_authn_utils:gen_salt().
|
|
||||||
|
|
||||||
hash(bcrypt, Password, Salt) ->
|
|
||||||
{ok, Hash} = bcrypt:hashpw(Password, Salt),
|
|
||||||
list_to_binary(Hash);
|
|
||||||
hash(Algorithm, Password, Salt) ->
|
|
||||||
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>).
|
|
||||||
|
|
||||||
hash(Password, #{password_hash_algorithm := Algorithm} = State) ->
|
|
||||||
Salt = gen_salt(State),
|
|
||||||
PasswordHash = hash(Algorithm, Password, Salt),
|
|
||||||
{PasswordHash, Salt}.
|
|
||||||
|
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
|
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
|
||||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
||||||
password_hash = PasswordHash,
|
password_hash = PasswordHash,
|
||||||
|
|
|
@ -63,8 +63,7 @@ common_fields() ->
|
||||||
, {password_hash_field, fun password_hash_field/1}
|
, {password_hash_field, fun password_hash_field/1}
|
||||||
, {salt_field, fun salt_field/1}
|
, {salt_field, fun salt_field/1}
|
||||||
, {is_superuser_field, fun is_superuser_field/1}
|
, {is_superuser_field, fun is_superuser_field/1}
|
||||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||||
, {salt_position, fun salt_position/1}
|
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
collection(type) -> binary();
|
collection(type) -> binary();
|
||||||
|
@ -84,14 +83,6 @@ is_superuser_field(type) -> binary();
|
||||||
is_superuser_field(nullable) -> true;
|
is_superuser_field(nullable) -> true;
|
||||||
is_superuser_field(_) -> undefined.
|
is_superuser_field(_) -> undefined.
|
||||||
|
|
||||||
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
|
||||||
password_hash_algorithm(default) -> sha256;
|
|
||||||
password_hash_algorithm(_) -> undefined.
|
|
||||||
|
|
||||||
salt_position(type) -> {enum, [prefix, suffix]};
|
|
||||||
salt_position(default) -> prefix;
|
|
||||||
salt_position(_) -> undefined.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -116,7 +107,7 @@ create(#{selector := Selector} = Config) ->
|
||||||
salt_position],
|
salt_position],
|
||||||
Config),
|
Config),
|
||||||
#{password_hash_algorithm := Algorithm} = State,
|
#{password_hash_algorithm := Algorithm} = State,
|
||||||
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
NState = State#{
|
NState = State#{
|
||||||
selector => NSelector,
|
selector => NSelector,
|
||||||
|
@ -203,24 +194,10 @@ normalize_selector(Selector) ->
|
||||||
|
|
||||||
check_password(undefined, _Selected, _State) ->
|
check_password(undefined, _Selected, _State) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password(Password,
|
|
||||||
Doc,
|
|
||||||
#{password_hash_algorithm := bcrypt,
|
|
||||||
password_hash_field := PasswordHashField}) ->
|
|
||||||
case maps:get(PasswordHashField, Doc, undefined) of
|
|
||||||
undefined ->
|
|
||||||
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
|
||||||
Hash ->
|
|
||||||
case {ok, to_list(Hash)} =:= bcrypt:hashpw(Password, Hash) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
Doc,
|
Doc,
|
||||||
#{password_hash_algorithm := Algorithm,
|
#{password_hash_algorithm := Algorithm,
|
||||||
password_hash_field := PasswordHashField,
|
password_hash_field := PasswordHashField} = State) ->
|
||||||
salt_position := SaltPosition} = State) ->
|
|
||||||
case maps:get(PasswordHashField, Doc, undefined) of
|
case maps:get(PasswordHashField, Doc, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
||||||
|
@ -229,7 +206,7 @@ check_password(Password,
|
||||||
undefined -> <<>>;
|
undefined -> <<>>;
|
||||||
SaltField -> maps:get(SaltField, Doc, <<>>)
|
SaltField -> maps:get(SaltField, Doc, <<>>)
|
||||||
end,
|
end,
|
||||||
case Hash =:= hash(Algorithm, Password, Salt, SaltPosition) of
|
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end
|
end
|
||||||
|
@ -240,12 +217,3 @@ is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) ->
|
||||||
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser});
|
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser});
|
||||||
is_superuser(_, _) ->
|
is_superuser(_, _) ->
|
||||||
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}).
|
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}).
|
||||||
|
|
||||||
hash(Algorithm, Password, Salt, prefix) ->
|
|
||||||
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
|
||||||
hash(Algorithm, Password, Salt, suffix) ->
|
|
||||||
emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>).
|
|
||||||
|
|
||||||
to_list(L) when is_list(L) -> L;
|
|
||||||
to_list(L) when is_binary(L) -> binary_to_list(L);
|
|
||||||
to_list(X) -> X.
|
|
||||||
|
|
|
@ -46,22 +46,13 @@ roots() -> [?CONF_NS].
|
||||||
fields(?CONF_NS) ->
|
fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
||||||
, {backend, emqx_authn_schema:backend(mysql)}
|
, {backend, emqx_authn_schema:backend(mysql)}
|
||||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||||
, {salt_position, fun salt_position/1}
|
|
||||||
, {query, fun query/1}
|
, {query, fun query/1}
|
||||||
, {query_timeout, fun query_timeout/1}
|
, {query_timeout, fun query_timeout/1}
|
||||||
] ++ emqx_authn_schema:common_fields()
|
] ++ emqx_authn_schema:common_fields()
|
||||||
++ emqx_connector_schema_lib:relational_db_fields()
|
++ emqx_connector_schema_lib:relational_db_fields()
|
||||||
++ emqx_connector_schema_lib:ssl_fields().
|
++ emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
|
||||||
password_hash_algorithm(default) -> sha256;
|
|
||||||
password_hash_algorithm(_) -> undefined.
|
|
||||||
|
|
||||||
salt_position(type) -> {enum, [prefix, suffix]};
|
|
||||||
salt_position(default) -> prefix;
|
|
||||||
salt_position(_) -> undefined.
|
|
||||||
|
|
||||||
query(type) -> string();
|
query(type) -> string();
|
||||||
query(_) -> undefined.
|
query(_) -> undefined.
|
||||||
|
|
||||||
|
@ -80,14 +71,13 @@ create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
create(#{password_hash_algorithm := Algorithm,
|
create(#{password_hash_algorithm := Algorithm,
|
||||||
salt_position := SaltPosition,
|
|
||||||
query := Query0,
|
query := Query0,
|
||||||
query_timeout := QueryTimeout
|
query_timeout := QueryTimeout
|
||||||
} = Config) ->
|
} = Config) ->
|
||||||
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
{Query, PlaceHolders} = parse_query(Query0),
|
{Query, PlaceHolders} = parse_query(Query0),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
State = #{password_hash_algorithm => Algorithm,
|
State = #{password_hash_algorithm => Algorithm,
|
||||||
salt_position => SaltPosition,
|
|
||||||
query => Query,
|
query => Query,
|
||||||
placeholders => PlaceHolders,
|
placeholders => PlaceHolders,
|
||||||
query_timeout => QueryTimeout,
|
query_timeout => QueryTimeout,
|
||||||
|
@ -116,13 +106,15 @@ authenticate(#{password := Password} = Credential,
|
||||||
#{placeholders := PlaceHolders,
|
#{placeholders := PlaceHolders,
|
||||||
query := Query,
|
query := Query,
|
||||||
query_timeout := Timeout,
|
query_timeout := Timeout,
|
||||||
resource_id := ResourceId} = State) ->
|
resource_id := ResourceId,
|
||||||
|
password_hash_algorithm := Algorithm}) ->
|
||||||
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
||||||
case emqx_resource:query(ResourceId, {sql, Query, Params, Timeout}) of
|
case emqx_resource:query(ResourceId, {sql, Query, Params, Timeout}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, Columns, [Row | _]} ->
|
{ok, Columns, [Row | _]} ->
|
||||||
Selected = maps:from_list(lists:zip(Columns, Row)),
|
Selected = maps:from_list(lists:zip(Columns, Row)),
|
||||||
case emqx_authn_utils:check_password(Password, Selected, State) of
|
case emqx_authn_utils:check_password_from_selected_map(
|
||||||
|
Algorithm, Selected, Password) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
|
@ -52,21 +52,12 @@ roots() -> [?CONF_NS].
|
||||||
fields(?CONF_NS) ->
|
fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
||||||
, {backend, emqx_authn_schema:backend(postgresql)}
|
, {backend, emqx_authn_schema:backend(postgresql)}
|
||||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||||
, {salt_position, fun salt_position/1}
|
|
||||||
, {query, fun query/1}
|
, {query, fun query/1}
|
||||||
] ++ emqx_authn_schema:common_fields()
|
] ++ emqx_authn_schema:common_fields()
|
||||||
++ emqx_connector_schema_lib:relational_db_fields()
|
++ emqx_connector_schema_lib:relational_db_fields()
|
||||||
++ emqx_connector_schema_lib:ssl_fields().
|
++ emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
|
||||||
password_hash_algorithm(default) -> sha256;
|
|
||||||
password_hash_algorithm(_) -> undefined.
|
|
||||||
|
|
||||||
salt_position(type) -> {enum, [prefix, suffix]};
|
|
||||||
salt_position(default) -> prefix;
|
|
||||||
salt_position(_) -> undefined.
|
|
||||||
|
|
||||||
query(type) -> string();
|
query(type) -> string();
|
||||||
query(_) -> undefined.
|
query(_) -> undefined.
|
||||||
|
|
||||||
|
@ -81,14 +72,13 @@ create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
create(#{query := Query0,
|
create(#{query := Query0,
|
||||||
password_hash_algorithm := Algorithm,
|
password_hash_algorithm := Algorithm} = Config) ->
|
||||||
salt_position := SaltPosition} = Config) ->
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
{Query, PlaceHolders} = parse_query(Query0),
|
{Query, PlaceHolders} = parse_query(Query0),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
State = #{query => Query,
|
State = #{query => Query,
|
||||||
placeholders => PlaceHolders,
|
placeholders => PlaceHolders,
|
||||||
password_hash_algorithm => Algorithm,
|
password_hash_algorithm => Algorithm,
|
||||||
salt_position => SaltPosition,
|
|
||||||
resource_id => ResourceId},
|
resource_id => ResourceId},
|
||||||
case emqx_resource:create_local(ResourceId, emqx_connector_pgsql, Config) of
|
case emqx_resource:create_local(ResourceId, emqx_connector_pgsql, Config) of
|
||||||
{ok, already_created} ->
|
{ok, already_created} ->
|
||||||
|
@ -113,14 +103,16 @@ authenticate(#{auth_method := _}, _) ->
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(#{password := Password} = Credential,
|
||||||
#{query := Query,
|
#{query := Query,
|
||||||
placeholders := PlaceHolders,
|
placeholders := PlaceHolders,
|
||||||
resource_id := ResourceId} = State) ->
|
resource_id := ResourceId,
|
||||||
|
password_hash_algorithm := Algorithm}) ->
|
||||||
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
||||||
case emqx_resource:query(ResourceId, {sql, Query, Params}) of
|
case emqx_resource:query(ResourceId, {sql, Query, Params}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, Columns, [Row | _]} ->
|
{ok, Columns, [Row | _]} ->
|
||||||
NColumns = [Name || #column{name = Name} <- Columns],
|
NColumns = [Name || #column{name = Name} <- Columns],
|
||||||
Selected = maps:from_list(lists:zip(NColumns, erlang:tuple_to_list(Row))),
|
Selected = maps:from_list(lists:zip(NColumns, erlang:tuple_to_list(Row))),
|
||||||
case emqx_authn_utils:check_password(Password, Selected, State) of
|
case emqx_authn_utils:check_password_from_selected_map(
|
||||||
|
Algorithm, Selected, Password) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
|
@ -59,21 +59,12 @@ common_fields() ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
|
||||||
, {backend, emqx_authn_schema:backend(redis)}
|
, {backend, emqx_authn_schema:backend(redis)}
|
||||||
, {cmd, fun cmd/1}
|
, {cmd, fun cmd/1}
|
||||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||||
, {salt_position, fun salt_position/1}
|
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
cmd(type) -> string();
|
cmd(type) -> string();
|
||||||
cmd(_) -> undefined.
|
cmd(_) -> undefined.
|
||||||
|
|
||||||
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
|
||||||
password_hash_algorithm(default) -> sha256;
|
|
||||||
password_hash_algorithm(_) -> undefined.
|
|
||||||
|
|
||||||
salt_position(type) -> {enum, [prefix, suffix]};
|
|
||||||
salt_position(default) -> prefix;
|
|
||||||
salt_position(_) -> undefined.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -89,6 +80,7 @@ create(_AuthenticatorID, Config) ->
|
||||||
|
|
||||||
create(#{cmd := Cmd,
|
create(#{cmd := Cmd,
|
||||||
password_hash_algorithm := Algorithm} = Config) ->
|
password_hash_algorithm := Algorithm} = Config) ->
|
||||||
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
try
|
try
|
||||||
NCmd = parse_cmd(Cmd),
|
NCmd = parse_cmd(Cmd),
|
||||||
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
|
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
|
||||||
|
@ -129,13 +121,15 @@ authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(#{password := Password} = Credential,
|
||||||
#{cmd := {Command, Key, Fields},
|
#{cmd := {Command, Key, Fields},
|
||||||
resource_id := ResourceId} = State) ->
|
resource_id := ResourceId,
|
||||||
|
password_hash_algorithm := Algorithm}) ->
|
||||||
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
|
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
|
||||||
case emqx_resource:query(ResourceId, {cmd, [Command, NKey | Fields]}) of
|
case emqx_resource:query(ResourceId, {cmd, [Command, NKey | Fields]}) of
|
||||||
{ok, Values} ->
|
{ok, Values} ->
|
||||||
case merge(Fields, Values) of
|
case merge(Fields, Values) of
|
||||||
#{<<"password_hash">> := _} = Selected ->
|
#{<<"password_hash">> := _} = Selected ->
|
||||||
case emqx_authn_utils:check_password(Password, Selected, State) of
|
case emqx_authn_utils:check_password_from_selected_map(
|
||||||
|
Algorithm, Selected, Password) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
|
@ -239,8 +239,8 @@ test_is_superuser({Value, ExpectedValue}) ->
|
||||||
raw_mongo_auth_config() ->
|
raw_mongo_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password-based">>,
|
mechanism => <<"password-based">>,
|
||||||
password_hash_algorithm => <<"plain">>,
|
password_hash_algorithm => #{name => <<"plain">>,
|
||||||
salt_position => <<"suffix">>,
|
salt_position => <<"suffix">>},
|
||||||
enable => <<"true">>,
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"mongodb">>,
|
backend => <<"mongodb">>,
|
||||||
|
@ -282,8 +282,8 @@ user_seeds() ->
|
||||||
password => <<"md5">>
|
password => <<"md5">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"md5">>,
|
password_hash_algorithm => #{name => <<"md5">>,
|
||||||
salt_position => <<"suffix">>
|
salt_position => <<"suffix">> }
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -300,8 +300,8 @@ user_seeds() ->
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
selector => #{<<"username">> => <<"${clientid}">>},
|
selector => #{<<"username">> => <<"${clientid}">>},
|
||||||
password_hash_algorithm => <<"sha256">>,
|
password_hash_algorithm => #{name => <<"sha256">>,
|
||||||
salt_position => <<"prefix">>
|
salt_position => <<"prefix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => true}}
|
result => {ok,#{is_superuser => true}}
|
||||||
},
|
},
|
||||||
|
@ -317,8 +317,7 @@ user_seeds() ->
|
||||||
password => <<"bcrypt">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">> % should be ignored
|
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -336,8 +335,7 @@ user_seeds() ->
|
||||||
config_params => #{
|
config_params => #{
|
||||||
% clientid variable & username credentials
|
% clientid variable & username credentials
|
||||||
selector => #{<<"username">> => <<"${clientid}">>},
|
selector => #{<<"username">> => <<"${clientid}">>},
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
@ -354,8 +352,7 @@ user_seeds() ->
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
selector => #{<<"userid">> => <<"${clientid}">>},
|
selector => #{<<"userid">> => <<"${clientid}">>},
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
@ -372,8 +369,7 @@ user_seeds() ->
|
||||||
password => <<"wrongpass">>
|
password => <<"wrongpass">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,bad_username_or_password}
|
result => {error,bad_username_or_password}
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,8 +205,8 @@ t_update(_Config) ->
|
||||||
raw_mysql_auth_config() ->
|
raw_mysql_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password-based">>,
|
mechanism => <<"password-based">>,
|
||||||
password_hash_algorithm => <<"plain">>,
|
password_hash_algorithm => #{name => <<"plain">>,
|
||||||
salt_position => <<"suffix">>,
|
salt_position => <<"suffix">>},
|
||||||
enable => <<"true">>,
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"mysql">>,
|
backend => <<"mysql">>,
|
||||||
|
@ -244,8 +244,8 @@ user_seeds() ->
|
||||||
password => <<"md5">>
|
password => <<"md5">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"md5">>,
|
password_hash_algorithm => #{name => <<"md5">>,
|
||||||
salt_position => <<"suffix">>
|
salt_position => <<"suffix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -263,8 +263,8 @@ user_seeds() ->
|
||||||
config_params => #{
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
FROM users where username = ${clientid} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"sha256">>,
|
password_hash_algorithm => #{name => <<"sha256">>,
|
||||||
salt_position => <<"prefix">>
|
salt_position => <<"prefix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => true}}
|
result => {ok,#{is_superuser => true}}
|
||||||
},
|
},
|
||||||
|
@ -282,8 +282,7 @@ user_seeds() ->
|
||||||
config_params => #{
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
FROM users where username = ${username} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">> % should be ignored
|
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -300,8 +299,7 @@ user_seeds() ->
|
||||||
config_params => #{
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
FROM users where username = ${username} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">> % should be ignored
|
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -320,8 +318,7 @@ user_seeds() ->
|
||||||
% clientid variable & username credentials
|
% clientid variable & username credentials
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
FROM users where username = ${clientid} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
@ -340,8 +337,7 @@ user_seeds() ->
|
||||||
% Bad keys in query
|
% Bad keys in query
|
||||||
query => <<"SELECT 1 AS unknown_field
|
query => <<"SELECT 1 AS unknown_field
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
FROM users where username = ${username} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
@ -358,8 +354,7 @@ user_seeds() ->
|
||||||
password => <<"wrongpass">>
|
password => <<"wrongpass">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,bad_username_or_password}
|
result => {error,bad_username_or_password}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authn_password_hashing_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-define(SIMPLE_HASHES, [plain, md5, sha, sha256, sha512]).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
{ok, _} = application:ensure_all_started(bcrypt),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_gen_salt(_Config) ->
|
||||||
|
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
|
||||||
|
++ [#{name => bcrypt, salt_rounds => 10}],
|
||||||
|
|
||||||
|
lists:foreach(
|
||||||
|
fun(Algorithm) ->
|
||||||
|
Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
|
||||||
|
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
|
||||||
|
?assert(is_binary(Salt))
|
||||||
|
end,
|
||||||
|
Algorithms).
|
||||||
|
|
||||||
|
t_init(_Config) ->
|
||||||
|
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
|
||||||
|
++ [#{name => bcrypt, salt_rounds => 10}],
|
||||||
|
|
||||||
|
lists:foreach(
|
||||||
|
fun(Algorithm) ->
|
||||||
|
ok = emqx_authn_password_hashing:init(Algorithm)
|
||||||
|
end,
|
||||||
|
Algorithms).
|
||||||
|
|
||||||
|
t_check_password(_Config) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun test_check_password/1,
|
||||||
|
hash_examples()).
|
||||||
|
|
||||||
|
test_check_password(#{
|
||||||
|
password_hash := Hash,
|
||||||
|
salt := Salt,
|
||||||
|
password := Password,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
} = Sample) ->
|
||||||
|
ct:pal("t_check_password sample: ~p", [Sample]),
|
||||||
|
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password),
|
||||||
|
false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>).
|
||||||
|
|
||||||
|
t_hash(_Config) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun test_hash/1,
|
||||||
|
hash_examples()).
|
||||||
|
|
||||||
|
test_hash(#{password := Password,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
} = Sample) ->
|
||||||
|
ct:pal("t_hash sample: ~p", [Sample]),
|
||||||
|
{Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
|
||||||
|
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password).
|
||||||
|
|
||||||
|
hash_examples() ->
|
||||||
|
[#{
|
||||||
|
password_hash => <<"plainsalt">>,
|
||||||
|
salt => <<"salt">>,
|
||||||
|
password => <<"plain">>,
|
||||||
|
password_hash_algorithm => #{name => plain,
|
||||||
|
salt_position => suffix}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||||
|
salt => <<"salt">>,
|
||||||
|
password => <<"md5">>,
|
||||||
|
password_hash_algorithm => #{name => md5,
|
||||||
|
salt_position => suffix}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
|
||||||
|
salt => <<"salt">>,
|
||||||
|
password => <<"sha">>,
|
||||||
|
password_hash_algorithm => #{name => sha,
|
||||||
|
salt_position => prefix}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||||
|
salt => <<"salt">>,
|
||||||
|
password => <<"sha256">>,
|
||||||
|
password_hash_algorithm => #{name => sha256,
|
||||||
|
salt_position => prefix}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
|
||||||
|
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>,
|
||||||
|
salt => <<"salt">>,
|
||||||
|
password => <<"sha512">>,
|
||||||
|
password_hash_algorithm => #{name => sha512,
|
||||||
|
salt_position => prefix}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
password => <<"bcrypt">>,
|
||||||
|
|
||||||
|
password_hash_algorithm => #{name => bcrypt,
|
||||||
|
salt_rounds => 10}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
password_hash => <<"01dbee7f4a9e243e988b62c73cda935d"
|
||||||
|
"a05378b93244ec8f48a99e61ad799d86">>,
|
||||||
|
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
|
password => <<"password">>,
|
||||||
|
|
||||||
|
password_hash_algorithm => #{name => pbkdf2,
|
||||||
|
iterations => 2,
|
||||||
|
dk_length => 32,
|
||||||
|
mac_fun => sha}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
||||||
|
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
|
password => <<"password">>,
|
||||||
|
|
||||||
|
password_hash_algorithm => #{name => pbkdf2,
|
||||||
|
iterations => 2,
|
||||||
|
mac_fun => sha}
|
||||||
|
}
|
||||||
|
].
|
|
@ -273,8 +273,8 @@ t_parse_query(_) ->
|
||||||
raw_pgsql_auth_config() ->
|
raw_pgsql_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password-based">>,
|
mechanism => <<"password-based">>,
|
||||||
password_hash_algorithm => <<"plain">>,
|
password_hash_algorithm => #{name => <<"plain">>,
|
||||||
salt_position => <<"suffix">>,
|
salt_position => <<"suffix">>},
|
||||||
enable => <<"true">>,
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"postgresql">>,
|
backend => <<"postgresql">>,
|
||||||
|
@ -312,8 +312,8 @@ user_seeds() ->
|
||||||
password => <<"md5">>
|
password => <<"md5">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"md5">>,
|
password_hash_algorithm => #{name => <<"md5">>,
|
||||||
salt_position => <<"suffix">>
|
salt_position => <<"suffix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -331,8 +331,8 @@ user_seeds() ->
|
||||||
config_params => #{
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
FROM users where username = ${clientid} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"sha256">>,
|
password_hash_algorithm => #{name => <<"sha256">>,
|
||||||
salt_position => <<"prefix">>
|
salt_position => <<"prefix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => true}}
|
result => {ok,#{is_superuser => true}}
|
||||||
},
|
},
|
||||||
|
@ -350,8 +350,7 @@ user_seeds() ->
|
||||||
config_params => #{
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
FROM users where username = ${username} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">> % should be ignored
|
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
@ -370,8 +369,7 @@ user_seeds() ->
|
||||||
% clientid variable & username credentials
|
% clientid variable & username credentials
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
FROM users where username = ${clientid} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
@ -390,8 +388,7 @@ user_seeds() ->
|
||||||
% Bad keys in query
|
% Bad keys in query
|
||||||
query => <<"SELECT 1 AS unknown_field
|
query => <<"SELECT 1 AS unknown_field
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
FROM users where username = ${username} LIMIT 1">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
@ -408,8 +405,7 @@ user_seeds() ->
|
||||||
password => <<"wrongpass">>
|
password => <<"wrongpass">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,bad_username_or_password}
|
result => {error,bad_username_or_password}
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,8 +209,8 @@ t_update(_Config) ->
|
||||||
raw_redis_auth_config() ->
|
raw_redis_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password-based">>,
|
mechanism => <<"password-based">>,
|
||||||
password_hash_algorithm => <<"plain">>,
|
password_hash_algorithm => #{name => <<"plain">>,
|
||||||
salt_position => <<"suffix">>,
|
salt_position => <<"suffix">>},
|
||||||
enable => <<"true">>,
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"redis">>,
|
backend => <<"redis">>,
|
||||||
|
@ -222,123 +222,136 @@ raw_redis_auth_config() ->
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
[#{data => #{
|
[#{data => #{
|
||||||
password_hash => "plainsalt",
|
password_hash => <<"plainsalt">>,
|
||||||
salt => "salt",
|
salt => <<"salt">>,
|
||||||
is_superuser => "1"
|
is_superuser => <<"1">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"plain">>,
|
username => <<"plain">>,
|
||||||
password => <<"plain">>},
|
password => <<"plain">>},
|
||||||
key => "mqtt_user:plain",
|
key => <<"mqtt_user:plain">>,
|
||||||
config_params => #{},
|
config_params => #{},
|
||||||
result => {ok,#{is_superuser => true}}
|
result => {ok,#{is_superuser => true}}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{data => #{
|
||||||
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
|
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||||
salt => "salt",
|
salt => <<"salt">>,
|
||||||
is_superuser => "0"
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"md5">>,
|
username => <<"md5">>,
|
||||||
password => <<"md5">>
|
password => <<"md5">>
|
||||||
},
|
},
|
||||||
key => "mqtt_user:md5",
|
key => <<"mqtt_user:md5">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"md5">>,
|
password_hash_algorithm => #{name => <<"md5">>,
|
||||||
salt_position => <<"suffix">>
|
salt_position => <<"suffix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{data => #{
|
||||||
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
|
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||||
salt => "salt",
|
salt => <<"salt">>,
|
||||||
is_superuser => "1"
|
is_superuser => <<"1">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
clientid => <<"sha256">>,
|
clientid => <<"sha256">>,
|
||||||
password => <<"sha256">>
|
password => <<"sha256">>
|
||||||
},
|
},
|
||||||
key => "mqtt_user:sha256",
|
key => <<"mqtt_user:sha256">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
|
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
|
||||||
password_hash_algorithm => <<"sha256">>,
|
password_hash_algorithm => #{name => <<"sha256">>,
|
||||||
salt_position => <<"prefix">>
|
salt_position => <<"prefix">>}
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => true}}
|
result => {ok,#{is_superuser => true}}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
is_superuser => "0"
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt">>,
|
username => <<"bcrypt">>,
|
||||||
password => <<"bcrypt">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
key => "mqtt_user:bcrypt",
|
key => <<"mqtt_user:bcrypt">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">> % should be ignored
|
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok,#{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
is_superuser => "0"
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"pbkdf2">>,
|
||||||
|
password => <<"password">>
|
||||||
|
},
|
||||||
|
key => <<"mqtt_user:pbkdf2">>,
|
||||||
|
config_params => #{
|
||||||
|
password_hash_algorithm => #{name => <<"pbkdf2">>,
|
||||||
|
iterations => 2,
|
||||||
|
mac_fun => sha
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result => {ok,#{is_superuser => false}}
|
||||||
|
},
|
||||||
|
#{data => #{
|
||||||
|
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt0">>,
|
username => <<"bcrypt0">>,
|
||||||
password => <<"bcrypt">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
key => "mqtt_user:bcrypt0",
|
key => <<"mqtt_user:bcrypt0">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
% clientid variable & username credentials
|
% clientid variable & username credentials
|
||||||
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
|
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
is_superuser => "0"
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt1">>,
|
username => <<"bcrypt1">>,
|
||||||
password => <<"bcrypt">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
key => "mqtt_user:bcrypt1",
|
key => <<"mqtt_user:bcrypt1">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
% Bad key in cmd
|
% Bad key in cmd
|
||||||
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
|
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {error,not_authorized}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
is_superuser => "0"
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt2">>,
|
username => <<"bcrypt2">>,
|
||||||
% Wrong password
|
% Wrong password
|
||||||
password => <<"wrongpass">>
|
password => <<"wrongpass">>
|
||||||
},
|
},
|
||||||
key => "mqtt_user:bcrypt2",
|
key => <<"mqtt_user:bcrypt2">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||||
password_hash_algorithm => <<"bcrypt">>,
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
salt_position => <<"suffix">>
|
|
||||||
},
|
},
|
||||||
result => {error,bad_username_or_password}
|
result => {error,bad_username_or_password}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue