%%%----------------------------------------------------------------------
%%% 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.
