diff --git a/big_tests/default.spec b/big_tests/default.spec
index 1cc9051c24d..cb857b274c6 100644
--- a/big_tests/default.spec
+++ b/big_tests/default.spec
@@ -17,6 +17,7 @@
{suites, "tests", amp_big_SUITE}.
{suites, "tests", anonymous_SUITE}.
{suites, "tests", bind2_SUITE}.
+{suites, "tests", fast_auth_token_SUITE}.
{suites, "tests", bosh_SUITE}.
{suites, "tests", carboncopy_SUITE}.
{suites, "tests", connect_SUITE}.
diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec
index b88a9dc765d..7125ebbfd01 100644
--- a/big_tests/dynamic_domains.spec
+++ b/big_tests/dynamic_domains.spec
@@ -16,6 +16,7 @@
{suites, "tests", amp_big_SUITE}.
{suites, "tests", anonymous_SUITE}.
{suites, "tests", bind2_SUITE}.
+{suites, "tests", fast_auth_token_SUITE}.
{suites, "tests", bosh_SUITE}.
{suites, "tests", carboncopy_SUITE}.
{suites, "tests", connect_SUITE}.
diff --git a/big_tests/src/time_helper.erl b/big_tests/src/time_helper.erl
new file mode 100644
index 00000000000..7ea66733a42
--- /dev/null
+++ b/big_tests/src/time_helper.erl
@@ -0,0 +1,24 @@
+-module(time_helper).
+-export([validate_datetime/1]).
+
+%% @doc Validates that string is in ISO 8601 format
+-spec validate_datetime(string()) -> boolean().
+validate_datetime(TimeStr) ->
+ [Date, Time] = string:tokens(TimeStr, "T"),
+ validate_date(Date) and validate_time(Time).
+
+validate_date(Date) ->
+ [Y, M, D] = string:tokens(Date, "-"),
+ Date1 = {list_to_integer(Y), list_to_integer(M), list_to_integer(D)},
+ calendar:valid_date(Date1).
+
+validate_time(Time) ->
+ [T | _] = string:tokens(Time, "Z"),
+ validate_time1(T).
+
+validate_time1(Time) ->
+ [H, M, S] = string:tokens(Time, ":"),
+ check_list([{H, 24}, {M, 60}, {S, 60}]).
+
+check_list(List) ->
+ lists:all(fun({V, L}) -> I = list_to_integer(V), I >= 0 andalso I < L end, List).
diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl
new file mode 100644
index 00000000000..d4509b2ffc6
--- /dev/null
+++ b/big_tests/tests/fast_auth_token_SUITE.erl
@@ -0,0 +1,512 @@
+%% Tests for XEP-0484: Fast Authentication Streamlining Tokens
+-module(fast_auth_token_SUITE).
+
+-compile([export_all, nowarn_export_all]).
+
+-include_lib("stdlib/include/assert.hrl").
+-include_lib("exml/include/exml.hrl").
+-include_lib("jid/include/jid.hrl").
+-include_lib("escalus/include/escalus.hrl").
+-include_lib("escalus/include/escalus_xmlns.hrl").
+
+-define(NS_SASL_2, <<"urn:xmpp:sasl:2">>).
+-define(NS_BIND_2, <<"urn:xmpp:bind:0">>).
+-define(NS_FAST, <<"urn:xmpp:fast:0">>).
+
+%%--------------------------------------------------------------------
+%% Suite configuration
+%%--------------------------------------------------------------------
+
+all() ->
+ [{group, Group} || {Group, _, _} <- groups()].
+
+groups() ->
+ [{Group, [parallel], tests()} || {Group, _Mech} <- mechanisms()].
+
+tests() ->
+ [server_advertises_support_for_fast,
+ request_token_with_initial_authentication,
+ request_token_with_unknown_mechanism_type,
+ client_authenticates_using_fast,
+ client_authenticate_several_times_with_the_same_token,
+ token_auth_fails_when_token_is_wrong,
+ token_auth_fails_when_token_is_not_found,
+ server_initiates_token_rotation,
+ could_still_use_old_token_when_server_initiates_token_rotation,
+ server_initiates_token_rotation_for_the_current_slot,
+ could_still_use_old_token_when_server_initiates_token_rotation_for_the_current_slot,
+ cannot_use_expired_token,
+ cannot_use_expired_token_in_the_current_slot,
+ rerequest_token_with_initial_authentication,
+ can_use_new_token_after_rerequest_token_with_initial_authentication,
+ can_use_current_token_after_rerequest_token_with_initial_authentication,
+ client_requests_token_invalidation,
+ client_requests_token_invalidation_1,
+ both_tokens_do_not_work_after_invalidation,
+ token_auth_fails_when_mechanism_does_not_match
+ ].
+
+mechanisms() ->
+ [{ht_sha_256_none, <<"HT-SHA-256-NONE">>},
+ {ht_sha_384_none, <<"HT-SHA-384-NONE">>},
+ {ht_sha_512_none, <<"HT-SHA-512-NONE">>},
+ {ht_sha_3_256_none, <<"HT-SHA-3-256-NONE">>},
+ {ht_sha_3_384_none, <<"HT-SHA-3-384-NONE">>},
+ {ht_sha_3_512_none, <<"HT-SHA-3-512-NONE">>}].
+
+mech_to_algo(<<"HT-SHA-256-NONE">>) -> sha256;
+mech_to_algo(<<"HT-SHA-384-NONE">>) -> sha384;
+mech_to_algo(<<"HT-SHA-512-NONE">>) -> sha512;
+mech_to_algo(<<"HT-SHA-3-256-NONE">>) -> sha3_256;
+mech_to_algo(<<"HT-SHA-3-384-NONE">>) -> sha3_384;
+mech_to_algo(<<"HT-SHA-3-512-NONE">>) -> sha3_512.
+
+another_mechanism(<<"HT-SHA-256-NONE">>) -> <<"HT-SHA-3-512-NONE">>;
+another_mechanism(<<"HT-SHA-384-NONE">>) -> <<"HT-SHA-3-256-NONE">>;
+another_mechanism(<<"HT-SHA-512-NONE">>) -> <<"HT-SHA-3-512-NONE">>;
+another_mechanism(<<"HT-SHA-3-512-NONE">>) -> <<"HT-SHA-256-NONE">>;
+another_mechanism(<<"HT-SHA-3-384-NONE">>) -> <<"HT-SHA-384-NONE">>;
+another_mechanism(<<"HT-SHA-3-256-NONE">>) -> <<"HT-SHA-512-NONE">>.
+
+%%--------------------------------------------------------------------
+%% Init & teardown
+%%--------------------------------------------------------------------
+
+init_per_suite(Config) ->
+ case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of
+ false ->
+ {skip, "No RDBMS enabled"};
+ true ->
+ Config1 = load_modules(Config),
+ escalus:init_per_suite(Config1)
+ end.
+
+end_per_suite(Config) ->
+ escalus_fresh:clean(),
+ dynamic_modules:restore_modules(Config),
+ escalus:end_per_suite(Config).
+
+init_per_group(Group, Config) ->
+ case lists:keyfind(Group, 1, mechanisms()) of
+ {Group, Mech} when is_binary(Mech) ->
+ [{ht_mech, Mech} | Config];
+ false ->
+ Config
+ end.
+
+end_per_group(_GroupName, Config) ->
+ Config.
+
+init_per_testcase(Name, Config) ->
+ escalus:init_per_testcase(Name, Config).
+
+end_per_testcase(Name, Config) ->
+ escalus:end_per_testcase(Name, Config).
+
+load_modules(Config) ->
+ HostType = domain_helper:host_type(),
+ Config1 = dynamic_modules:save_modules(HostType, Config),
+ sasl2_helper:load_all_sasl2_modules(HostType),
+ Config1.
+
+%%--------------------------------------------------------------------
+%% tests
+%%--------------------------------------------------------------------
+
+%% 3.1 Server advertises support for FAST
+%% https://xmpp.org/extensions/xep-0484.html#support
+server_advertises_support_for_fast(Config) ->
+ Steps = [create_connect_tls, start_stream_get_features],
+ #{features := Features} = sasl2_helper:apply_steps(Steps, Config),
+ Fast = exml_query:path(Features, [{element_with_ns, <<"authentication">>, ?NS_SASL_2},
+ {element, <<"inline">>},
+ {element_with_ns, <<"fast">>, ?NS_FAST}]),
+ ?assertNotEqual(undefined, Fast).
+
+%% Client performs initial authentication
+%% https://xmpp.org/extensions/xep-0484.html#initial-auth
+%% 3.3 Server provides token to client
+%% https://xmpp.org/extensions/xep-0484.html#token-response
+request_token_with_initial_authentication(Config) ->
+ #{token := Token, expire := Expire} = connect_and_ask_for_token(Config),
+ ?assertEqual(true, byte_size(Token) > 5),
+ %% Check timestamp in ISO 8601 format
+ ?assertEqual(true, time_helper:validate_datetime(binary_to_list(Expire))).
+
+request_token_with_unknown_mechanism_type(Config0) ->
+ Config = [{ht_mech, <<"HT-WEIRD-ONE">>} | Config0],
+ Steps = [start_new_user, {?MODULE, auth_and_request_token},
+ receive_features],
+ #{answer := Success} = sasl2_helper:apply_steps(Steps, Config),
+ ?assertMatch(#xmlel{name = <<"success">>,
+ attrs = #{<<"xmlns">> := ?NS_SASL_2}}, Success),
+ Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]),
+ ?assertEqual(undefined, Fast).
+
+%% 3.4 Client authenticates using FAST
+%% https://xmpp.org/extensions/xep-0484.html#fast-auth
+client_authenticates_using_fast(Config) ->
+ #{token := Token, spec := Spec} = connect_and_ask_for_token(Config),
+ auth_with_token(success, Token, Config, Spec).
+
+%% Check that we can reuse the token
+client_authenticate_several_times_with_the_same_token(Config) ->
+ #{token := Token, spec := Spec} = connect_and_ask_for_token(Config),
+ auth_with_token(success, Token, Config, Spec),
+ auth_with_token(success, Token, Config, Spec),
+ auth_with_token(success, Token, Config, Spec).
+
+token_auth_fails_when_token_is_wrong(Config) ->
+ %% New token is not set, but we try to login with a wrong one
+ #{spec := Spec} = connect_and_ask_for_token(Config),
+ Token = <<"wrongtoken">>,
+ auth_with_token(failure, Token, Config, Spec).
+
+token_auth_fails_when_token_is_not_found(Config) ->
+ %% New token is not set
+ Steps = [start_new_user],
+ #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config),
+ Token = <<"wrongtoken">>,
+ auth_with_token(failure, Token, Config, Spec).
+
+%% 3.5 Server initiates token rotation
+%% If client connects with the `current' token (and it is about to expire), we
+%% create a brand new token and set it to the `new' slot.
+%% Most likely the client lost his token from tthe `new' slot.
+%%
+%% If client connects with the `new' token (and it is about to expire), we
+%% should set this token into the `current' position and generate a `new' token.
+%%
+%% Output from server is in the same format as for the regular token request.
+server_initiates_token_rotation(Config) ->
+ #{new_token := NewToken, spec := Spec} = connect_with_almost_expired_token(Config),
+ %% Can use new token
+ auth_with_token(success, NewToken, Config, Spec).
+
+could_still_use_old_token_when_server_initiates_token_rotation(Config) ->
+ #{old_token := OldToken, spec := Spec} = connect_with_almost_expired_token(Config),
+ %% Can still use old token
+ auth_with_token(success, OldToken, Config, Spec).
+
+%% Connect with almost exired token in the current slot
+server_initiates_token_rotation_for_the_current_slot(Config) ->
+ #{new_token := NewToken, spec := Spec} = connect_with_almost_expired_token_in_the_current_slot(Config),
+ %% Can use new token
+ auth_with_token(success, NewToken, Config, Spec).
+
+could_still_use_old_token_when_server_initiates_token_rotation_for_the_current_slot(Config) ->
+ #{old_token := OldToken, spec := Spec} = connect_with_almost_expired_token_in_the_current_slot(Config),
+ %% Can still use old token
+ auth_with_token(success, OldToken, Config, Spec).
+
+cannot_use_expired_token(Config) ->
+ #{expired_token := Token, spec := Spec} = start_new_user_and_make_expired_token(Config),
+ auth_with_token(failure, Token, Config, Spec).
+
+cannot_use_expired_token_in_the_current_slot(Config) ->
+ #{new_token := NewToken, spec := Spec, old_token := CurrentToken} =
+ start_new_user_and_make_expired_token_in_the_current_slot(Config),
+ auth_with_token(failure, CurrentToken, Config, Spec),
+ %% But could use the new non-expired token
+ auth_with_token(success, NewToken, Config, Spec),
+ %% NewToken should be moved into the current slot and still work
+ auth_with_token(success, NewToken, Config, Spec).
+
+rerequest_token_with_initial_authentication(Config) ->
+ #{token := Token, spec := Spec} = connect_and_ask_for_token(Config),
+ ConnectRes = auth_with_token(success, Token, Config, Spec, request_token),
+ #{token := NewToken} = parse_connect_result(ConnectRes),
+ ?assertNotEqual(Token, NewToken),
+ #{token => Token, new_token => NewToken, spec => Spec}.
+
+can_use_new_token_after_rerequest_token_with_initial_authentication(Config) ->
+ #{new_token := Token, spec := Spec} = rerequest_token_with_initial_authentication(Config),
+ auth_with_token(success, Token, Config, Spec).
+
+can_use_current_token_after_rerequest_token_with_initial_authentication(Config) ->
+ #{token := Token, spec := Spec} = rerequest_token_with_initial_authentication(Config),
+ auth_with_token(success, Token, Config, Spec).
+
+client_requests_token_invalidation(Config) ->
+ #{token := Token, spec := Spec} = connect_and_ask_for_token(Config),
+ auth_with_token(success, Token, Config, Spec, request_invalidation),
+ auth_with_token(failure, Token, Config, Spec).
+
+client_requests_token_invalidation_1(Config) ->
+ #{token := Token, spec := Spec} = connect_and_ask_for_token(Config),
+ auth_with_token(success, Token, Config, Spec, request_invalidation_1),
+ auth_with_token(failure, Token, Config, Spec).
+
+both_tokens_do_not_work_after_invalidation(Config) ->
+ #{new_token := NewToken, token := Token, spec := Spec} =
+ rerequest_token_with_initial_authentication(Config),
+ auth_with_token(success, Token, Config, Spec, request_invalidation),
+ auth_with_token(failure, NewToken, Config, Spec),
+ auth_with_token(failure, Token, Config, Spec).
+
+%% Servers MUST bind tokens to the mechanism selected by the client in its
+%% original request, and reject attempts to use them with other mechanisms.
+%% For example, if the client selected a mechanism capable of channel binding,
+%% an attempt to use a mechanism without channel binding MUST fail even if the
+%% token would otherwise be accepted by that mechanism.
+token_auth_fails_when_mechanism_does_not_match(Config) ->
+ #{token := Token, spec := Spec} = connect_and_ask_for_token(Config),
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ Config2 = [{ht_mech, another_mechanism(Mech)} | Config],
+ auth_with_token(failure, Token, Config2, Spec).
+
+%%--------------------------------------------------------------------
+%% helpers
+%%--------------------------------------------------------------------
+
+start_new_user_and_make_expired_token(Config) ->
+ Steps = [start_new_user],
+ #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config),
+ HostType = domain_helper:host_type(),
+ {LUser, LServer} = spec_to_lus(Spec),
+ AgentId = user_agent_id(),
+ Token = <<"verysecret">>,
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ ExpireTS = erlang:system_time(second) - 600, %% 10 minutes ago
+ %% Set almost expiring token into the new slot
+ Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, false],
+ ok = distributed_helper:rpc(distributed_helper:mim(), mod_fast_auth_token_backend, store_new_token, Args),
+ #{expired_token => Token, spec => Spec}.
+
+start_new_user_and_make_expired_token_in_the_current_slot(Config) ->
+ Now = erlang:system_time(second),
+ Steps = [start_new_user],
+ #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config),
+ HostType = domain_helper:host_type(),
+ {LUser, LServer} = spec_to_lus(Spec),
+ AgentId = user_agent_id(),
+ Token = <<"verysecret">>,
+ CurrentToken = <<"currentsecret">>,
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ ExpireTS = Now + 86400, %% 24 hours into the future
+ SetCurrent = #{
+ current_token => CurrentToken,
+ current_expire => Now - 600, %% 10 minutes ago
+ current_count => 0,
+ current_mech => Mech
+ },
+ %% Set almost expiring token into the new slot
+ Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent],
+ ok = distributed_helper:rpc(distributed_helper:mim(), mod_fast_auth_token_backend, store_new_token, Args),
+ #{new_token => Token, spec => Spec, old_token => CurrentToken}.
+
+connect_with_almost_expired_token(Config) ->
+ Steps = [start_new_user],
+ #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config),
+ HostType = domain_helper:host_type(),
+ {LUser, LServer} = spec_to_lus(Spec),
+ AgentId = user_agent_id(),
+ Token = <<"verysecret">>,
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ ExpireTS = erlang:system_time(second) + 600, %% 10 minutes into the future
+ %% Set almost expiring token into the new slot
+ Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, false],
+ ok = distributed_helper:rpc(distributed_helper:mim(), mod_fast_auth_token_backend, store_new_token, Args),
+ ConnectRes = auth_with_token(success, Token, Config, Spec),
+ #{token := NewToken} = parse_connect_result(ConnectRes),
+ ?assertNotEqual(Token, NewToken),
+ #{new_token => NewToken, spec => Spec, old_token => Token}.
+
+connect_with_almost_expired_token_in_the_current_slot(Config) ->
+ Now = erlang:system_time(second),
+ Steps = [start_new_user],
+ #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config),
+ HostType = domain_helper:host_type(),
+ {LUser, LServer} = spec_to_lus(Spec),
+ AgentId = user_agent_id(),
+ Token = <<"verysecret">>,
+ CurrentToken = <<"currentsecret">>,
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ ExpireTS = Now + 86400, %% 24 hours into the future
+ SetCurrent = #{
+ current_token => CurrentToken,
+ current_expire => Now + 600, %% 10 minutes into the future
+ current_count => 0,
+ current_mech => Mech
+ },
+ %% Set almost expiring token into the new slot
+ Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent],
+ ok = distributed_helper:rpc(distributed_helper:mim(), mod_fast_auth_token_backend, store_new_token, Args),
+ ConnectRes = auth_with_token(success, CurrentToken, Config, Spec),
+ #{token := NewToken} = parse_connect_result(ConnectRes),
+ ?assertNotEqual(Token, NewToken),
+ ?assertNotEqual(Token, CurrentToken),
+ #{new_token => NewToken, spec => Spec, old_token => CurrentToken}.
+
+connect_and_ask_for_token(Config) ->
+ Steps = [start_new_user, {?MODULE, auth_and_request_token},
+ receive_features],
+ ConnectRes = sasl2_helper:apply_steps(Steps, Config),
+ parse_connect_result(ConnectRes).
+
+parse_connect_result(#{answer := Success, spec := Spec}) ->
+ ?assertMatch(#xmlel{name = <<"success">>,
+ attrs = #{<<"xmlns">> := ?NS_SASL_2}}, Success),
+ Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]),
+ Expire = exml_query:attr(Fast, <<"expire">>),
+ Token = exml_query:attr(Fast, <<"token">>),
+ #{expire => Expire, token => Token, spec => Spec}.
+
+auth_and_request_token(Config, Client, Data) ->
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ Extra = [request_token(Mech), user_agent()],
+ auth_with_method(Config, Client, Data, [], Extra, <<"PLAIN">>).
+
+auth_using_token(Config, Client, Data) ->
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ Extra = [user_agent()],
+ auth_with_method(Config, Client, Data, [], Extra, Mech).
+
+auth_using_token_and_request_token(Config, Client, Data) ->
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ Extra = [request_token(Mech), user_agent()],
+ auth_with_method(Config, Client, Data, [], Extra, Mech).
+
+auth_using_token_and_request_invalidation(Config, Client, Data) ->
+ %% While XEP does not specify, what to do with another tokens,
+ %% we invalidate both new and current tokens.
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ Extra = [request_invalidation(), user_agent()],
+ auth_with_method(Config, Client, Data, [], Extra, Mech).
+
+auth_using_token_and_request_invalidation_1(Config, Client, Data) ->
+ Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>),
+ Extra = [request_invalidation_1(), user_agent()],
+ auth_with_method(Config, Client, Data, [], Extra, Mech).
+
+%%
+request_token(Mech) ->
+ #xmlel{name = <<"request-token">>,
+ attrs = #{<<"xmlns">> => ?NS_FAST,
+ <<"mechanism">> => Mech}}.
+
+%%
+request_invalidation() ->
+ #xmlel{name = <<"fast">>,
+ attrs = #{<<"xmlns">> => ?NS_FAST,
+ <<"invalidate">> => <<"true">>}}.
+
+%% or
+request_invalidation_1() ->
+ #xmlel{name = <<"fast">>,
+ attrs = #{<<"xmlns">> => ?NS_FAST,
+ <<"invalidate">> => <<"1">>}}.
+
+auth_with_token(Success, Token, Config, Spec) ->
+ auth_with_token(Success, Token, Config, Spec, dont_request_token).
+
+auth_with_token(Success, Token, Config, Spec, RequestToken) ->
+ Spec2 = [{secret_token, Token} | Spec],
+ Steps = steps(Success, auth_function(RequestToken)),
+ Data = #{spec => Spec2},
+ Res = sasl2_helper:apply_steps(Steps, Config, undefined, Data),
+ #{answer := Answer} = Res,
+ case Success of
+ success ->
+ ?assertMatch(#xmlel{name = <<"success">>,
+ attrs = #{<<"xmlns">> := ?NS_SASL_2}}, Answer);
+ failure ->
+ ?assertMatch(#xmlel{name = <<"failure">>,
+ attrs = #{<<"xmlns">> := ?NS_SASL_2},
+ children = [#xmlel{name = <<"not-authorized">>}]},
+ Answer)
+ end,
+ Res.
+
+auth_function(dont_request_token) ->
+ auth_using_token;
+auth_function(request_token) ->
+ auth_using_token_and_request_token;
+auth_function(request_invalidation) ->
+ auth_using_token_and_request_invalidation;
+auth_function(request_invalidation_1) ->
+ auth_using_token_and_request_invalidation_1.
+
+steps(success, AuthFun) ->
+ [connect_tls, start_stream_get_features,
+ {?MODULE, AuthFun},
+ receive_features,
+ has_no_more_stanzas];
+steps(failure, AuthFun) ->
+ [connect_tls, start_stream_get_features,
+ {?MODULE, AuthFun}].
+
+user_agent_id() ->
+ <<"d4565fa7-4d72-4749-b3d3-740edbf87770">>.
+
+user_agent() ->
+ #xmlel{name = <<"user-agent">>,
+ attrs = #{<<"id">> => user_agent_id()},
+ children = [cdata_elem(<<"software">>, <<"AwesomeXMPP">>),
+ cdata_elem(<<"device">>, <<"Kiva's Phone">>)]}.
+
+cdata_elem(Name, Value) ->
+ #xmlel{name = Name,
+ children = [#xmlcdata{content = Value}]}.
+
+%% See bind2_SUITE:plain_auth
+auth_with_method(_Config, Client, Data, BindElems, Extra, Method) ->
+ %% we need proof of posesion mechanism
+ InitEl = case Method of
+ <<"PLAIN">> ->
+ sasl2_helper:plain_auth_initial_response(Client);
+ <<"HT-", _/binary>> ->
+ ht_auth_initial_response(Client, Method)
+ end,
+ BindEl = #xmlel{name = <<"bind">>,
+ attrs = #{<<"xmlns">> => ?NS_BIND_2},
+ children = BindElems},
+ Authenticate = auth_elem(Method, [InitEl, BindEl | Extra]),
+ escalus:send(Client, Authenticate),
+ Answer = escalus_client:wait_for_stanza(Client),
+ ct:log("Answer ~p", [Answer]),
+ Identifier = exml_query:path(Answer, [{element, <<"authorization-identifier">>}, cdata]),
+ case Identifier of
+ undefined ->
+ {Client, Data#{answer => Answer}};
+ _ ->
+ #jid{lresource = LResource} = jid:from_binary(Identifier),
+ {Client, Data#{answer => Answer, client_1_jid => Identifier, bind2_resource => LResource}}
+ end.
+
+auth_elem(Mech, Children) ->
+ #xmlel{name = <<"authenticate">>,
+ attrs = #{<<"xmlns">> => ?NS_SASL_2, <<"mechanism">> => Mech},
+ children = Children}.
+
+%% Creates "Initiator First Message"
+%% https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html#section-3.1
+%%
+%% The HT mechanism starts with the initiator-msg, send by the initiator to the
+%% responder. The following lists the ABNF grammar for the initiator-msg:
+%%
+%% initiator-msg = authcid NUL initiator-hashed-token
+%% authcid = 1*SAFE ; MUST accept up to 255 octets
+%% initiator-hashed-token = 1*OCTET
+%%
+%% NUL = %0x00 ; The null octet
+%% SAFE = UTF8-encoded string
+ht_auth_initial_response(#client{props = Props}, Method) ->
+ %% authcid is the username before "@" sign.
+ Username = proplists:get_value(username, Props),
+ Token = proplists:get_value(secret_token, Props),
+ CBData = <<>>,
+ ToHash = <<"Initiator", CBData/binary>>,
+ Algo = mech_to_algo(Method),
+ InitiatorHashedToken = crypto:mac(hmac, Algo, Token, ToHash),
+ Payload = <>,
+ initial_response_elem(Payload).
+
+initial_response_elem(Payload) ->
+ Encoded = base64:encode(Payload),
+ #xmlel{name = <<"initial-response">>,
+ children = [#xmlcdata{content = Encoded}]}.
+
+spec_to_lus(Spec) ->
+ #{username := Username, server := Server} = maps:from_list(Spec),
+ jid:to_lus(jid:make_bare(Username, Server)).
diff --git a/big_tests/tests/gdpr_SUITE.erl b/big_tests/tests/gdpr_SUITE.erl
index 752d51a94ff..d19c11a4d87 100644
--- a/big_tests/tests/gdpr_SUITE.erl
+++ b/big_tests/tests/gdpr_SUITE.erl
@@ -963,7 +963,7 @@ retrieve_offline(Config) ->
#{ "packet" => [{contains, Body}],
"from" => binary_to_list(From),
"to" => binary_to_list(To),
- "timestamp" => [{validate, fun validate_datetime/1}]}
+ "timestamp" => [{validate, fun time_helper:validate_datetime/1}]}
end, Expected),
retrieve_and_validate_personal_data(
@@ -1777,28 +1777,6 @@ send_and_assert_is_chat_message(UserFrom, UserTo, Body) ->
Msg = escalus:wait_for_stanza(UserTo),
escalus:assert(is_chat_message, [Body], Msg).
-validate_datetime(TimeStr) ->
- [Date, Time] = string:tokens(TimeStr, "T"),
- validate_date(Date),
- validate_time(Time).
-
-validate_date(Date) ->
- [Y, M, D] = string:tokens(Date, "-"),
- Date1 = {list_to_integer(Y), list_to_integer(M), list_to_integer(D)},
- calendar:valid_date(Date1).
-
-validate_time(Time) ->
- [T | _] = string:tokens(Time, "Z"),
- validate_time1(T).
-
-
-validate_time1(Time) ->
- [H, M, S] = string:tokens(Time, ":"),
- check_list([{H, 24}, {M, 60}, {S, 60}]).
-
-check_list(List) ->
- lists:all(fun({V, L}) -> I = list_to_integer(V), I >= 0 andalso I < L end, List).
-
expected_header(mod_roster) -> ["jid", "name", "subscription",
"ask", "groups", "askmessage", "xs"].
diff --git a/big_tests/tests/sasl2_helper.erl b/big_tests/tests/sasl2_helper.erl
index 2a7376e7c29..5a669555d8f 100644
--- a/big_tests/tests/sasl2_helper.erl
+++ b/big_tests/tests/sasl2_helper.erl
@@ -21,9 +21,18 @@ load_all_sasl2_modules(HostType) ->
{mod_sasl2, default_mod_config(mod_sasl2)},
{mod_csi, default_mod_config(mod_csi)},
{mod_carboncopy, default_mod_config(mod_carboncopy)},
- {mod_stream_management, mod_config(mod_stream_management, SMOpts)}],
+ {mod_stream_management, mod_config(mod_stream_management, SMOpts)}]
+ ++ rdbms_mods(),
dynamic_modules:ensure_modules(HostType, Modules).
+rdbms_mods() ->
+ case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of
+ true ->
+ [{mod_fast_auth_token, mod_config(mod_fast_auth_token, #{backend => rdbms})}];
+ false ->
+ []
+ end.
+
apply_steps(Steps, Config) ->
apply_steps(Steps, Config, undefined, #{}).
diff --git a/doc/configuration/Modules.md b/doc/configuration/Modules.md
index 1b8ab7efdeb..a6ac1bffdcf 100644
--- a/doc/configuration/Modules.md
+++ b/doc/configuration/Modules.md
@@ -99,6 +99,9 @@ This applies to situations such as sending messages or presences to mobile/SMS/e
Implements [XEP-0215: External Service Discovery](http://xmpp.org/extensions/xep-0215.html) for discovering information about services external to the XMPP network.
The main use-case is to help discover STUN/TURN servers to allow for negotiating media exchanges.
+### [mod_fast_auth_token](../modules/mod_fast_auth_token.md)
+A module that implements [XEP-0484: Fast Authentication Streamlining Tokens](https://xmpp.org/extensions/xep-0484.html)..
+
### [mod_http_upload](../modules/mod_http_upload.md)
Implements [XEP-0363: HTTP File Upload](https://xmpp.org/extensions/xep-0363.html) for coordinating with an XMPP server to upload files via HTTP and receive URLs that can be shared in messages.
diff --git a/doc/modules/mod_fast_auth_token.md b/doc/modules/mod_fast_auth_token.md
new file mode 100644
index 00000000000..03f8959861c
--- /dev/null
+++ b/doc/modules/mod_fast_auth_token.md
@@ -0,0 +1,39 @@
+## Module Description
+
+This module implements [XEP-0484: Fast Authentication Streamlining Tokens](https://xmpp.org/extensions/xep-0484.html).
+It provides services necessary to:
+
+* issue auth tokens for authenticated users;
+* reconnect to the server using the tokens instead of the original auth method.
+
+Tokens are stored in RDBMS.
+
+It is not related to another similar module `mod_auth_token`.
+
+## Options
+
+### `modules.mod_fast_auth_token.backend`
+* **Syntax:** non-empty string
+* **Default:** `"rdbms"`
+* **Example:** `backend = "rdbms"`
+
+Token storage backend. Currently only `"rdbms"` is supported.
+
+### `modules.mod_fast_auth_token.validity_period`
+* **Syntax:** TOML table. Each key is either `access` or `rotate_before_expire`.Each value is a nested TOML table with the following mandatory keys: `value` (non-negative integer) and `unit` (`"days"`, `"hours"`, `"minutes"` or `"seconds"`).
+* **Default:** `{access = {value = 3, unit = "days"}, rotate_before_expire = {value = 6, unit = "hours"}}`
+* **Example:** `validity_period.access = {value = 30, unit = "minutes"}`
+
+The user can use each token for `access` period of time before it expired.
+
+The server would [send](https://xmpp.org/extensions/xep-0484.html#token-rotation)
+a new token at the login time `rotate_before_expire` time before it expires.
+Set it to 0 to disable automatic rotation.
+
+## Example configuration
+
+```toml
+[modules.mod_fast_auth_token]
+ validity_period.access = {value = 1, unit = "days"}
+ validity_period.rotate_before_expire = {value = 0, unit = "days"}
+```
diff --git a/include/mongoose_ns.hrl b/include/mongoose_ns.hrl
index ba3abd6ea1a..a98eca60171 100644
--- a/include/mongoose_ns.hrl
+++ b/include/mongoose_ns.hrl
@@ -85,6 +85,7 @@
-define(NS_SESSION, <<"urn:ietf:params:xml:ns:xmpp-session">>).
-define(NS_BIND, <<"urn:ietf:params:xml:ns:xmpp-bind">>).
-define(NS_BIND_2, <<"urn:xmpp:bind:0">>).
+-define(NS_FAST, <<"urn:xmpp:fast:0">>).
-define(NS_FEATURE_IQAUTH, <<"http://jabber.org/features/iq-auth">>).
-define(NS_FEATURE_IQREGISTER, <<"http://jabber.org/features/iq-register">>).
diff --git a/mkdocs.yml b/mkdocs.yml
index 2b5b7d7f44e..c125abf834b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -133,6 +133,7 @@ nav:
- 'RabbitMQ backend': 'modules/mod_event_pusher_rabbit.md'
- 'SNS backend': 'modules/mod_event_pusher_sns.md'
- 'mod_extdisco': 'modules/mod_extdisco.md'
+ - 'mod_fast_auth_token': 'modules/mod_fast_auth_token.md'
- 'mod_global_distrib': 'modules/mod_global_distrib.md'
- 'mod_http_upload': 'modules/mod_http_upload.md'
- 'mod_inbox': 'modules/mod_inbox.md'
diff --git a/priv/cockroachdb.sql b/priv/cockroachdb.sql
index fcb99c38dfe..ec52d1430bf 100644
--- a/priv/cockroachdb.sql
+++ b/priv/cockroachdb.sql
@@ -524,3 +524,23 @@ CREATE TABLE caps (
features text NOT NULL,
PRIMARY KEY (node, sub_node)
);
+
+-- XEP-0484: Fast Authentication Streamlining Tokens
+-- Module: mod_fast_auth_token
+CREATE TABLE fast_auth_token(
+ server VARCHAR(250) NOT NULL,
+ username VARCHAR(250) NOT NULL,
+ -- Device installation ID (User-Agent ID)
+ -- Unique for each device
+ -- https://xmpp.org/extensions/xep-0388.html#initiation
+ user_agent_id VARCHAR(250) NOT NULL,
+ current_token VARCHAR(250),
+ current_expire BIGINT, -- seconds unix timestamp
+ current_count INT, -- replay counter
+ current_mech_id smallint,
+ new_token VARCHAR(250),
+ new_expire BIGINT, -- seconds unix timestamp
+ new_count INT,
+ new_mech_id smallint,
+ PRIMARY KEY(server, username, user_agent_id)
+);
diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql
index e686afe1f4a..f35ef92351e 100644
--- a/priv/mssql2012.sql
+++ b/priv/mssql2012.sql
@@ -768,3 +768,23 @@ CREATE TABLE caps (
features text NOT NULL,
PRIMARY KEY (node, sub_node)
);
+
+-- XEP-0484: Fast Authentication Streamlining Tokens
+-- Module: mod_fast_auth_token
+CREATE TABLE fast_auth_token(
+ server VARCHAR(250) NOT NULL,
+ username VARCHAR(250) NOT NULL,
+ -- Device installation ID (User-Agent ID)
+ -- Unique for each device
+ -- https://xmpp.org/extensions/xep-0388.html#initiation
+ user_agent_id VARCHAR(250) NOT NULL,
+ current_token VARCHAR(250),
+ current_expire BIGINT, -- seconds unix timestamp
+ current_count INT, -- replay counter
+ current_mech_id TINYINT,
+ new_token VARCHAR(250),
+ new_expire BIGINT, -- seconds unix timestamp
+ new_count INT,
+ new_mech_id TINYINT,
+ PRIMARY KEY(server, username, user_agent_id)
+);
diff --git a/priv/mysql.sql b/priv/mysql.sql
index 664b5bae0a1..e53e70a3f3c 100644
--- a/priv/mysql.sql
+++ b/priv/mysql.sql
@@ -557,3 +557,23 @@ CREATE TABLE caps (
features text NOT NULL,
PRIMARY KEY (node, sub_node)
);
+
+-- XEP-0484: Fast Authentication Streamlining Tokens
+-- Module: mod_fast_auth_token
+CREATE TABLE fast_auth_token(
+ server VARCHAR(250) NOT NULL,
+ username VARCHAR(250) NOT NULL,
+ -- Device installation ID (User-Agent ID)
+ -- Unique for each device
+ -- https://xmpp.org/extensions/xep-0388.html#initiation
+ user_agent_id VARCHAR(250) NOT NULL,
+ current_token VARCHAR(250),
+ current_expire BIGINT, -- seconds unix timestamp
+ current_count INT, -- replay counter
+ current_mech_id TINYINT UNSIGNED,
+ new_token VARCHAR(250),
+ new_expire BIGINT, -- seconds unix timestamp
+ new_count INT,
+ new_mech_id TINYINT UNSIGNED,
+ PRIMARY KEY(server, username, user_agent_id)
+);
diff --git a/priv/pg.sql b/priv/pg.sql
index 66874f4eff5..9de3017fb06 100644
--- a/priv/pg.sql
+++ b/priv/pg.sql
@@ -499,3 +499,23 @@ CREATE TABLE caps (
features text NOT NULL,
PRIMARY KEY (node, sub_node)
);
+
+-- XEP-0484: Fast Authentication Streamlining Tokens
+-- Module: mod_fast_auth_token
+CREATE TABLE fast_auth_token(
+ server VARCHAR(250) NOT NULL,
+ username VARCHAR(250) NOT NULL,
+ -- Device installation ID (User-Agent ID)
+ -- Unique for each device
+ -- https://xmpp.org/extensions/xep-0388.html#initiation
+ user_agent_id VARCHAR(250) NOT NULL,
+ current_token VARCHAR(250),
+ current_expire BIGINT, -- seconds unix timestamp
+ current_count INT, -- replay counter
+ current_mech_id smallint,
+ new_token VARCHAR(250),
+ new_expire BIGINT, -- seconds unix timestamp
+ new_count INT,
+ new_mech_id smallint,
+ PRIMARY KEY(server, username, user_agent_id)
+);
diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl
index 5395d6cf026..c9c4671336e 100644
--- a/src/c2s/mongoose_c2s.erl
+++ b/src/c2s/mongoose_c2s.erl
@@ -20,7 +20,8 @@
get_info/1, set_info/2,
get_mod_state/2, get_listener_opts/1, merge_mod_state/2, remove_mod_state/2,
get_ip/1, get_socket/1, get_lang/1, get_stream_id/1, hook_arg/5]).
--export([get_auth_mechs/1, c2s_stream_error/2, maybe_retry_state/1, merge_states/2]).
+-export([get_auth_mechs/1, get_auth_mechs_to_announce/1,
+ c2s_stream_error/2, maybe_retry_state/1, merge_states/2]).
-export([route/2, reroute_buffer/2, reroute_buffer_to_pid/3, open_session/1]).
-export([set_jid/2, set_auth_module/2, state_timeout/1, handle_state_after_packet/3]).
-export([replace_resource/2, generate_random_resource/0]).
@@ -1158,6 +1159,16 @@ create_data(#{host_type := HostType, jid := Jid}) ->
get_auth_mechs(#c2s_data{host_type = HostType} = StateData) ->
[M || M <- cyrsasl:listmech(HostType), filter_mechanism(StateData, M)].
+%% Mechanisms without XEP-0484 token mechanisms
+%% (HT mechanisms are announced as inlined instead)
+-spec get_auth_mechs_to_announce(data()) -> [mongoose_c2s_sasl:mechanism()].
+get_auth_mechs_to_announce(StateData) ->
+ [M || M <- get_auth_mechs(StateData), not skip_announce_mechanism(M)].
+
+-spec skip_announce_mechanism(binary()) -> boolean().
+skip_announce_mechanism(Mech) ->
+ mod_fast_auth_token_generic_mech:skip_announce_mechanism(Mech).
+
-spec filter_mechanism(data(), binary()) -> boolean().
filter_mechanism(#c2s_data{socket = Socket}, <<"SCRAM-SHA-1-PLUS">>) ->
mongoose_c2s_socket:is_channel_binding_supported(Socket);
diff --git a/src/c2s/mongoose_c2s_sasl.erl b/src/c2s/mongoose_c2s_sasl.erl
index 65db1674ce0..4542214808e 100644
--- a/src/c2s/mongoose_c2s_sasl.erl
+++ b/src/c2s/mongoose_c2s_sasl.erl
@@ -18,7 +18,8 @@
-type maybe_username() :: undefined | jid:luser().
-type success() :: #{server_out := undefined | binary(),
jid := jid:jid(),
- auth_module := cyrsasl:sasl_module()}.
+ auth_module := cyrsasl:sasl_module(),
+ creds := mongoose_credentials:t()}.
-type continue() :: #{server_out := binary()}.
-type failure() :: #{server_out := binary() | {binary(), undefined | iodata()},
maybe_username := maybe_username()}.
@@ -48,7 +49,10 @@ start(C2SData, SaslAcc, Mech, ClientIn) ->
{error, SaslAcc, #{type => policy_violation, text => <<"Use of STARTTLS required">>}};
_ ->
AuthMech = mongoose_c2s:get_auth_mechs(C2SData),
- SocketData = #{socket => Socket, auth_mech => AuthMech, listener_opts => LOpts},
+ %% Provide SaslAcc for readonly access, so the cyrsasl mechanism
+ %% has more visibility to initialize the mechanism state.
+ SocketData = #{socket => Socket, auth_mech => AuthMech, listener_opts => LOpts,
+ sasl_state => SaslAcc},
CyrSaslState = get_cyrsasl_state_from_acc(SaslAcc),
CyrSaslResult = cyrsasl:server_start(CyrSaslState, Mech, ClientIn, SocketData),
handle_sasl_step(C2SData, CyrSaslResult, SaslAcc)
@@ -78,7 +82,7 @@ handle_sasl_success(C2SData, Creds, SaslAcc) ->
User = mongoose_credentials:get(Creds, username),
LServer = mongoose_c2s:get_lserver(C2SData),
Jid = jid:make_bare(User, LServer),
- Ret = #{server_out => ServerOut, jid => Jid, auth_module => AuthModule},
+ Ret = #{server_out => ServerOut, jid => Jid, auth_module => AuthModule, creds => Creds},
{success, SaslAcc, Ret}.
-spec handle_sasl_continue(
diff --git a/src/c2s/mongoose_c2s_stanzas.erl b/src/c2s/mongoose_c2s_stanzas.erl
index aa1f8f9b91e..fbf7c1a1642 100644
--- a/src/c2s/mongoose_c2s_stanzas.erl
+++ b/src/c2s/mongoose_c2s_stanzas.erl
@@ -71,7 +71,7 @@ determine_features(StateData, HostType, LServer, _, _) ->
-spec maybe_sasl_mechanisms(mongoose_c2s:data()) -> [exml:element()].
maybe_sasl_mechanisms(StateData) ->
- case mongoose_c2s:get_auth_mechs(StateData) of
+ case mongoose_c2s:get_auth_mechs_to_announce(StateData) of
[] -> [];
Mechanisms ->
[#xmlel{name = <<"mechanisms">>,
diff --git a/src/config/mongoose_config_spec.erl b/src/config/mongoose_config_spec.erl
index 3843d273044..0502cc37c18 100644
--- a/src/config/mongoose_config_spec.erl
+++ b/src/config/mongoose_config_spec.erl
@@ -32,6 +32,10 @@
process_domain_cert/1,
process_infinity_as_zero/1]).
+%% For tests
+-export([configurable_modules/0]).
+-ignore_xref([configurable_modules/0]).
+
-include("mongoose_config_spec.hrl").
-type config_node() :: config_section() | config_list() | config_option().
@@ -753,6 +757,7 @@ configurable_modules() ->
mod_disco,
mod_event_pusher,
mod_extdisco,
+ mod_fast_auth_token,
mod_global_distrib,
mod_http_upload,
mod_inbox,
diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl
new file mode 100644
index 00000000000..ffb91528be2
--- /dev/null
+++ b/src/fast_auth_token/mod_fast_auth_token.erl
@@ -0,0 +1,405 @@
+-module(mod_fast_auth_token).
+-xep([{xep, 484}, {version, "0.2.0"}]).
+-behaviour(gen_mod).
+-include("mongoose_ns.hrl").
+-include("mongoose.hrl").
+-include("jlib.hrl").
+-include("mongoose_config_spec.hrl").
+
+%% `gen_mod' callbacks
+-export([start/2,
+ stop/1,
+ hooks/1,
+ config_spec/0,
+ supported_features/0]).
+
+%% hooks handlers
+-export([sasl2_stream_features/3,
+ sasl2_start/3,
+ sasl2_success/3,
+ remove_user/3,
+ remove_domain/3]).
+
+-export([read_tokens/4]).
+
+%% For mocking
+-export([utc_now_as_seconds/0,
+ generate_unique_token/0]).
+
+-type seconds() :: integer().
+-type counter() :: non_neg_integer().
+%% Base64 encoded token
+-type token() :: binary().
+-type agent_id() :: binary().
+-type mechanism() :: binary().
+
+-type validity_type() :: days | hours | minutes | seconds.
+-type period() :: #{value := non_neg_integer(),
+ unit := days | hours | minutes | seconds}.
+-type token_type() :: access | rotate_before_expire.
+-type token_slot() :: new | current.
+-type add_reason() :: requested | auto_rotate.
+
+-define(REQ, mod_fast_auth_token_request).
+-define(FAST, mod_fast_auth_token_fast).
+
+-export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0,
+ mechanism/0, token_slot/0, set_current/0]).
+
+-type tokens_data() :: #{
+ now_timestamp := seconds(),
+ current_token := token() | undefined,
+ current_expire := seconds() | undefined,
+ current_count := counter() | undefined,
+ current_mech := mechanism() | undefined,
+ new_token := token() | undefined,
+ new_expire := seconds() | undefined,
+ new_count := counter() | undefined,
+ new_mech := mechanism() | undefined
+ }.
+
+-type set_current() :: #{
+ current_token := token(),
+ current_expire := seconds(),
+ current_count := counter(),
+ current_mech := mechanism()
+ }.
+
+-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
+start(HostType, Opts) ->
+ mod_fast_auth_token_backend:init(HostType, Opts),
+ ok.
+
+-spec stop(mongooseim:host_type()) -> ok.
+stop(_HostType) ->
+ ok.
+
+-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
+hooks(HostType) ->
+ [{sasl2_stream_features, HostType, fun ?MODULE:sasl2_stream_features/3, #{}, 50},
+ {sasl2_start, HostType, fun ?MODULE:sasl2_start/3, #{}, 50},
+ {sasl2_success, HostType, fun ?MODULE:sasl2_success/3, #{}, 50},
+ {remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50},
+ {remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50}].
+
+-spec config_spec() -> mongoose_config_spec:config_section().
+config_spec() ->
+ #section{
+ items = #{<<"backend">> => #option{type = atom,
+ validate = {module, ?MODULE}},
+ <<"validity_period">> => validity_periods_spec()},
+ defaults = #{<<"backend">> => rdbms}
+ }.
+
+validity_periods_spec() ->
+ #section{
+ items = #{<<"access">> => validity_period_spec(),
+ <<"rotate_before_expire">> => validity_period_spec()},
+ defaults = #{<<"access">> => #{value => 3, unit => days},
+ <<"rotate_before_expire">> => #{value => 6, unit => hours}},
+ include = always
+ }.
+
+validity_period_spec() ->
+ #section{
+ items = #{<<"value">> => #option{type = integer,
+ validate = non_negative},
+ <<"unit">> => #option{type = atom,
+ validate = {enum, [days, hours, minutes, seconds]}}
+ },
+ required = all
+ }.
+
+-spec supported_features() -> [atom()].
+supported_features() -> [dynamic_domains].
+
+-spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
+ Acc :: mongoose_acc:t(),
+ Params :: map(),
+ Extra :: gen_hook:extra().
+remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type := HostType}) ->
+ mod_fast_auth_token_backend:remove_user(HostType, LUser, LServer),
+ {ok, Acc}.
+
+-spec remove_domain(Acc, Params, Extra) -> {ok, Acc} when
+ Acc :: mongoose_domain_api:remove_domain_acc(),
+ Params :: map(),
+ Extra :: gen_hook:extra().
+remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) ->
+ mod_fast_auth_token_backend:remove_domain(HostType, Domain),
+ {ok, Acc}.
+
+-spec sasl2_stream_features(Acc, #{c2s_data := mongoose_c2s:data()}, gen_hook:extra()) ->
+ {ok, Acc} when Acc :: [exml:element()].
+sasl2_stream_features(Acc, _, _) ->
+ {ok, [fast() | Acc]}.
+
+fast() ->
+ #xmlel{name = <<"fast">>,
+ attrs = #{<<"xmlns">> => ?NS_FAST},
+ children = mechanisms_elems(mechanisms())}.
+
+mechanisms_elems(Mechs) ->
+ [#xmlel{name = <<"mechanism">>,
+ children = [#xmlcdata{content = Mech}]} || Mech <- Mechs].
+
+-spec mechanisms() -> [mechanism()].
+mechanisms() ->
+ mod_fast_auth_token_generic_mech:mechanisms().
+
+-spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) ->
+ {ok, SaslAcc} when SaslAcc :: mongoose_acc:t().
+sasl2_start(SaslAcc, #{stanza := El}, _) ->
+ Req = exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}]),
+ Fast = exml_query:path(El, [{element_with_ns, <<"fast">>, ?NS_FAST}]),
+ AgentId = exml_query:path(El, [{element, <<"user-agent">>}, {attr, <<"id">>}]),
+ SaslAcc2 = mongoose_acc:set(?MODULE, agent_id, AgentId, SaslAcc),
+ SaslAcc3 = maybe_put_inline_request(SaslAcc2, ?REQ, Req),
+ {ok, maybe_put_inline_request(SaslAcc3, ?FAST, Fast)}.
+
+maybe_put_inline_request(SaslAcc, _Module, undefined) ->
+ SaslAcc;
+maybe_put_inline_request(SaslAcc, Module, Request) ->
+ mod_sasl2:put_inline_request(SaslAcc, Module, Request).
+
+-spec sasl2_success(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) ->
+ {ok, SaslAcc} when SaslAcc :: mongoose_acc:t().
+sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType}) ->
+ #{c2s_data := C2SData} = C2SStateData,
+ #jid{luser = LUser, lserver = LServer} = mongoose_c2s:get_jid(C2SData),
+ case check_if_should_add_token(HostType, SaslAcc, Creds) of
+ skip ->
+ {ok, SaslAcc};
+ invalidate ->
+ AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc),
+ invalidate_token(HostType, LServer, LUser, AgentId),
+ {ok, SaslAcc};
+ {ok, Mech, AddReason} ->
+ AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc),
+ %% Attach Token to the response to be used to authentificate
+ Response = make_fast_token_response(HostType, LServer, LUser, Mech, AgentId, Creds),
+ SaslAcc2 = maybe_init_inline_request(AddReason, SaslAcc),
+ SaslAcc3 = mod_sasl2:update_inline_request(SaslAcc2, ?REQ, Response, success),
+ {ok, SaslAcc3}
+ end.
+
+-spec check_if_should_add_token(HostType :: mongooseim:host_type(),
+ SaslAcc :: mongoose_acc:t(),
+ Creds :: mongoose_credentials:t()) ->
+ skip | invalidate | {ok, mechanism(), Reason :: add_reason()}.
+check_if_should_add_token(HostType, SaslAcc, Creds) ->
+ Parsed = parse_inline_requests(SaslAcc),
+ case Parsed of
+ #{invalidate := true} ->
+ invalidate;
+ #{mech := Mech} ->
+ case lists:member(Mech, mechanisms()) of
+ true ->
+ {ok, Mech, requested};
+ false ->
+ skip
+ end;
+ #{} ->
+ maybe_auto_rotate(HostType, Creds)
+ end.
+
+-spec parse_inline_requests(SaslAcc :: mongoose_acc:t()) -> map().
+parse_inline_requests(SaslAcc) ->
+ Req = mod_sasl2:get_inline_request(SaslAcc, ?REQ, undefined),
+ Fast = mod_sasl2:get_inline_request(SaslAcc, ?FAST, undefined),
+ map_skip_undefined(maps:merge(parse_request(Req), parse_fast(Fast))).
+
+-spec map_skip_undefined(map()) -> map().
+map_skip_undefined(Map) ->
+ maps:filter(fun(_, Val) -> Val =/= undefined end, Map).
+
+parse_request(#{request := Req = #xmlel{name = <<"request-token">>}}) ->
+ Mech = exml_query:attr(Req, <<"mechanism">>),
+ #{mech => Mech};
+parse_request(undefined) ->
+ #{}.
+
+parse_fast(#{request := Fast = #xmlel{name = <<"fast">>}}) ->
+ Inv = is_true(exml_query:attr(Fast, <<"invalidate">>)),
+ Count = exml_query:attr(Fast, <<"count">>),
+ #{invalidate => Inv, count => maybe_parse_integer(Count)};
+parse_fast(undefined) ->
+ #{}.
+
+is_true(<<"true">>) -> true;
+is_true(<<"1">>) -> true;
+is_true(_) -> false.
+
+maybe_parse_integer(X) when is_binary(X) ->
+ binary_to_integer(X);
+maybe_parse_integer(undefined) ->
+ undefined.
+
+-spec maybe_auto_rotate(HostType :: mongooseim:host_type(),
+ Creds :: mongoose_credentials:t()) ->
+ skip | {ok, mechanism(), Reason :: add_reason()}.
+maybe_auto_rotate(HostType, Creds) ->
+ %% Creds could contain data from mod_fast_auth_token_generic_mech
+ SlotUsed = mongoose_credentials:get(Creds, fast_token_slot_used, undefined),
+ DataUsed = mongoose_credentials:get(Creds, fast_token_data, undefined),
+ case user_used_token_to_login(SlotUsed) of
+ true ->
+ case is_used_token_about_to_expire(HostType, SlotUsed, DataUsed) of
+ true ->
+ {ok, data_used_to_mech_type(SlotUsed, DataUsed), auto_rotate};
+ false ->
+ skip
+ end;
+ false ->
+ skip
+ end.
+
+-spec is_used_token_about_to_expire(HostType :: mongooseim:host_type(),
+ SlotUsed :: token_slot(),
+ DataUsed :: tokens_data()) -> boolean().
+is_used_token_about_to_expire(HostType, SlotUsed, DataUsed) ->
+ is_timestamp_about_to_expire(HostType,
+ slot_to_expire_timestamp(SlotUsed, DataUsed)).
+
+-spec is_timestamp_about_to_expire(HostType :: mongooseim:host_type(),
+ Timestamp :: seconds()) -> boolean().
+is_timestamp_about_to_expire(HostType, Timestamp) ->
+ Now = utc_now_as_seconds(),
+ TimeBeforeRotate = get_time_to_rotate_before_expire_seconds(HostType),
+ SecondsBeforeExpire = Timestamp - Now,
+ SecondsBeforeExpire =< TimeBeforeRotate.
+
+-spec user_used_token_to_login(token_slot() | undefined) -> boolean().
+user_used_token_to_login(SlotUsed) ->
+ undefined =/= SlotUsed.
+
+-spec slot_to_expire_timestamp(Slot :: token_slot(), Data :: tokens_data()) -> seconds().
+slot_to_expire_timestamp(new, #{new_expire := Timestamp}) ->
+ Timestamp;
+slot_to_expire_timestamp(current, #{current_expire := Timestamp}) ->
+ Timestamp.
+
+-spec data_used_to_mech_type(SlotUsed :: token_slot(),
+ DataUsed :: tokens_data()) -> Mech :: mechanism().
+data_used_to_mech_type(new, #{new_mech := Mech}) ->
+ Mech;
+data_used_to_mech_type(current, #{current_mech := Mech}) ->
+ Mech.
+
+-spec maybe_init_inline_request(AddReason, SaslAcc) -> SaslAcc
+ when AddReason :: add_reason(),
+ SaslAcc :: mongoose_acc:t().
+maybe_init_inline_request(requested, SaslAcc) ->
+ SaslAcc;
+maybe_init_inline_request(auto_rotate, SaslAcc) ->
+ %% Add something, so update_inline_request would actually attach data
+ mod_sasl2:put_inline_request(SaslAcc, ?REQ, #xmlel{name = <<"auto">>}).
+
+%% Generate expirable auth token and store it in DB
+-spec make_fast_token_response(HostType, LServer, LUser, Mech, AgentId, Creds) -> exml:element()
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: agent_id(),
+ Mech :: mechanism(),
+ Creds :: mongoose_credentials:t().
+make_fast_token_response(HostType, LServer, LUser, Mech, AgentId, Creds) ->
+ TTLSeconds = get_ttl_seconds(HostType),
+ NowTS = ?MODULE:utc_now_as_seconds(),
+ ExpireTS = NowTS + TTLSeconds,
+ Expire = seconds_to_binary(ExpireTS),
+ Token = ?MODULE:generate_unique_token(),
+ SetCurrent = maybe_set_current_slot(Creds),
+ store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent),
+ #xmlel{name = <<"token">>,
+ attrs = #{<<"xmlns">> => ?NS_FAST, <<"expire">> => Expire,
+ <<"token">> => Token}}.
+
+-spec maybe_set_current_slot(Creds :: mongoose_credentials:t()) ->
+ SetCurrent :: set_current().
+maybe_set_current_slot(Creds) ->
+ %% Creds could contain data from mod_fast_auth_token_generic_mech
+ SlotUsed = mongoose_credentials:get(Creds, fast_token_slot_used, undefined),
+ DataUsed = mongoose_credentials:get(Creds, fast_token_data, undefined),
+ case SlotUsed of
+ new ->
+ token_data_to_set_current(DataUsed);
+ _ ->
+ false
+ end.
+
+-spec token_data_to_set_current(DataUsed :: tokens_data()) -> set_current().
+token_data_to_set_current(#{
+ new_token := Token,
+ new_expire := Expire,
+ new_count := Counter,
+ new_mech := Mech}) ->
+ #{current_token => Token,
+ current_expire => Expire,
+ current_count => Counter,
+ current_mech => Mech}.
+
+-spec seconds_to_binary(seconds()) -> binary().
+seconds_to_binary(Secs) ->
+ Opts = [{offset, "Z"}, {unit, second}],
+ list_to_binary(calendar:system_time_to_rfc3339(Secs, Opts)).
+
+-spec utc_now_as_seconds() -> seconds().
+utc_now_as_seconds() ->
+ erlang:system_time(second).
+
+-spec get_ttl_seconds(mongooseim:host_type()) -> seconds().
+get_ttl_seconds(HostType) ->
+ #{value := Value, unit := Unit} = get_validity_period(HostType, access),
+ period_to_seconds(Value, Unit).
+
+-spec get_time_to_rotate_before_expire_seconds(mongooseim:host_type()) -> seconds().
+get_time_to_rotate_before_expire_seconds(HostType) ->
+ #{value := Value, unit := Unit} = get_validity_period(HostType, rotate_before_expire),
+ period_to_seconds(Value, Unit).
+
+-spec get_validity_period(mongooseim:host_type(), token_type()) -> period().
+get_validity_period(HostType, Type) ->
+ gen_mod:get_module_opt(HostType, ?MODULE, [validity_period, Type]).
+
+-spec period_to_seconds(non_neg_integer(), validity_type()) -> seconds().
+period_to_seconds(Days, days) -> 24 * 3600 * Days;
+period_to_seconds(Hours, hours) -> 3600 * Hours;
+period_to_seconds(Minutes, minutes) -> 60 * Minutes;
+period_to_seconds(Seconds, seconds) -> Seconds.
+
+-spec generate_unique_token() -> token().
+generate_unique_token() ->
+ base64:encode(crypto:strong_rand_bytes(25)).
+
+-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS,
+ Token, Mech, SetCurrent) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: agent_id(),
+ ExpireTS :: seconds(),
+ Token :: token(),
+ Mech :: mechanism(),
+ SetCurrent :: set_current() | false.
+store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent) ->
+ mod_fast_auth_token_backend:store_new_token(
+ HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent).
+
+-spec invalidate_token(HostType, LServer, LUser, AgentId) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: agent_id().
+invalidate_token(HostType, LServer, LUser, AgentId) ->
+ mod_fast_auth_token_backend:invalidate_token(HostType, LServer, LUser, AgentId),
+ ok.
+
+-spec read_tokens(HostType, LServer, LUser, AgentId) ->
+ {ok, tokens_data()} | {error, not_found}
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: agent_id().
+read_tokens(HostType, LServer, LUser, AgentId) ->
+ mod_fast_auth_token_backend:read_tokens(HostType, LServer, LUser, AgentId).
diff --git a/src/fast_auth_token/mod_fast_auth_token_backend.erl b/src/fast_auth_token/mod_fast_auth_token_backend.erl
new file mode 100644
index 00000000000..111c9c9e880
--- /dev/null
+++ b/src/fast_auth_token/mod_fast_auth_token_backend.erl
@@ -0,0 +1,97 @@
+-module(mod_fast_auth_token_backend).
+
+-export([init/2,
+ store_new_token/8,
+ read_tokens/4,
+ invalidate_token/4,
+ remove_user/3,
+ remove_domain/2]).
+
+-define(MAIN_MODULE, mod_fast_auth_token).
+
+-callback init(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
+
+-callback store_new_token(HostType, LServer, LUser, AgentId, ExpireTS,
+ Token, Mech, SetCurrent) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id(),
+ ExpireTS :: mod_fast_auth_token:seconds(),
+ Token :: mod_fast_auth_token:token(),
+ Mech :: mod_fast_auth_token:mechanism(),
+ SetCurrent :: mod_fast_auth_token:set_current() | false.
+
+-callback read_tokens(HostType, LServer, LUser, AgentId) ->
+ {ok, mod_fast_auth_token:tokens_data()} | {error, not_found}
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id().
+
+-callback invalidate_token(HostType, LServer, LUser, AgentId) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id().
+
+-callback remove_user(mongooseim:host_type(), jid:luser(), jid:lserver()) -> ok.
+
+-callback remove_domain(mongooseim:host_type(), jid:lserver()) -> ok.
+
+-optional_callbacks([remove_domain/2]).
+
+-spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
+init(HostType, Opts) ->
+ Tracked = [store_new_token, read_tokens, invalidate_token],
+ mongoose_backend:init(HostType, ?MAIN_MODULE, Tracked, Opts),
+ Args = [HostType, Opts],
+ mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).
+
+-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS,
+ Token, Mech, SetCurrent) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id(),
+ ExpireTS :: mod_fast_auth_token:seconds(),
+ Token :: mod_fast_auth_token:token(),
+ Mech :: mod_fast_auth_token:mechanism(),
+ SetCurrent :: mod_fast_auth_token:set_current() | false.
+store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent) ->
+ Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent],
+ mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).
+
+-spec read_tokens(HostType, LServer, LUser, AgentId) ->
+ {ok, mod_fast_auth_token:tokens_data()} | {error, not_found}
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id().
+read_tokens(HostType, LServer, LUser, AgentId) ->
+ Args = [HostType, LServer, LUser, AgentId],
+ mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).
+
+-spec invalidate_token(HostType, LServer, LUser, AgentId) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id().
+invalidate_token(HostType, LServer, LUser, AgentId) ->
+ Args = [HostType, LServer, LUser, AgentId],
+ mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).
+
+-spec remove_user(mongooseim:host_type(), jid:luser(), jid:lserver()) -> ok.
+remove_user(HostType, LUser, LServer) ->
+ Args = [HostType, LUser, LServer],
+ mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).
+
+-spec remove_domain(mongooseim:host_type(), jid:lserver()) -> ok.
+remove_domain(HostType, LServer) ->
+ Args = [HostType, LServer],
+ case mongoose_backend:is_exported(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, 2) of
+ true ->
+ mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args);
+ false ->
+ ok
+ end.
diff --git a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl
new file mode 100644
index 00000000000..04475613469
--- /dev/null
+++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl
@@ -0,0 +1,200 @@
+-module(mod_fast_auth_token_generic_mech).
+
+-export([mech_new/4, mech_step/2]).
+%% Called from mongoose_c2s
+-export([skip_announce_mechanism/1]).
+%% Called from mod_fast_auth_token
+-export([mechanisms/0]).
+%% Called from mod_fast_auth_token_rdbms
+-export([mech_id/1, mech_name/1]).
+%% Called from cyrsasl
+-export([supports_sasl_module/2, sasl_modules/0]).
+
+-record(fast_info, {
+ creds :: mongoose_credentials:t(),
+ agent_id :: mod_fast_auth_token:agent_id(),
+ mechanism :: mod_fast_auth_token:mechanism()
+ }).
+-include("mongoose.hrl").
+-type fast_info() :: #fast_info{}.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term(),
+ Mech :: mod_fast_auth_token:mechanism()) -> {ok, fast_info()} | {error, binary()}.
+mech_new(_Host, Creds, _SocketData = #{sasl_state := SaslState}, Mech) ->
+ SaslModState = mod_sasl2:get_mod_state(SaslState),
+ case SaslModState of
+ #{encoded_id := AgentId} ->
+ {ok, #fast_info{creds = Creds, agent_id = AgentId, mechanism = Mech}};
+ _ ->
+ {error, <<"not-sasl2">>}
+ end;
+mech_new(_Host, _Creds, _SocketData, _Mech) ->
+ {error, <<"not-sasl2">>}.
+
+-spec mech_step(State :: fast_info(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(#fast_info{creds = Creds, agent_id = AgentId, mechanism = Mech}, SerializedToken) ->
+ %% SerializedToken is base64 decoded.
+ Parts = binary:split(SerializedToken, <<0>>),
+ [Username, InitiatorHashedToken] = Parts,
+ HostType = mongoose_credentials:host_type(Creds),
+ LServer = mongoose_credentials:lserver(Creds),
+ LUser = jid:nodeprep(Username),
+ case mod_fast_auth_token:read_tokens(HostType, LServer, LUser, AgentId) of
+ {ok, TokenData} ->
+ CBData = <<>>,
+ case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of
+ {true, TokenSlot} ->
+ {ok, mongoose_credentials:extend(Creds,
+ [{username, LUser},
+ {auth_module, ?MODULE},
+ {fast_token_slot_used, TokenSlot},
+ {fast_token_data, TokenData}])};
+ false ->
+ {error, <<"not-authorized">>}
+ end;
+ {error, _Reason} ->
+ {error, <<"not-authorized">>}
+ end.
+
+
+%% For every client using FAST, have two token slots - 'current' and 'new'.
+%% Whenever generating a new token, always place it into the 'new' slot.
+%% During authentication, first check against the token
+%% in the 'new' slot (if any).
+%% If successful, move the token from the 'new' slot to the 'current' slot
+%% (overwrite any existing token in that slot).
+%%
+%% If the client's provided token does not match the token in the 'new' slot,
+%% or if the 'new' slot is empty, compare against the token
+%% in the 'current' slot (if any).
+-spec handle_auth(Data :: mod_fast_auth_token:tokens_data(),
+ InitiatorHashedToken :: binary(),
+ CBData :: binary(),
+ Mech :: mod_fast_auth_token:mechanism()) ->
+ {true, Slot :: mod_fast_auth_token:token_slot()} | false.
+handle_auth(#{
+ now_timestamp := NowTimestamp,
+ current_token := CurrentToken,
+ current_expire := CurrentExpire,
+ current_count := CurrentCount,
+ current_mech := CurrentMech,
+ new_token := NewToken,
+ new_expire := NewExpire,
+ new_count := NewCount,
+ new_mech := NewMech
+ }, InitiatorHashedToken, CBData, Mech) ->
+ ToHash = <<"Initiator", CBData/binary>>,
+ TokenNew = {NewToken, NewExpire, NewCount, NewMech},
+ TokenCur = {CurrentToken, CurrentExpire, CurrentCount, CurrentMech},
+ Shared = {NowTimestamp, ToHash, InitiatorHashedToken, Mech},
+ case check_token(TokenNew, Shared) of
+ true ->
+ {true, new};
+ false ->
+ case check_token(TokenCur, Shared) of
+ true ->
+ {true, current};
+ false ->
+ false
+ end
+ end.
+
+%% Mech of the token in DB should match the mech the client is using.
+check_token({Token, Expire, _Count, Mech},
+ {NowTimestamp, ToHash, InitiatorHashedToken, Mech})
+ when is_binary(Token), Expire > NowTimestamp ->
+ Algo = mech_to_algo(Mech),
+ ComputedToken = crypto:mac(hmac, Algo, Token, ToHash),
+ %% To be theoretically safe against timing attacks (attacks that measure
+ %% the time it take to compare to binaries to guess how many bytes were
+ %% guessed correctly when the comparison is executed byte-by-byte and
+ %% shortcircuit upon the first difference)
+ crypto:hash_equals(ComputedToken, InitiatorHashedToken);
+check_token(_, _) ->
+ false.
+
+%% List:
+%% https://www.iana.org/assignments/named-information/named-information.xhtml#hash-alg
+%% 1 sha-256 256 bits [RFC6920] current
+%% 2 sha-256-128 128 bits [RFC6920] current
+%% 3 sha-256-120 120 bits [RFC6920] current
+%% 4 sha-256-96 96 bits [RFC6920] current
+%% 5 sha-256-64 64 bits [RFC6920] current
+%% 6 sha-256-32 32 bits [RFC6920] current
+%% 7 sha-384 384 bits [FIPS 180-4] current
+%% 8 sha-512 512 bits [FIPS 180-4] current
+%% 9 sha3-224 224 bits [FIPS 202] current
+%% 10 sha3-256 256 bits [FIPS 202] current
+%% 11 sha3-384 384 bits [FIPS 202] current
+%% 12 sha3-512 512 bits [FIPS 202] current
+%% blake2s-256 256 bits [RFC7693] current
+%% blake2b-256 256 bits [RFC7693] current
+%% blake2b-512 512 bits [RFC7693] current
+%% k12-256 256 bits [draft-irtf-cfrg-kangarootwelve-06] current
+%% k12-512 512 bits [draft-irtf-cfrg-kangarootwelve-06] current
+-spec mech_to_algo(mod_fast_auth_token:mechanism()) -> atom().
+mech_to_algo(<<"HT-SHA-256-NONE">>) -> sha256;
+mech_to_algo(<<"HT-SHA-384-NONE">>) -> sha384;
+mech_to_algo(<<"HT-SHA-512-NONE">>) -> sha512;
+
+mech_to_algo(<<"HT-SHA-3-256-NONE">>) -> sha3_256;
+mech_to_algo(<<"HT-SHA-3-384-NONE">>) -> sha3_384;
+mech_to_algo(<<"HT-SHA-3-512-NONE">>) -> sha3_512;
+
+mech_to_algo(_) -> unknown.
+
+-spec skip_announce_mechanism(mod_fast_auth_token:mechanism()) -> boolean().
+skip_announce_mechanism(Mech) ->
+ mech_to_algo(Mech) =/= unknown.
+
+-spec mechanisms() -> [mod_fast_auth_token:mechanism()].
+mechanisms() ->
+ %% Mechanisms described in
+ %% https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html
+ [
+ <<"HT-SHA-256-NONE">>,
+ <<"HT-SHA-384-NONE">>,
+ <<"HT-SHA-512-NONE">>,
+
+ <<"HT-SHA-3-256-NONE">>,
+ <<"HT-SHA-3-384-NONE">>,
+ <<"HT-SHA-3-512-NONE">>
+ ].
+
+-spec mech_id(mod_fast_auth_token:mechanism()) -> non_neg_integer().
+mech_id(<<"HT-SHA-256-NONE">>) -> 1;
+mech_id(<<"HT-SHA-384-NONE">>) -> 7;
+mech_id(<<"HT-SHA-512-NONE">>) -> 8;
+
+mech_id(<<"HT-SHA-3-256-NONE">>) -> 10;
+mech_id(<<"HT-SHA-3-384-NONE">>) -> 11;
+mech_id(<<"HT-SHA-3-512-NONE">>) -> 12.
+
+-spec mech_name(non_neg_integer()) -> mod_fast_auth_token:mechanism().
+mech_name(1) -> <<"HT-SHA-256-NONE">>;
+mech_name(7) -> <<"HT-SHA-384-NONE">>;
+mech_name(8) -> <<"HT-SHA-512-NONE">>;
+
+mech_name(10) -> <<"HT-SHA-3-256-NONE">>;
+mech_name(11) -> <<"HT-SHA-3-384-NONE">>;
+mech_name(12) -> <<"HT-SHA-3-512-NONE">>;
+
+mech_name(_) -> <<"UNKNOWN-MECH">>. %% Just in case DB has an unknown mech_id
+
+-spec supports_sasl_module(mongooseim:host_type(), module()) -> boolean().
+supports_sasl_module(HostType, Module) ->
+ lists:member(Module, sasl_modules())
+ andalso gen_mod:is_loaded(HostType, mod_fast_auth_token).
+
+sasl_modules() ->
+ [cyrsasl_ht_sha256_none,
+ cyrsasl_ht_sha384_none,
+ cyrsasl_ht_sha512_none,
+
+ cyrsasl_ht_sha3_256_none,
+ cyrsasl_ht_sha3_384_none,
+ cyrsasl_ht_sha3_512_none].
diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl
new file mode 100644
index 00000000000..9cd4c4f181a
--- /dev/null
+++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl
@@ -0,0 +1,154 @@
+-module(mod_fast_auth_token_rdbms).
+-behaviour(mod_fast_auth_token_backend).
+-include("mongoose_logger.hrl").
+
+-export([init/2,
+ store_new_token/8,
+ read_tokens/4,
+ invalidate_token/4,
+ remove_user/3,
+ remove_domain/2]).
+
+-import(mongoose_rdbms, [prepare/4, execute/3, execute_successfully/3]).
+
+-spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
+init(HostType, _Opts) ->
+ prepare_upsert(HostType),
+ prepare_upsert_and_set_current(HostType),
+ prepare(fast_select, fast_auth_token,
+ [server, username, user_agent_id],
+ <<"SELECT "
+ "current_token, current_expire, current_count, current_mech_id, "
+ "new_token, new_expire, new_count, new_mech_id "
+ "FROM fast_auth_token "
+ "WHERE server = ? AND username = ? AND user_agent_id = ?">>),
+ prepare(fast_invalidate_token, fast_auth_token,
+ [server, username, user_agent_id],
+ <<"DELETE FROM fast_auth_token "
+ "WHERE server = ? AND username = ? AND user_agent_id = ?">>),
+ prepare(fast_remove_user, fast_auth_token,
+ [server, username],
+ <<"DELETE FROM fast_auth_token "
+ "WHERE server = ? AND username = ?">>),
+ prepare(fast_remove_domain, fast_auth_token,
+ [server],
+ <<"DELETE FROM fast_auth_token WHERE server = ?">>),
+ ok.
+
+prepare_upsert(HostType) ->
+ Key = [<<"server">>, <<"username">>, <<"user_agent_id">>],
+ Upd = [<<"new_token">>, <<"new_expire">>, <<"new_count">>, <<"new_mech_id">>],
+ Ins = Key ++ Upd,
+ rdbms_queries:prepare_upsert(HostType, fast_upsert, fast_auth_token, Ins, Upd, Key).
+
+prepare_upsert_and_set_current(HostType) ->
+ Key = [<<"server">>, <<"username">>, <<"user_agent_id">>],
+ Upd = [<<"new_token">>, <<"new_expire">>, <<"new_count">>, <<"new_mech_id">>,
+ <<"current_token">>, <<"current_expire">>, <<"current_count">>, <<"current_mech_id">>],
+ Ins = Key ++ Upd,
+ rdbms_queries:prepare_upsert(HostType, fast_upsert_and_set_current, fast_auth_token, Ins, Upd, Key).
+
+-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS,
+ Token, Mech, SetCurrent) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id(),
+ ExpireTS :: mod_fast_auth_token:seconds(),
+ Token :: mod_fast_auth_token:token(),
+ Mech :: mod_fast_auth_token:mechanism(),
+ SetCurrent :: mod_fast_auth_token:set_current() | false.
+store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, false) ->
+ Key = [LServer, LUser, AgentId],
+ Upd = [Token, ExpireTS, 0, mech_id(Mech)],
+ Ins = Key ++ Upd,
+ rdbms_queries:execute_upsert(HostType, fast_upsert, Ins, Upd, Key),
+ ok;
+store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent) ->
+ %% Move new_token into the current_token slot
+ %% We pass data directly instead of checking from the database to avoid
+ %% race conditions
+ #{current_token := CurrentToken,
+ current_expire := CurrentExpire,
+ current_count := CurrentCount,
+ current_mech := CurrentMechId} = SetCurrent,
+ Key = [LServer, LUser, AgentId],
+ Upd = [Token, ExpireTS, 0, mech_id(Mech),
+ CurrentToken, CurrentExpire, CurrentCount, mech_id(CurrentMechId)],
+ Ins = Key ++ Upd,
+ rdbms_queries:execute_upsert(HostType, fast_upsert_and_set_current, Ins, Upd, Key),
+ ok.
+
+-spec read_tokens(HostType, LServer, LUser, AgentId) ->
+ {ok, mod_fast_auth_token:tokens_data()} | {error, not_found}
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id().
+read_tokens(HostType, LServer, LUser, AgentId) ->
+ case execute(HostType, fast_select, [LServer, LUser, AgentId]) of
+ {selected, []} ->
+ {error, not_found};
+ {selected, [{CurrentToken, CurrentExpire, CurrentCount, CurrentMechId,
+ NewToken, NewExpire, NewCount, NewMechId}]} ->
+ Data = #{
+ now_timestamp => mod_fast_auth_token:utc_now_as_seconds(),
+ current_token => null_as_undefined(CurrentToken),
+ current_expire => maybe_to_integer(CurrentExpire),
+ current_count => maybe_to_integer(CurrentCount),
+ current_mech => maybe_to_mech(CurrentMechId),
+ new_token => null_as_undefined(NewToken),
+ new_expire => maybe_to_integer(NewExpire),
+ new_count => maybe_to_integer(NewCount),
+ new_mech => maybe_to_mech(NewMechId)
+ },
+ {ok, Data};
+ Other ->
+ ?LOG_ERROR(#{what => fast_token_read_failed,
+ username => LUser, server => LServer, result => Other}),
+ {error, not_found}
+ end.
+
+maybe_to_integer(null) ->
+ undefined;
+maybe_to_integer(Result) ->
+ mongoose_rdbms:result_to_integer(Result).
+
+maybe_to_mech(null) ->
+ undefined;
+maybe_to_mech(Result) ->
+ Int = mongoose_rdbms:result_to_integer(Result),
+ mech_name(Int).
+
+null_as_undefined(null) ->
+ undefined;
+null_as_undefined(Result) ->
+ Result.
+
+-spec remove_user(mongooseim:host_type(), jid:luser(), jid:lserver()) -> ok.
+remove_user(HostType, LUser, LServer) ->
+ execute_successfully(HostType, fast_remove_user, [LServer, LUser]),
+ ok.
+
+-spec invalidate_token(HostType, LServer, LUser, AgentId) -> ok
+ when HostType :: mongooseim:host_type(),
+ LServer :: jid:lserver(),
+ LUser :: jid:luser(),
+ AgentId :: mod_fast_auth_token:agent_id().
+invalidate_token(HostType, LServer, LUser, AgentId) ->
+ execute_successfully(HostType, fast_invalidate_token,
+ [LServer, LUser, AgentId]),
+ ok.
+
+-spec remove_domain(mongooseim:host_type(), jid:lserver()) -> ok.
+remove_domain(HostType, LServer) ->
+ execute_successfully(HostType, fast_remove_domain, [LServer]),
+ ok.
+
+-spec mech_id(mod_fast_auth_token:mechanism()) -> non_neg_integer().
+mech_id(Mech) ->
+ mod_fast_auth_token_generic_mech:mech_id(Mech).
+
+-spec mech_name(non_neg_integer()) -> mod_fast_auth_token:mechanism().
+mech_name(MechId) ->
+ mod_fast_auth_token_generic_mech:mech_name(MechId).
diff --git a/src/hooks/mongoose_hooks.erl b/src/hooks/mongoose_hooks.erl
index 475178c77e1..02665bd33d7 100644
--- a/src/hooks/mongoose_hooks.erl
+++ b/src/hooks/mongoose_hooks.erl
@@ -42,7 +42,7 @@
bind2_stream_features/2,
bind2_enable_features/3,
sasl2_start/3,
- sasl2_success/3]).
+ sasl2_success/4]).
-export([get_pep_recipients/2,
filter_pep_recipient/3,
@@ -515,13 +515,14 @@ sasl2_start(HostType, Acc, Element) ->
run_hook_for_host_type(sasl2_start, HostType, Acc, Params).
%% If SASL authentication is successful, inline features can be triggered
--spec sasl2_success(HostType, Acc, Params) -> Result when
+-spec sasl2_success(HostType, Acc, Params, Creds) -> Result when
HostType :: mongooseim:host_type(),
Acc :: mongoose_acc:t(),
Params :: mod_sasl2:c2s_state_data(),
+ Creds :: mongoose_credentials:t(),
Result :: mongoose_acc:t().
-sasl2_success(HostType, Acc, Params) ->
- run_hook_for_host_type(sasl2_success, HostType, Acc, Params).
+sasl2_success(HostType, Acc, Params, Creds) ->
+ run_hook_for_host_type(sasl2_success, HostType, Acc, Params#{creds => Creds}).
-spec check_bl_c2s(IP) -> Result when
IP :: inet:ip_address(),
diff --git a/src/mod_sasl2.erl b/src/mod_sasl2.erl
index 4765d2ba451..b543be0dfdc 100644
--- a/src/mod_sasl2.erl
+++ b/src/mod_sasl2.erl
@@ -36,7 +36,8 @@
-export([get_inline_request/2, get_inline_request/3, put_inline_request/3,
append_inline_response/3, update_inline_request/4,
get_state_data/1, set_state_data/2,
- request_block_future_stream_features/1]).
+ request_block_future_stream_features/1,
+ get_mod_state/1]).
-type maybe_binary() :: undefined | binary().
-type status() :: pending | success | failure.
@@ -45,6 +46,7 @@
status := status()}.
-type mod_state() :: #{authenticated := boolean(),
id := not_provided | uuid:uuid(),
+ encoded_id := not_provided | binary(),
software := not_provided | binary(),
device := not_provided | binary()}.
-type c2s_state_data() :: #{c2s_state := mongoose_c2s:state(),
@@ -227,7 +229,7 @@ handle_sasl_step({error, NewSaslAcc, #{type := Type}}, OriginalStateData, Retrie
-spec handle_sasl_success(mongoose_acc:t(), mongoose_c2s_sasl:success(), c2s_state_data()) ->
mongoose_c2s:fsm_res().
handle_sasl_success(SaslAcc,
- #{server_out := MaybeServerOut, jid := Jid, auth_module := AuthMod},
+ #{server_out := MaybeServerOut, jid := Jid, auth_module := AuthMod, creds := Creds},
#{c2s_data := C2SData} = OriginalStateData) ->
C2SData1 = build_final_c2s_data(C2SData, Jid, AuthMod),
OriginalStateData1 = OriginalStateData#{c2s_data := C2SData1},
@@ -235,7 +237,7 @@ handle_sasl_success(SaslAcc,
user => jid:to_binary(Jid), c2s_state => C2SData1}),
HostType = mongoose_c2s:get_host_type(C2SData1),
SaslAcc1 = set_state_data(SaslAcc, OriginalStateData1),
- SaslAcc2 = mongoose_hooks:sasl2_success(HostType, SaslAcc1, OriginalStateData1),
+ SaslAcc2 = mongoose_hooks:sasl2_success(HostType, SaslAcc1, OriginalStateData1, Creds),
process_sasl2_success(SaslAcc2, OriginalStateData1, MaybeServerOut).
-spec handle_sasl_continue(
@@ -392,7 +394,8 @@ get_initial_response(El) ->
-spec init_mod_state(not_provided | exml:element()) -> invalid_agent | mod_state().
init_mod_state(not_provided) ->
- #{authenticated => false, id => not_provided, software => not_provided, device => not_provided};
+ #{authenticated => false, id => not_provided, software => not_provided, device => not_provided,
+ encoded_id => not_provided};
init_mod_state(El) ->
MaybeId = exml_query:attr(El, <<"id">>, not_provided),
case if_provided_then_is_not_invalid_uuid_v4(MaybeId) of
@@ -401,7 +404,8 @@ init_mod_state(El) ->
Value ->
Software = exml_query:path(El, [{element, <<"software">>}, cdata], not_provided),
Device = exml_query:path(El, [{element, <<"device">>}, cdata], not_provided),
- #{authenticated => false, id => Value, software => Software, device => Device}
+ #{authenticated => false, id => Value, software => Software, device => Device,
+ encoded_id => MaybeId}
end.
-spec if_provided_then_is_not_invalid_uuid_v4(not_provided | binary()) ->
diff --git a/src/offline/mod_offline.erl b/src/offline/mod_offline.erl
index abacc68bf84..8808d3c5eb1 100644
--- a/src/offline/mod_offline.erl
+++ b/src/offline/mod_offline.erl
@@ -469,12 +469,7 @@ remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type :
Params :: map(),
Extra :: gen_hook:extra().
remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) ->
- case mongoose_lib:is_exported(mod_offline_backend, remove_domain, 2) of
- true ->
- mod_offline_backend:remove_domain(HostType, Domain);
- false ->
- ok
- end,
+ mod_offline_backend:remove_domain(HostType, Domain),
{ok, Acc}.
-spec disco_features(Acc, Params, Extra) -> {ok, Acc} when
diff --git a/src/sasl/cyrsasl.erl b/src/sasl/cyrsasl.erl
index 80d6418390e..a944b2efdc3 100644
--- a/src/sasl/cyrsasl.erl
+++ b/src/sasl/cyrsasl.erl
@@ -120,7 +120,9 @@ server_start(#sasl_state{myname = Host, host_type = HostType} = State,
is_module_supported(HostType, cyrsasl_oauth) ->
gen_mod:is_loaded(HostType, mod_auth_token);
is_module_supported(HostType, Module) ->
- mongoose_fips:supports_sasl_module(Module) andalso ejabberd_auth:supports_sasl_module(HostType, Module).
+ mod_fast_auth_token_generic_mech:supports_sasl_module(HostType, Module)
+ orelse (mongoose_fips:supports_sasl_module(Module) andalso
+ ejabberd_auth:supports_sasl_module(HostType, Module)).
-spec server_step(State :: sasl_state(), ClientIn :: binary()) -> Result when
Result :: sasl_result().
@@ -156,4 +158,5 @@ default_modules() ->
cyrsasl_scram_sha1,
cyrsasl_plain,
cyrsasl_anonymous,
- cyrsasl_oauth].
+ cyrsasl_oauth]
+ ++ mod_fast_auth_token_generic_mech:sasl_modules().
diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl
new file mode 100644
index 00000000000..afbc8e9c852
--- /dev/null
+++ b/src/sasl/cyrsasl_ht_sha256_none.erl
@@ -0,0 +1,21 @@
+-module(cyrsasl_ht_sha256_none).
+-behaviour(cyrsasl).
+
+-export([mechanism/0, mech_new/3, mech_step/2]).
+-ignore_xref([mech_new/3]).
+
+-spec mechanism() -> cyrsasl:mechanism().
+mechanism() ->
+ <<"HT-SHA-256-NONE">>.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term()) -> {ok, tuple()} | {error, binary()}.
+mech_new(Host, Creds, SocketData) ->
+ mod_fast_auth_token_generic_mech:mech_new(Host, Creds, SocketData, mechanism()).
+
+-spec mech_step(State :: tuple(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(State, SerializedToken) ->
+ mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken).
diff --git a/src/sasl/cyrsasl_ht_sha384_none.erl b/src/sasl/cyrsasl_ht_sha384_none.erl
new file mode 100644
index 00000000000..f0f4e59b3f9
--- /dev/null
+++ b/src/sasl/cyrsasl_ht_sha384_none.erl
@@ -0,0 +1,21 @@
+-module(cyrsasl_ht_sha384_none).
+-behaviour(cyrsasl).
+
+-export([mechanism/0, mech_new/3, mech_step/2]).
+-ignore_xref([mech_new/3]).
+
+-spec mechanism() -> cyrsasl:mechanism().
+mechanism() ->
+ <<"HT-SHA-384-NONE">>.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term()) -> {ok, tuple()} | {error, binary()}.
+mech_new(Host, Creds, SocketData) ->
+ mod_fast_auth_token_generic_mech:mech_new(Host, Creds, SocketData, mechanism()).
+
+-spec mech_step(State :: tuple(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(State, SerializedToken) ->
+ mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken).
diff --git a/src/sasl/cyrsasl_ht_sha3_256_none.erl b/src/sasl/cyrsasl_ht_sha3_256_none.erl
new file mode 100644
index 00000000000..9ac306d0438
--- /dev/null
+++ b/src/sasl/cyrsasl_ht_sha3_256_none.erl
@@ -0,0 +1,21 @@
+-module(cyrsasl_ht_sha3_256_none).
+-behaviour(cyrsasl).
+
+-export([mechanism/0, mech_new/3, mech_step/2]).
+-ignore_xref([mech_new/3]).
+
+-spec mechanism() -> cyrsasl:mechanism().
+mechanism() ->
+ <<"HT-SHA-3-256-NONE">>.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term()) -> {ok, tuple()} | {error, binary()}.
+mech_new(Host, Creds, SocketData) ->
+ mod_fast_auth_token_generic_mech:mech_new(Host, Creds, SocketData, mechanism()).
+
+-spec mech_step(State :: tuple(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(State, SerializedToken) ->
+ mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken).
diff --git a/src/sasl/cyrsasl_ht_sha3_384_none.erl b/src/sasl/cyrsasl_ht_sha3_384_none.erl
new file mode 100644
index 00000000000..632fecdec88
--- /dev/null
+++ b/src/sasl/cyrsasl_ht_sha3_384_none.erl
@@ -0,0 +1,21 @@
+-module(cyrsasl_ht_sha3_384_none).
+-behaviour(cyrsasl).
+
+-export([mechanism/0, mech_new/3, mech_step/2]).
+-ignore_xref([mech_new/3]).
+
+-spec mechanism() -> cyrsasl:mechanism().
+mechanism() ->
+ <<"HT-SHA-3-384-NONE">>.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term()) -> {ok, tuple()} | {error, binary()}.
+mech_new(Host, Creds, SocketData) ->
+ mod_fast_auth_token_generic_mech:mech_new(Host, Creds, SocketData, mechanism()).
+
+-spec mech_step(State :: tuple(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(State, SerializedToken) ->
+ mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken).
diff --git a/src/sasl/cyrsasl_ht_sha3_512_none.erl b/src/sasl/cyrsasl_ht_sha3_512_none.erl
new file mode 100644
index 00000000000..40cd28f9ddd
--- /dev/null
+++ b/src/sasl/cyrsasl_ht_sha3_512_none.erl
@@ -0,0 +1,21 @@
+-module(cyrsasl_ht_sha3_512_none).
+-behaviour(cyrsasl).
+
+-export([mechanism/0, mech_new/3, mech_step/2]).
+-ignore_xref([mech_new/3]).
+
+-spec mechanism() -> cyrsasl:mechanism().
+mechanism() ->
+ <<"HT-SHA-3-512-NONE">>.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term()) -> {ok, tuple()} | {error, binary()}.
+mech_new(Host, Creds, SocketData) ->
+ mod_fast_auth_token_generic_mech:mech_new(Host, Creds, SocketData, mechanism()).
+
+-spec mech_step(State :: tuple(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(State, SerializedToken) ->
+ mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken).
diff --git a/src/sasl/cyrsasl_ht_sha512_none.erl b/src/sasl/cyrsasl_ht_sha512_none.erl
new file mode 100644
index 00000000000..a2aaeb436f0
--- /dev/null
+++ b/src/sasl/cyrsasl_ht_sha512_none.erl
@@ -0,0 +1,21 @@
+-module(cyrsasl_ht_sha512_none).
+-behaviour(cyrsasl).
+
+-export([mechanism/0, mech_new/3, mech_step/2]).
+-ignore_xref([mech_new/3]).
+
+-spec mechanism() -> cyrsasl:mechanism().
+mechanism() ->
+ <<"HT-SHA-512-NONE">>.
+
+-spec mech_new(Host :: jid:server(),
+ Creds :: mongoose_credentials:t(),
+ SocketData :: term()) -> {ok, tuple()} | {error, binary()}.
+mech_new(Host, Creds, SocketData) ->
+ mod_fast_auth_token_generic_mech:mech_new(Host, Creds, SocketData, mechanism()).
+
+-spec mech_step(State :: tuple(),
+ ClientIn :: binary()) -> {ok, mongoose_credentials:t()}
+ | {error, binary()}.
+mech_step(State, SerializedToken) ->
+ mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken).
diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl
index 9d78f925b0f..4a37fda9d6e 100644
--- a/test/common/config_parser_helper.erl
+++ b/test/common/config_parser_helper.erl
@@ -867,6 +867,10 @@ default_mod_config(mod_auth_token) ->
#{backend => rdbms, iqdisc => no_queue,
validity_period => #{access => #{unit => hours, value => 1},
refresh => #{unit => days, value => 25}}};
+default_mod_config(mod_fast_auth_token) ->
+ #{backend => rdbms,
+ validity_period => #{access => #{unit => days, value => 3},
+ rotate_before_expire => #{unit => hours, value => 6}}};
default_mod_config(mod_bind2) ->
#{};
default_mod_config(mod_blocking) ->
diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl
index 90c6ce5eaa2..577348b7847 100644
--- a/test/config_parser_SUITE.erl
+++ b/test/config_parser_SUITE.erl
@@ -173,6 +173,7 @@ groups() ->
s2s_max_retry_delay]},
{modules, [parallel], [mod_adhoc,
mod_auth_token,
+ mod_fast_auth_token,
mod_blocking,
mod_bosh,
mod_caps,
@@ -1493,6 +1494,27 @@ mod_auth_token(_Config) ->
?errh(T(<<"validity_period">>, #{<<"access">> => #{<<"value">> => 10}})),
?errh(T(<<"validity_period">>, #{<<"access">> => #{<<"unit">> => <<"days">>}})).
+mod_fast_auth_token(_Config) ->
+ check_module_defaults(mod_fast_auth_token),
+ P = [modules, mod_fast_auth_token],
+ T = fun(K, V) -> #{<<"modules">> => #{<<"mod_fast_auth_token">> => #{K => V}}} end,
+ ?cfgh(P ++ [backend], rdbms, T(<<"backend">>, <<"rdbms">>)),
+ ?cfgh(P ++ [validity_period, access], #{unit => minutes, value => 13},
+ T(<<"validity_period">>,
+ #{<<"access">> => #{<<"value">> => 13, <<"unit">> => <<"minutes">>}})),
+
+ ?cfgh(P ++ [validity_period, rotate_before_expire], #{unit => days, value => 31},
+ T(<<"validity_period">>,
+ #{<<"rotate_before_expire">> => #{<<"value">> => 31, <<"unit">> => <<"days">>}})),
+
+ ?errh(T(<<"backend">>, <<"nosql">>)),
+ ?errh(T(<<"validity_period">>,
+ #{<<"access">> => #{<<"value">> => -1, <<"unit">> => <<"minutes">>}})),
+ ?errh(T(<<"validity_period">>,
+ #{<<"access">> => #{<<"value">> => 10, <<"unit">> => <<"centuries">>}})),
+ ?errh(T(<<"validity_period">>, #{<<"access">> => #{<<"value">> => 10}})),
+ ?errh(T(<<"validity_period">>, #{<<"access">> => #{<<"unit">> => <<"days">>}})).
+
mod_blocking(_Config) ->
test_privacy_opts(mod_blocking).
@@ -3096,8 +3118,22 @@ check_iqdisc(ParentP, ParentT) when is_function(ParentT, 1) ->
check_module_defaults(Mod) ->
ExpectedCfg = default_mod_config(Mod),
+ case maps:size(ExpectedCfg) of
+ 0 ->
+ ok;
+ _ ->
+ assert_configurable_module(mod_fast_auth_token)
+ end,
?cfgh([modules, Mod], ExpectedCfg, #{<<"modules">> => #{atom_to_binary(Mod) => #{}}}).
+assert_configurable_module(Module) ->
+ case lists:member(Module, mongoose_config_spec:configurable_modules()) of
+ true -> ok;
+ false ->
+ ct:fail({assert_configurable_module, Module,
+ "Don't forget to add module into mongoose_config_spec:configurable_modules/1"})
+ end.
+
%% helpers for 'listen' tests
listener(Type, Opts) ->