Packages
ejabberd
25.4.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/ejabberd_auth.erl
%%%----------------------------------------------------------------------
%%% File : ejabberd_auth.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Authentication
%%% Created : 23 Nov 2002 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2025 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(ejabberd_auth).
-behaviour(gen_server).
-author('alexey@process-one.net').
-protocol({rfc, 5802}).
%% External exports
-export([start_link/0, host_up/1, host_down/1, config_reloaded/0,
set_password/3, check_password/4,
check_password/6, check_password_with_authmodule/4,
check_password_with_authmodule/6, try_register/3,
get_users/0, get_users/1, password_to_scram/2,
get_users/2, import_info/0,
count_users/1, import/5, import_start/2,
count_users/2, get_password/2,
get_password_s/2, get_password_with_authmodule/2,
user_exists/2, user_exists_in_other_modules/3,
remove_user/2, remove_user/3, plain_password_required/1,
store_type/1, entropy/1, backend_type/1, password_format/1,
which_users_exists/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([auth_modules/1, convert_to_scram/1, drop_password_type/2, set_password_instance/3]).
-include_lib("xmpp/include/scram.hrl").
-include("logger.hrl").
-define(SALT_LENGTH, 16).
-record(state, {host_modules = #{} :: host_modules()}).
-type host_modules() :: #{binary => [module()]}.
-type password() :: binary() | #scram{}.
-type digest_fun() :: fun((binary()) -> binary()).
-export_type([password/0]).
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
-type opts() :: [{prefix, binary()} | {from, integer()} |
{to, integer()} | {limit, integer()} |
{offset, integer()}].
-callback start(binary()) -> any().
-callback stop(binary()) -> any().
-callback reload(binary()) -> any().
-callback plain_password_required(binary()) -> boolean().
-callback store_type(binary()) -> plain | external | scram.
-callback set_password(binary(), binary(), password()) ->
{ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}.
-callback set_password_multiple(binary(), binary(), [password()]) ->
{ets_cache:tag(), {ok, [password()]} | {error, db_failure | not_allowed}}.
-callback set_password_instance(binary(), binary(), password()) ->
ok | {error, db_failure | not_allowed}.
-callback remove_user(binary(), binary()) -> ok | {error, db_failure | not_allowed}.
-callback user_exists(binary(), binary()) -> {ets_cache:tag(), boolean() | {error, db_failure}}.
-callback check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}.
-callback try_register(binary(), binary(), password()) ->
{ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}.
-callback try_register_multiple(binary(), binary(), [password()]) ->
{ets_cache:tag(), {ok, [password()]} | {error, exists | db_failure | not_allowed}}.
-callback get_users(binary(), opts()) -> [{binary(), binary()}].
-callback count_users(binary(), opts()) -> number().
-callback get_password(binary(), binary()) -> {ets_cache:tag(), {ok, password() | [password()]} | error}.
-callback drop_password_type(binary(), atom()) ->
ok | {error, db_failure | not_allowed}.
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> boolean().
-optional_callbacks([reload/1,
set_password/3,
set_password_multiple/3,
set_password_instance/3,
remove_user/2,
user_exists/2,
check_password/4,
try_register/3,
try_register_multiple/3,
get_users/2,
count_users/2,
get_password/2,
drop_password_type/2,
use_cache/1,
cache_nodes/1]).
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
ejabberd_hooks:add(host_up, ?MODULE, host_up, 30),
ejabberd_hooks:add(host_down, ?MODULE, host_down, 80),
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 40),
HostModules = lists:foldl(
fun(Host, Acc) ->
Modules = auth_modules(Host),
maps:put(Host, Modules, Acc)
end, #{}, ejabberd_option:hosts()),
lists:foreach(
fun({Host, Modules}) ->
start(Host, Modules)
end, maps:to_list(HostModules)),
init_cache(HostModules),
{ok, #state{host_modules = HostModules}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast({host_up, Host}, #state{host_modules = HostModules} = State) ->
Modules = auth_modules(Host),
start(Host, Modules),
NewHostModules = maps:put(Host, Modules, HostModules),
init_cache(NewHostModules),
{noreply, State#state{host_modules = NewHostModules}};
handle_cast({host_down, Host}, #state{host_modules = HostModules} = State) ->
Modules = maps:get(Host, HostModules, []),
stop(Host, Modules),
NewHostModules = maps:remove(Host, HostModules),
init_cache(NewHostModules),
{noreply, State#state{host_modules = NewHostModules}};
handle_cast(config_reloaded, #state{host_modules = HostModules} = State) ->
NewHostModules =
lists:foldl(
fun(Host, Acc) ->
OldModules = maps:get(Host, HostModules, []),
NewModules = auth_modules(Host),
start(Host, NewModules -- OldModules),
stop(Host, OldModules -- NewModules),
reload(Host, misc:intersection(OldModules, NewModules)),
maps:put(Host, NewModules, Acc)
end, HostModules, ejabberd_option:hosts()),
init_cache(NewHostModules),
{noreply, State#state{host_modules = NewHostModules}};
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, State) ->
ejabberd_hooks:delete(host_up, ?MODULE, host_up, 30),
ejabberd_hooks:delete(host_down, ?MODULE, host_down, 80),
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 40),
lists:foreach(
fun({Host, Modules}) ->
stop(Host, Modules)
end, maps:to_list(State#state.host_modules)).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
start(Host, Modules) ->
lists:foreach(fun(M) -> M:start(Host) end, Modules).
stop(Host, Modules) ->
lists:foreach(fun(M) -> M:stop(Host) end, Modules).
reload(Host, Modules) ->
lists:foreach(
fun(M) ->
case erlang:function_exported(M, reload, 1) of
true -> M:reload(Host);
false -> ok
end
end, Modules).
host_up(Host) ->
gen_server:cast(?MODULE, {host_up, Host}).
host_down(Host) ->
gen_server:cast(?MODULE, {host_down, Host}).
config_reloaded() ->
gen_server:cast(?MODULE, config_reloaded).
-spec plain_password_required(binary()) -> boolean().
plain_password_required(Server) ->
lists:any(fun (M) -> M:plain_password_required(Server) end,
auth_modules(Server)).
-spec store_type(binary()) -> plain | scram | external.
store_type(Server) ->
case auth_modules(Server) of
[ejabberd_auth_anonymous] -> external;
Modules ->
lists:foldl(
fun(ejabberd_auth_anonymous, Type) -> Type;
(_, external) -> external;
(M, scram) ->
case M:store_type(Server) of
external -> external;
_ -> scram
end;
(M, plain) ->
M:store_type(Server)
end, plain, Modules)
end.
-spec check_password(binary(), binary(), binary(), binary()) -> boolean().
check_password(User, AuthzId, Server, Password) ->
check_password(User, AuthzId, Server, Password, <<"">>, undefined).
-spec check_password(binary(), binary(), binary(), binary(), binary(),
digest_fun() | undefined) -> boolean().
check_password(User, AuthzId, Server, Password, Digest, DigestGen) ->
case check_password_with_authmodule(
User, AuthzId, Server, Password, Digest, DigestGen) of
{true, _AuthModule} -> true;
false -> false
end.
-spec check_password_with_authmodule(binary(), binary(),
binary(), binary()) -> false | {true, atom()}.
check_password_with_authmodule(User, AuthzId, Server, Password) ->
check_password_with_authmodule(
User, AuthzId, Server, Password, <<"">>, undefined).
-spec check_password_with_authmodule(
binary(), binary(), binary(), binary(), binary(),
digest_fun() | undefined) -> false | {false, atom(), binary()} | {true, atom()}.
check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGen) ->
case validate_credentials(User, Server) of
{ok, LUser, LServer} ->
case {jid:nodeprep(AuthzId), get_is_banned(LUser, LServer)} of
{error, _} ->
false;
{_, {is_banned, BanReason}} ->
{false, 'account-disabled', BanReason};
{LAuthzId, _} ->
untag_stop(
lists:foldl(
fun(Mod, false) ->
case db_check_password(
LUser, LAuthzId, LServer, Password,
Digest, DigestGen, Mod) of
true -> {true, Mod};
false -> false;
{stop, true} -> {stop, {true, Mod}};
{stop, false} -> {stop, false}
end;
(_, Acc) ->
Acc
end, false, auth_modules(LServer)))
end;
_ ->
false
end.
convert_password_for_storage(_Server, #scram{} = Password) ->
{Password, [Password]};
convert_password_for_storage(Server, Password) ->
P = case ejabberd_option:auth_stored_password_types(Server) of
[] ->
case ejabberd_option:auth_password_format(Server) of
plain ->
[Password];
_ ->
[password_to_scram(Server, Password)]
end;
M ->
lists:sort(lists:map(
fun(scram_sha1) ->
password_to_scram(Server, Password, sha, ?SCRAM_DEFAULT_ITERATION_COUNT);
(scram_sha256) ->
password_to_scram(Server, Password, sha256, ?SCRAM_DEFAULT_ITERATION_COUNT);
(scram_sha512) ->
password_to_scram(Server, Password, sha512, ?SCRAM_DEFAULT_ITERATION_COUNT);
(plain) ->
Password
end, M))
end,
{Password, P}.
-spec set_password(binary(), binary(), password()) -> ok | {error,
db_failure | not_allowed |
invalid_jid | invalid_password}.
set_password(User, Server, Password) ->
case validate_credentials(User, Server, Password) of
{ok, LUser, LServer} ->
{Plain, Passwords} = convert_password_for_storage(Server, Password),
lists:foldl(
fun(M, {error, _}) ->
db_set_password(LUser, LServer, Plain, Passwords, M);
(_, ok) ->
ok
end, {error, not_allowed}, auth_modules(LServer));
Err ->
Err
end.
set_password_instance(User, Server, Password) ->
case validate_credentials(User, Server, Password) of
{ok, LUser, LServer} ->
lists:foldl(
fun(Mod, {error, _} = Acc) ->
case erlang:function_exported(Mod, set_password_instance, 3) of
true ->
R = Mod:set_password_instance(LUser, LServer, Password),
case use_cache(Mod, LServer) of
true ->
ets_cache:delete(cache_tab(Mod), {LUser, LServer},
cache_nodes(Mod, LServer));
_ ->
ok
end,
R;
_ ->
Acc
end;
(_, ok) ->
ok
end, {error, not_allowed}, auth_modules(LServer));
Err ->
Err
end.
-spec try_register(binary(), binary(), password()) -> ok | {error,
db_failure | not_allowed | exists |
invalid_jid | invalid_password}.
try_register(User, Server, Password) ->
case validate_credentials(User, Server, Password) of
{ok, LUser, LServer} ->
case user_exists(LUser, LServer) of
true ->
{error, exists};
false ->
case ejabberd_router:is_my_host(LServer) of
true ->
case ejabberd_hooks:run_fold(check_register_user, LServer, true,
[User, Server, Password]) of
true ->
{Plain, Passwords} = convert_password_for_storage(Server, Password),
case lists:foldl(
fun(_, ok) ->
ok;
(Mod, _) ->
db_try_register(
LUser, LServer, Plain, Passwords, Mod)
end, {error, not_allowed}, auth_modules(LServer)) of
ok ->
ejabberd_hooks:run(
register_user, LServer, [LUser, LServer]);
{error, _} = Err ->
Err
end;
false ->
{error, not_allowed}
end;
false ->
{error, not_allowed}
end
end;
Err ->
Err
end.
-spec get_users() -> [{binary(), binary()}].
get_users() ->
lists:flatmap(
fun({Host, Mod}) ->
db_get_users(Host, [], Mod)
end, auth_modules()).
-spec get_users(binary()) -> [{binary(), binary()}].
get_users(Server) ->
get_users(Server, []).
-spec get_users(binary(), opts()) -> [{binary(), binary()}].
get_users(Server, Opts) ->
case jid:nameprep(Server) of
error -> [];
LServer ->
lists:flatmap(
fun(M) -> db_get_users(LServer, Opts, M) end,
auth_modules(LServer))
end.
-spec count_users(binary()) -> non_neg_integer().
count_users(Server) ->
count_users(Server, []).
-spec count_users(binary(), opts()) -> non_neg_integer().
count_users(Server, Opts) ->
case jid:nameprep(Server) of
error -> 0;
LServer ->
lists:sum(
lists:map(
fun(M) -> db_count_users(LServer, Opts, M) end,
auth_modules(LServer)))
end.
-spec get_password(binary(), binary()) -> false | [password()].
get_password(User, Server) ->
case get_password_with_authmodule(User, Server) of
{Passwords, _} -> Passwords
end.
-spec get_password_s(binary(), binary()) -> password().
get_password_s(User, Server) ->
case get_password(User, Server) of
false -> <<"">>;
Passwords ->
{_, Pass} = lists:foldl(
fun(Plain, _) when is_binary(Plain) -> {true, Plain};
(Pass, {false, _}) -> {true, Pass};
(_, Acc) -> Acc
end, {false, <<"">>}, Passwords),
Pass
end.
-spec get_password_with_authmodule(binary(), binary()) ->
{false | {false, atom(), binary()} | [password()], module()}.
get_password_with_authmodule(User, Server) ->
case validate_credentials(User, Server) of
{ok, LUser, LServer} ->
case get_is_banned(LUser, LServer) of
{is_banned, BanReason} ->
{{false, 'account-disabled', BanReason}, module_not_consulted};
not_banned ->
case lists:foldl(
fun(M, {error, _}) ->
{db_get_password(LUser, LServer, M), M};
(_M, Acc) ->
Acc
end, {error, undefined}, auth_modules(LServer)) of
{{ok, Password}, Module} when is_list(Password) ->
{Password, Module};
{{ok, Password}, Module} ->
{[Password], Module};
{error, Module} ->
{false, Module}
end
end;
_ ->
{false, undefined}
end.
-spec user_exists(binary(), binary()) -> boolean().
user_exists(_User, <<"">>) ->
false;
user_exists(User, Server) ->
case validate_credentials(User, Server) of
{ok, LUser, LServer} ->
{Exists, PerformExternalUserCheck} =
lists:foldl(
fun(M, {Exists0, PerformExternalUserCheck0}) ->
case db_user_exists(LUser, LServer, M) of
{{error, _}, Check} ->
{Exists0, PerformExternalUserCheck0 orelse Check};
{Else, Check2} ->
{Exists0 orelse Else, PerformExternalUserCheck0 orelse Check2}
end
end, {false, false}, auth_modules(LServer)),
case (not Exists) andalso PerformExternalUserCheck andalso
ejabberd_option:auth_external_user_exists_check(Server) andalso
gen_mod:is_loaded(Server, mod_last) of
true ->
case mod_last:get_last_info(User, Server) of
not_found ->
false;
_ ->
true
end;
_ ->
Exists
end;
_ ->
false
end.
-spec user_exists_in_other_modules(atom(), binary(), binary()) -> boolean() | maybe_exists.
user_exists_in_other_modules(Module, User, Server) ->
user_exists_in_other_modules_loop(
auth_modules(Server) -- [Module], User, Server).
user_exists_in_other_modules_loop([], _User, _Server) ->
false;
user_exists_in_other_modules_loop([AuthModule | AuthModules], User, Server) ->
case db_user_exists(User, Server, AuthModule) of
{true, _} ->
true;
{false, _} ->
user_exists_in_other_modules_loop(AuthModules, User, Server);
{{error, _}, _} ->
maybe_exists
end.
drop_password_type(LServer, Type) ->
Hash = case Type of
plain -> plain;
scram_sha1 -> sha;
scram_sha256 -> sha256;
scram_sha512 -> sha512
end,
lists:foreach(
fun(M) ->
case erlang:function_exported(M, drop_password_type, 2) of
true ->
M:drop_password_type(LServer, Hash),
case use_cache(M, LServer) of
true ->
ets_cache:clear(cache_tab(M),
cache_nodes(M, LServer));
false ->
ok
end;
_ ->
ok
end
end, auth_modules(LServer)).
-spec which_users_exists(list({binary(), binary()})) -> list({binary(), binary()}).
which_users_exists(USPairs) ->
ByServer = lists:foldl(
fun({User, Server}, Dict) ->
LServer = jid:nameprep(Server),
LUser = jid:nodeprep(User),
case gb_trees:lookup(LServer, Dict) of
none ->
gb_trees:insert(LServer, gb_sets:singleton(LUser), Dict);
{value, Set} ->
gb_trees:update(LServer, gb_sets:add(LUser, Set), Dict)
end
end, gb_trees:empty(), USPairs),
Set = lists:foldl(
fun({LServer, UsersSet}, Results) ->
UsersList = gb_sets:to_list(UsersSet),
lists:foldl(
fun(M, Results2) ->
try M:which_users_exists(LServer, UsersList) of
{error, _} ->
Results2;
Res ->
gb_sets:union(
gb_sets:from_list([{U, LServer} || U <- Res]),
Results2)
catch
_:undef ->
lists:foldl(
fun(U, R2) ->
case user_exists(U, LServer) of
true ->
gb_sets:add({U, LServer}, R2);
_ ->
R2
end
end, Results2, UsersList)
end
end, Results, auth_modules(LServer))
end, gb_sets:empty(), gb_trees:to_list(ByServer)),
gb_sets:to_list(Set).
-spec remove_user(binary(), binary()) -> ok.
remove_user(User, Server) ->
case validate_credentials(User, Server) of
{ok, LUser, LServer} ->
lists:foreach(
fun(Mod) -> db_remove_user(LUser, LServer, Mod) end,
auth_modules(LServer)),
ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]);
_Err ->
ok
end.
-spec remove_user(binary(), binary(), password()) -> ok | {error, atom()}.
remove_user(User, Server, Password) ->
case validate_credentials(User, Server, Password) of
{ok, LUser, LServer} ->
case lists:foldl(
fun (_, ok) ->
ok;
(Mod, _) ->
case db_check_password(
LUser, <<"">>, LServer, Password,
<<"">>, undefined, Mod) of
true ->
db_remove_user(LUser, LServer, Mod);
{stop, true} ->
db_remove_user(LUser, LServer, Mod);
false ->
{error, not_allowed};
{stop, false} ->
{error, not_allowed}
end
end, {error, not_allowed}, auth_modules(Server)) of
ok ->
ejabberd_hooks:run(
remove_user, LServer, [LUser, LServer]);
Err ->
Err
end;
Err ->
Err
end.
%% @doc Calculate informational entropy.
-spec entropy(iodata()) -> float().
entropy(B) ->
case binary_to_list(B) of
"" -> 0.0;
S ->
Set = lists:foldl(fun (C,
[Digit, Printable, LowLetter, HiLetter,
Other]) ->
if C >= $a, C =< $z ->
[Digit, Printable, 26, HiLetter,
Other];
C >= $0, C =< $9 ->
[9, Printable, LowLetter, HiLetter,
Other];
C >= $A, C =< $Z ->
[Digit, Printable, LowLetter, 26,
Other];
C >= 33, C =< 126 ->
[Digit, 33, LowLetter, HiLetter,
Other];
true ->
[Digit, Printable, LowLetter,
HiLetter, 128]
end
end,
[0, 0, 0, 0, 0], S),
length(S) * math:log(lists:sum(Set)) / math:log(2)
end.
-spec backend_type(atom()) -> atom().
backend_type(Mod) ->
case atom_to_list(Mod) of
"ejabberd_auth_" ++ T -> list_to_atom(T);
_ -> Mod
end.
-spec password_format(binary() | global) -> plain | scram.
password_format(LServer) ->
ejabberd_option:auth_password_format(LServer).
get_is_banned(User, Server) ->
case mod_admin_extra:get_ban_details(User, Server) of
[] ->
not_banned;
BanDetails ->
{_, ReasonText} = lists:keyfind("reason", 1, BanDetails),
{is_banned, <<"Account is banned: ", ReasonText/binary>>}
end.
%%%----------------------------------------------------------------------
%%% Backend calls
%%%----------------------------------------------------------------------
-spec db_try_register(binary(), binary(), binary(), [password()], module()) -> ok | {error, exists | db_failure | not_allowed}.
db_try_register(User, Server, PlainPassword, Passwords, Mod) ->
Ret = case erlang:function_exported(Mod, try_register_multiple, 3) of
true ->
case use_cache(Mod, Server) of
true ->
ets_cache:update(
cache_tab(Mod), {User, Server}, {ok, Passwords},
fun() -> Mod:try_register_multiple(User, Server, Passwords) end,
cache_nodes(Mod, Server));
false ->
ets_cache:untag(Mod:try_register_multiple(User, Server, Passwords))
end;
_ ->
case erlang:function_exported(Mod, try_register, 3) of
true ->
case use_cache(Mod, Server) of
true ->
ets_cache:update(
cache_tab(Mod), {User, Server}, {ok, [PlainPassword]},
fun() ->
case Mod:try_register(User, Server, PlainPassword) of
{Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}};
Other -> Other
end
end, cache_nodes(Mod, Server));
false ->
case Mod:try_register(User, Server, PlainPassword) of
{_, {ok, Pass}} -> {ok, [Pass]};
V -> ets_cache:untag(V)
end
end;
false ->
{error, not_allowed}
end
end,
case Ret of
{ok, _} -> ok;
{error, _} = Err -> Err
end.
-spec db_set_password(binary(), binary(), binary(), [password()], module()) -> ok | {error, db_failure | not_allowed}.
db_set_password(User, Server, PlainPassword, Passwords, Mod) ->
Ret = case erlang:function_exported(Mod, set_password_multiple, 3) of
true ->
case use_cache(Mod, Server) of
true ->
ets_cache:update(
cache_tab(Mod), {User, Server}, {ok, Passwords},
fun() -> Mod:set_password_multiple(User, Server, Passwords) end,
cache_nodes(Mod, Server));
false ->
ets_cache:untag(Mod:set_password_multiple(User, Server, Passwords))
end;
_ ->
case erlang:function_exported(Mod, set_password, 3) of
true ->
case use_cache(Mod, Server) of
true ->
ets_cache:update(
cache_tab(Mod), {User, Server}, {ok, [PlainPassword]},
fun() ->
case Mod:set_password(User, Server, PlainPassword) of
{Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}};
Other -> Other
end
end, cache_nodes(Mod, Server));
false ->
case Mod:set_password(User, Server, PlainPassword) of
{_, {ok, Pass}} -> {ok, [Pass]};
V -> ets_cache:untag(V)
end
end;
false ->
{error, not_allowed}
end
end,
case Ret of
{ok, _} -> ok;
{error, _} = Err -> Err
end.
db_get_password(User, Server, Mod) ->
UseCache = use_cache(Mod, Server),
case erlang:function_exported(Mod, get_password, 2) of
false when UseCache ->
case ets_cache:lookup(cache_tab(Mod), {User, Server}) of
{ok, exists} -> error;
not_found -> error;
{ok, List} = V when is_list(List) -> V;
{ok, Single} -> {ok, [Single]};
Other -> Other
end;
false ->
error;
true when UseCache ->
ets_cache:lookup(
cache_tab(Mod), {User, Server},
fun() ->
case Mod:get_password(User, Server) of
{_, {ok, List}} = V when is_list(List) -> V;
{Tag, {ok, Single}} -> {Tag, {ok, [Single]}};
Other -> Other
end
end);
true ->
case Mod:get_password(User, Server) of
{_, {ok, List}} when is_list(List) -> {ok, List};
{_, {ok, Single}} -> {ok, [Single]};
Other -> ets_cache:untag(Other)
end
end.
db_user_exists(User, Server, Mod) ->
case db_get_password(User, Server, Mod) of
{ok, _} ->
{true, false};
not_found ->
{false, false};
error ->
case {Mod:store_type(Server), use_cache(Mod, Server)} of
{external, true} ->
Val = case ets_cache:lookup(cache_tab(Mod), {User, Server}, error) of
error ->
ets_cache:update(cache_tab(Mod), {User, Server}, {ok, exists},
fun() ->
case Mod:user_exists(User, Server) of
{CacheTag, true} -> {CacheTag, {ok, exists}};
{CacheTag, false} -> {CacheTag, not_found};
{_, {error, _}} = Err -> Err
end
end);
Other ->
Other
end,
case Val of
{ok, _} ->
{true, Mod /= ejabberd_auth_anonymous} ;
not_found ->
{false, Mod /= ejabberd_auth_anonymous};
error ->
{false, Mod /= ejabberd_auth_anonymous};
{error, _} = Err ->
{Err, Mod /= ejabberd_auth_anonymous}
end;
{external, false} ->
{ets_cache:untag(Mod:user_exists(User, Server)), Mod /= ejabberd_auth_anonymous};
_ ->
{false, false}
end
end.
db_check_password(User, AuthzId, Server, ProvidedPassword,
Digest, DigestFun, Mod) ->
case db_get_password(User, Server, Mod) of
{ok, ValidPasswords} ->
match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun);
error ->
case {Mod:store_type(Server), use_cache(Mod, Server)} of
{external, true} ->
case ets_cache:update(
cache_tab(Mod), {User, Server}, {ok, ProvidedPassword},
fun() ->
case Mod:check_password(
User, AuthzId, Server, ProvidedPassword) of
{CacheTag, true} -> {CacheTag, {ok, ProvidedPassword}};
{CacheTag, {stop, true}} -> {CacheTag, {ok, ProvidedPassword}};
{CacheTag, false} -> {CacheTag, error};
{CacheTag, {stop, false}} -> {CacheTag, error}
end
end) of
{ok, _} ->
true;
error ->
false
end;
{external, false} ->
ets_cache:untag(
Mod:check_password(User, AuthzId, Server, ProvidedPassword));
_ ->
false
end
end.
db_remove_user(User, Server, Mod) ->
case erlang:function_exported(Mod, remove_user, 2) of
true ->
case Mod:remove_user(User, Server) of
ok ->
case use_cache(Mod, Server) of
true ->
ets_cache:delete(cache_tab(Mod), {User, Server},
cache_nodes(Mod, Server));
false ->
ok
end;
{error, _} = Err ->
Err
end;
false ->
{error, not_allowed}
end.
db_get_users(Server, Opts, Mod) ->
case erlang:function_exported(Mod, get_users, 2) of
true ->
Mod:get_users(Server, Opts);
false ->
case use_cache(Mod, Server) of
true ->
ets_cache:fold(
fun({User, S}, {ok, _}, Users) when S == Server ->
[{User, Server}|Users];
(_, _, Users) ->
Users
end, [], cache_tab(Mod));
false ->
[]
end
end.
db_count_users(Server, Opts, Mod) ->
case erlang:function_exported(Mod, count_users, 2) of
true ->
Mod:count_users(Server, Opts);
false ->
case use_cache(Mod, Server) of
true ->
ets_cache:fold(
fun({_, S}, {ok, _}, Num) when S == Server ->
Num + 1;
(_, _, Num) ->
Num
end, 0, cache_tab(Mod));
false ->
0
end
end.
%%%----------------------------------------------------------------------
%%% SCRAM stuff
%%%----------------------------------------------------------------------
is_password_scram_valid(Password, Scram) ->
case jid:resourceprep(Password) of
error ->
false;
_ ->
IterationCount = Scram#scram.iterationcount,
Hash = Scram#scram.hash,
Salt = base64:decode(Scram#scram.salt),
SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount),
StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)),
base64:decode(Scram#scram.storedkey) == StoredKey
end.
password_to_scram(Host, Password) ->
password_to_scram(Host, Password, ?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(_Host, #scram{} = Password, _IterationCount) ->
Password;
password_to_scram(Host, Password, IterationCount) ->
password_to_scram(Host, Password, ejabberd_option:auth_scram_hash(Host), IterationCount).
password_to_scram(_Host, Password, Hash, IterationCount) ->
Salt = p1_rand:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount),
StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)),
ServerKey = scram:server_key(Hash, SaltedPassword),
#scram{storedkey = base64:encode(StoredKey),
serverkey = base64:encode(ServerKey),
salt = base64:encode(Salt),
hash = Hash,
iterationcount = IterationCount}.
%%%----------------------------------------------------------------------
%%% Cache stuff
%%%----------------------------------------------------------------------
-spec init_cache(host_modules()) -> ok.
init_cache(HostModules) ->
CacheOpts = cache_opts(),
{True, False} = use_cache(HostModules),
lists:foreach(
fun(Module) ->
ets_cache:new(cache_tab(Module), CacheOpts)
end, True),
lists:foreach(
fun(Module) ->
ets_cache:delete(cache_tab(Module))
end, False).
-spec cache_opts() -> [proplists:property()].
cache_opts() ->
MaxSize = ejabberd_option:auth_cache_size(),
CacheMissed = ejabberd_option:auth_cache_missed(),
LifeTime = ejabberd_option:auth_cache_life_time(),
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
-spec use_cache(host_modules()) -> {True :: [module()], False :: [module()]}.
use_cache(HostModules) ->
{Enabled, Disabled} =
maps:fold(
fun(Host, Modules, Acc) ->
lists:foldl(
fun(Module, {True, False}) ->
case use_cache(Module, Host) of
true ->
{sets:add_element(Module, True), False};
false ->
{True, sets:add_element(Module, False)}
end
end, Acc, Modules)
end, {sets:new(), sets:new()}, HostModules),
{sets:to_list(Enabled), sets:to_list(sets:subtract(Disabled, Enabled))}.
-spec use_cache(module(), binary()) -> boolean().
use_cache(Mod, LServer) ->
case erlang:function_exported(Mod, use_cache, 1) of
true -> Mod:use_cache(LServer);
false ->
ejabberd_option:auth_use_cache(LServer)
end.
-spec cache_nodes(module(), binary()) -> [node()].
cache_nodes(Mod, LServer) ->
case erlang:function_exported(Mod, cache_nodes, 1) of
true -> Mod:cache_nodes(LServer);
false -> ejabberd_cluster:get_nodes()
end.
-spec cache_tab(module()) -> atom().
cache_tab(Mod) ->
list_to_atom(atom_to_list(Mod) ++ "_cache").
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
-spec auth_modules() -> [{binary(), module()}].
auth_modules() ->
lists:flatmap(
fun(Host) ->
[{Host, Mod} || Mod <- auth_modules(Host)]
end, ejabberd_option:hosts()).
-spec auth_modules(binary()) -> [module()].
auth_modules(Server) ->
LServer = jid:nameprep(Server),
Methods = ejabberd_option:auth_method(LServer),
[ejabberd:module_name([<<"auth">>,
misc:atom_to_binary(M)])
|| M <- Methods].
-spec match_passwords(password(), [password()],
binary(), digest_fun() | undefined) -> boolean().
match_passwords(Provided, Passwords, Digest, DigestFun) ->
lists:any(
fun(Pass) ->
match_password(Provided, Pass, Digest, DigestFun)
end, Passwords).
-spec match_password(password(), password(),
binary(), digest_fun() | undefined) -> boolean().
match_password(Password, #scram{} = Scram, <<"">>, undefined) ->
is_password_scram_valid(Password, Scram);
match_password(Password, #scram{} = Scram, Digest, DigestFun) ->
StoredKey = base64:decode(Scram#scram.storedkey),
DigRes = if Digest /= <<"">> ->
Digest == DigestFun(StoredKey);
true -> false
end,
if DigRes ->
true;
true ->
StoredKey == Password andalso Password /= <<"">>
end;
match_password(ProvidedPassword, ValidPassword, <<"">>, undefined) ->
ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>;
match_password(ProvidedPassword, ValidPassword, Digest, DigestFun) ->
DigRes = if Digest /= <<"">> ->
Digest == DigestFun(ValidPassword);
true -> false
end,
if DigRes ->
true;
true ->
ValidPassword == ProvidedPassword andalso ProvidedPassword /= <<"">>
end.
-spec validate_credentials(binary(), binary()) ->
{ok, binary(), binary()} | {error, invalid_jid}.
validate_credentials(User, Server) ->
validate_credentials(User, Server, #scram{}).
-spec validate_credentials(binary(), binary(), password()) ->
{ok, binary(), binary()} | {error, invalid_jid | invalid_password}.
validate_credentials(_User, _Server, <<"">>) ->
{error, invalid_password};
validate_credentials(User, Server, Password) ->
case jid:nodeprep(User) of
error ->
{error, invalid_jid};
LUser ->
case jid:nameprep(Server) of
error ->
{error, invalid_jid};
LServer ->
if is_record(Password, scram) ->
{ok, LUser, LServer};
true ->
case jid:resourceprep(Password) of
error ->
{error, invalid_password};
_ ->
{ok, LUser, LServer}
end
end
end
end.
untag_stop({stop, Val}) -> Val;
untag_stop(Val) -> Val.
import_info() ->
[{<<"users">>, 3}].
import_start(_LServer, mnesia) ->
ejabberd_auth_mnesia:init_db();
import_start(_LServer, _) ->
ok.
import(Server, {sql, _}, mnesia, <<"users">>, Fields) ->
ejabberd_auth_mnesia:import(Server, Fields);
import(_LServer, {sql, _}, sql, <<"users">>, _) ->
ok.
-spec convert_to_scram(binary()) -> {error, any()} | ok.
convert_to_scram(Server) ->
LServer = jid:nameprep(Server),
if
LServer == error;
LServer == <<>> ->
{error, {incorrect_server_name, Server}};
true ->
lists:foreach(
fun({U, S}) ->
case get_password(U, S) of
[Pass] when is_binary(Pass) ->
SPass = password_to_scram(Server, Pass),
set_password(U, S, SPass);
_ ->
ok
end
end, get_users(LServer)),
ok
end.