feat(authn redis): support authn with redis

This commit is contained in:
zhouzb 2021-08-06 14:05:31 +08:00
parent 58b39361b3
commit 3761db0525
10 changed files with 251 additions and 14 deletions

View File

@ -4,6 +4,8 @@ services:
redis_server:
container_name: redis
image: redis:${REDIS_TAG}
ports:
- "6379:6379"
command:
- redis-server
- "--bind 0.0.0.0 ::"

View File

@ -21,6 +21,17 @@ authentication: {
# salt_field: salt
# password_hash_algorithm: sha256
# salt_position: prefix
# },
# {
# name: "authenticator 3"
# mechanism: password-based
# server_type: redis
# server: "127.0.0.1:6379"
# password: "public"
# database: 0
# query: "HMGET ${mqtt-username} password_hash salt"
# password_hash_algorithm: sha256
# salt_position: prefix
# }
]
}

View File

@ -325,6 +325,8 @@ authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'})
emqx_authn_pgsql;
authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) ->
emqx_authn_mongodb;
authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) ->
emqx_authn_redis;
authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
emqx_authn_http;
authenticator_provider(#{mechanism := jwt}) ->

View File

@ -49,7 +49,10 @@ authenticators(type) ->
, hoconsc:ref(emqx_authn_pgsql, config)
, hoconsc:ref(emqx_authn_mongodb, standalone)
, hoconsc:ref(emqx_authn_mongodb, 'replica-set')
, hoconsc:ref(emqx_authn_mongodb, sharded)
, hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
, hoconsc:ref(emqx_authn_redis, standalone)
, hoconsc:ref(emqx_authn_redis, cluster)
, hoconsc:ref(emqx_authn_redis, sentinel)
, hoconsc:ref(emqx_authn_http, get)
, hoconsc:ref(emqx_authn_http, post)
, hoconsc:ref(emqx_authn_jwt, 'hmac-based')

View File

@ -18,6 +18,7 @@
-export([ replace_placeholders/2
, replace_placeholder/2
, hash/4
, gen_salt/0
, bin/1
]).
@ -54,6 +55,10 @@ replace_placeholder(<<"${cert-common-name}">>, Credential) ->
replace_placeholder(Constant, _) ->
Constant.
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),

View File

@ -41,7 +41,7 @@ structs() -> [""].
fields("") ->
[ {config, {union, [ hoconsc:t(standalone)
, hoconsc:t('replica-set')
, hoconsc:t(sharded)
, hoconsc:t('sharded-cluster')
]}}
];
@ -51,7 +51,7 @@ fields(standalone) ->
fields('replica-set') ->
common_fields() ++ emqx_connector_mongo:fields(rs);
fields(sharded) ->
fields('sharded-cluster') ->
common_fields() ++ emqx_connector_mongo:fields(sharded).
common_fields() ->

View File

@ -146,11 +146,7 @@ check_password(Password,
#{password_hash_algorithm := Algorithm,
salt_position := SaltPosition}) ->
Salt = maps:get(salt, Selected, <<>>),
Hash0 = case SaltPosition of
prefix -> emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
end,
case Hash0 =:= Hash of
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
true -> ok;
false -> {error, bad_username_or_password}
end.

View File

@ -132,11 +132,7 @@ check_password(Password,
#{password_hash_algorithm := Algorithm,
salt_position := SaltPosition}) ->
Salt = maps:get(salt, Selected, <<>>),
Hash0 = case SaltPosition of
prefix -> emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
end,
case Hash0 =:= Hash of
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
true -> ok;
false -> {error, bad_username_or_password}
end.

View File

@ -0,0 +1,222 @@
%%--------------------------------------------------------------------
%% 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_redis).
-include("emqx_authn.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-export([ structs/0
, fields/1
]).
-export([ create/1
, update/2
, authenticate/2
, destroy/1
]).
%%------------------------------------------------------------------------------
%% Hocon Schema
%%------------------------------------------------------------------------------
structs() -> [""].
fields("") ->
[ {config, {union, [ hoconsc:t(standalone)
, hoconsc:t(cluster)
, hoconsc:t(sentinel)
]}}
];
fields(standalone) ->
common_fields() ++ emqx_connector_redis:fields(single);
fields(cluster) ->
common_fields() ++ emqx_connector_redis:fields(cluster);
fields(sentinel) ->
common_fields() ++ emqx_connector_redis:fields(sentinel).
common_fields() ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, [redis]}}
, {query, fun query/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
].
query(type) -> string();
query(nullable) -> false;
query(_) -> 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
%%------------------------------------------------------------------------------
create(#{ query := Query
, '_unique' := Unique
} = Config) ->
try
NQuery = parse_query(Query),
State = maps:with([ password_hash_algorithm
, salt_position
, '_unique'], Config),
NState = State#{query => NQuery},
case emqx_resource:create_local(Unique, emqx_connector_redis, Config) of
{ok, _} ->
{ok, NState};
{error, already_created} ->
{ok, NState};
{error, Reason} ->
{error, Reason}
end
catch
error:{unsupported_query, Query} ->
{error, {unsupported_query, Query}};
error:missing_password_hash ->
{error, missing_password_hash};
error:{unsupported_field, Field} ->
{error, {unsupported_field, Field}}
end.
update(Config, State) ->
case create(Config) of
{ok, NewState} ->
ok = destroy(State),
{ok, NewState};
{error, Reason} ->
{error, Reason}
end.
authenticate(#{auth_method := _}, _) ->
ignore;
authenticate(#{password := Password} = Credential,
#{ query := {Command, Key, Fields}
, '_unique' := Unique
} = State) ->
try
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
{ok, Values} ->
check_password(Password, merge(Fields, Values), State);
{error, Reason} ->
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
ignore
end
catch
error:{cannot_get_variable, Placeholder} ->
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, {cannot_get_variable, Placeholder}]),
ignore
end.
destroy(#{'_unique' := Unique}) ->
_ = emqx_resource:remove_local(Unique),
ok.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
%% Only support HGET and HMGET
parse_query(Query) ->
case string:tokens(Query, " ") of
[Command, Key, Field | Fields] when Command =:= "HGET" orelse Command =:= "HMGET" ->
NFields = [Field | Fields],
check_fields(NFields),
NKey = parse_key(Key),
{Command, NKey, NFields};
_ ->
error({unsupported_query, Query})
end.
check_fields(Fields) ->
check_fields(Fields, false).
check_fields([], false) ->
error(missing_password_hash);
check_fields([], true) ->
ok;
check_fields(["password_hash" | More], false) ->
check_fields(More, true);
check_fields(["salt" | More], HasPassHash) ->
check_fields(More, HasPassHash);
% check_fields(["is_superuser" | More], HasPassHash) ->
% check_fields(More, HasPassHash);
check_fields([Field | _], _) ->
error({unsupported_field, Field}).
parse_key(Key) ->
Tokens = re:split(Key, "(" ++ ?RE_PLACEHOLDER ++ ")", [{return, binary}, group, trim]),
parse_key(Tokens, []).
parse_key([], Acc) ->
lists:reverse(Acc);
parse_key([[Constant, Placeholder] | Tokens], Acc) ->
parse_key(Tokens, [{placeholder, Placeholder}, {constant, Constant} | Acc]);
parse_key([[Constant] | Tokens], Acc) ->
parse_key(Tokens, [{constant, Constant} | Acc]).
replace_placeholders(Key, Credential) ->
lists:map(fun({constant, Constant}) ->
Constant;
({placeholder, Placeholder}) ->
case emqx_authn_utils:replace_placeholder(Placeholder, Credential) of
undefined -> error({cannot_get_variable, Placeholder});
Value -> Value
end
end, Key).
merge(Fields, Value) when not is_list(Value) ->
merge(Fields, [Value]);
merge(Fields, Values) ->
maps:from_list(
lists:filter(fun({_, V}) ->
V =/= undefined
end, lists:zip(Fields, Values))).
check_password(undefined, _Selected, _State) ->
{error, bad_username_or_password};
check_password(Password,
#{"password_hash" := PasswordHash},
#{password_hash_algorithm := bcrypt}) ->
case {ok, PasswordHash} =:= bcrypt:hashpw(Password, PasswordHash) of
true -> ok;
false -> {error, bad_username_or_password}
end;
check_password(Password,
#{"password_hash" := PasswordHash} = Selected,
#{password_hash_algorithm := Algorithm,
salt_position := SaltPosition}) ->
Salt = maps:get("salt", Selected, <<>>),
case PasswordHash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
true -> ok;
false -> {error, bad_username_or_password}
end;
check_password(_Password, _Selected, _State) ->
ignore.

View File

@ -43,7 +43,7 @@
{deps,
[ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
, {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.8"}}}
, {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.9"}}}
, {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}