From 2b432bad44e8a8fe8dfbdceac470ded2f23c994a Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Thu, 26 Sep 2024 00:45:35 +0200 Subject: [PATCH 01/46] Implement adding token on initial auth for XEP-0484 --- big_tests/default.spec | 1 + big_tests/tests/fast_SUITE.erl | 96 ++++++++++++++++++++++++++++ big_tests/tests/sasl2_helper.erl | 3 +- include/mongoose_ns.hrl | 1 + src/mod_fast.erl | 93 +++++++++++++++++++++++++++ test/common/config_parser_helper.erl | 4 +- 6 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 big_tests/tests/fast_SUITE.erl create mode 100644 src/mod_fast.erl diff --git a/big_tests/default.spec b/big_tests/default.spec index 1cc9051c24d..cbb6e50fa22 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_SUITE}. {suites, "tests", bosh_SUITE}. {suites, "tests", carboncopy_SUITE}. {suites, "tests", connect_SUITE}. diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl new file mode 100644 index 00000000000..a0fdeb6ff09 --- /dev/null +++ b/big_tests/tests/fast_SUITE.erl @@ -0,0 +1,96 @@ +-module(fast_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_FAST, <<"urn:xmpp:fast:0">>). + +%%-------------------------------------------------------------------- +%% Suite configuration +%%-------------------------------------------------------------------- + +all() -> + [ + {group, basic} + ]. + +groups() -> + [ + {basic, [parallel], + [ + server_announces_fast, + request_token_with_initial_authentication + ]} + ]. + +%%-------------------------------------------------------------------- +%% Init & teardown +%%-------------------------------------------------------------------- + +init_per_suite(Config) -> + Config1 = load_modules(Config), + escalus:init_per_suite(Config1). + +end_per_suite(Config) -> + escalus_fresh:clean(), + dynamic_modules:restore_modules(Config), + escalus:end_per_suite(Config). + +init_per_group(_GroupName, Config) -> + Config. + +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 +%%-------------------------------------------------------------------- + +server_announces_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), + ct:fail(Fast), + ok. + +request_token_with_initial_authentication(Config) -> + Steps = [start_new_user, {?MODULE, auth_and_request_token}, + receive_features, has_no_more_stanzas], + #{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}]), + Expire = exml_query:attr(Fast, <<"expire">>), + Token = exml_query:attr(Fast, <<"token">>), + ct:fail({Expire, Token}). + +auth_and_request_token(Config, Client, Data) -> + Extra = [request_token()], + bind2_SUITE:plain_auth(Config, Client, Data, [], Extra). + +%% +request_token() -> + #xmlel{name = <<"request-token">>, + attrs = [{<<"xmlns">>, ?NS_FAST}, + {<<"mechanism">>, <<"HT-SHA-256-ENDP">>}]}. diff --git a/big_tests/tests/sasl2_helper.erl b/big_tests/tests/sasl2_helper.erl index 2a7376e7c29..79b6889441e 100644 --- a/big_tests/tests/sasl2_helper.erl +++ b/big_tests/tests/sasl2_helper.erl @@ -21,7 +21,8 @@ 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)}, + {mod_fast, default_mod_config(mod_fast)}], dynamic_modules:ensure_modules(HostType, Modules). apply_steps(Steps, Config) -> 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/src/mod_fast.erl b/src/mod_fast.erl new file mode 100644 index 00000000000..a0070ec95b5 --- /dev/null +++ b/src/mod_fast.erl @@ -0,0 +1,93 @@ +-module(mod_fast). +-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]). + +-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. +start(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}]. + +-spec config_spec() -> mongoose_config_spec:config_section(). +config_spec() -> + #section{ + defaults = #{} + }. + +supported_features() -> [dynamic_domains]. + +-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 = <<"fast">>, + children = [#xmlcdata{content = Mech}]} || Mech <- Mechs]. + +mechanisms() -> + [<<"HT-SHA-256-ENDP">>, <<"HT-SHA-256-EXPR">>, <<"HT-SHA-256-NONE">>]. + +-spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> + {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). +sasl2_start(SaslAcc, #{stanza := El}, _) -> + case exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}]) of + undefined -> + {ok, SaslAcc}; + Request -> + Mech = exml_query:attr(Request, <<"mechanism">>), + case Mech of + <<"HT-SHA-256-ENDP">> -> + {ok, mod_sasl2:put_inline_request(SaslAcc, ?MODULE, Request)}; + _ -> + {ok, SaslAcc} + end + end. + +-spec sasl2_success(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) -> + {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). +sasl2_success(SaslAcc, _, #{host_type := HostType}) -> + case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of + undefined -> + {ok, SaslAcc}; + #{request := Request} -> + Response = make_fast_token_response(Request), + SaslAcc2 = mod_sasl2:update_inline_request(SaslAcc, ?MODULE, Response, success), + {ok, SaslAcc2} + end. + +make_fast_token_response(Request) -> + Expire = <<"2020-03-12T14:36:15Z">>, + Token = <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, + #xmlel{name = <<"token">>, + attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"expire">>, Expire}, + {<<"token">>, Token}]}. diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 9d78f925b0f..921f3273403 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -637,7 +637,9 @@ all_modules() -> resume_timeout => 600, stale_h => #{enabled => true, geriatric => 3600, - repeat_after => 1800}}) + repeat_after => 1800}}), + mod_fast => + mod_config(mod_fast, #{}) }. custom_mod_event_pusher_http() -> From d95e3cad753f85fb7fbf912ef32457c2c83ea518 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Fri, 27 Sep 2024 13:56:16 +0200 Subject: [PATCH 02/46] Provide agent-id into mod_fast Do not announce mod_fast mechanisms --- big_tests/tests/fast_SUITE.erl | 91 +++++++++++++++++++++++++++-- src/c2s/mongoose_c2s.erl | 15 ++++- src/c2s/mongoose_c2s_sasl.erl | 5 +- src/c2s/mongoose_c2s_stanzas.erl | 2 +- src/mod_fast.erl | 25 +++++--- src/mod_sasl2.erl | 3 +- src/sasl/cyrsasl.erl | 5 +- src/sasl/cyrsasl_ht_sha256_none.erl | 58 ++++++++++++++++++ 8 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 src/sasl/cyrsasl_ht_sha256_none.erl diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl index a0fdeb6ff09..2fcd035ca3b 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_SUITE.erl @@ -9,6 +9,7 @@ -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">>). %%-------------------------------------------------------------------- @@ -77,20 +78,98 @@ server_announces_fast(Config) -> request_token_with_initial_authentication(Config) -> Steps = [start_new_user, {?MODULE, auth_and_request_token}, receive_features, has_no_more_stanzas], - #{answer := Success} = sasl2_helper:apply_steps(Steps, Config), + #{answer := Success, spec := Spec} = 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}]), Expire = exml_query:attr(Fast, <<"expire">>), Token = exml_query:attr(Fast, <<"token">>), - ct:fail({Expire, Token}). + auth_with_token(Token, Config, Spec), + ok. +% ct:fail({Expire, Token}). auth_and_request_token(Config, Client, Data) -> - Extra = [request_token()], - bind2_SUITE:plain_auth(Config, Client, Data, [], Extra). + Extra = [request_token(), user_agent()], + auth_with_method(Config, Client, Data, [], Extra, <<"PLAIN">>). + +auth_using_token(Config, Client, Data) -> + Extra = [user_agent()], + auth_with_method(Config, Client, Data, [], Extra, <<"HT-SHA-256-NONE">>). -%% +%% request_token() -> #xmlel{name = <<"request-token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, - {<<"mechanism">>, <<"HT-SHA-256-ENDP">>}]}. + {<<"mechanism">>, <<"HT-SHA-256-NONE">>}]}. + +auth_with_token(Token, Config, Spec) -> + Spec2 = [{secret_token, Token} | Spec], + Steps = [connect_tls, start_stream_get_features, + {?MODULE, auth_using_token}, + receive_features, has_no_more_stanzas], + Data = #{spec => Spec2}, + #{answer := Success} = sasl2_helper:apply_steps(Steps, Config, undefined, Data), + ?assertMatch(#xmlel{name = <<"success">>, + attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success). + +user_agent() -> + #xmlel{name = <<"user-agent">>, + attrs = [{<<"id">>, <<"d4565fa7-4d72-4749-b3d3-740edbf87770">>}], + 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-SHA-256-NONE">> -> + ht_auth_initial_response(Client) + 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:pal("Answer ~p", [Answer]), + Identifier = exml_query:path(Answer, [{element, <<"authorization-identifier">>}, cdata]), + #jid{lresource = LResource} = jid:from_binary(Identifier), + {Client, Data#{answer => Answer, client_1_jid => Identifier, bind2_resource => LResource}}. + +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}) -> + %% authcid is the username before "@" sign. + Username = proplists:get_value(username, Props), + Token = proplists:get_value(secret_token, Props), + CBData = <<>>, + ToHash = <<"Initiator", CBData/binary>>, + InitiatorHashedToken = crypto:mac(hmac, sha256, Token, ToHash), + Payload = <>, + initial_response_elem(Payload). + +initial_response_elem(Payload) -> + Encoded = base64:encode(Payload), + #xmlel{name = <<"initial-response">>, + children = [#xmlcdata{content = Encoded}]}. diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl index 5395d6cf026..3437c87f233 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,18 @@ 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(<<"HT-SHA-256-ENDP">>) -> true; +skip_announce_mechanism(<<"HT-SHA-256-EXPR">>) -> true; +skip_announce_mechanism(<<"HT-SHA-256-NONE">>) -> true; +skip_announce_mechanism(_) -> false. + -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..4dea9d830ea 100644 --- a/src/c2s/mongoose_c2s_sasl.erl +++ b/src/c2s/mongoose_c2s_sasl.erl @@ -48,7 +48,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) 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/mod_fast.erl b/src/mod_fast.erl index a0070ec95b5..85c46f6ee20 100644 --- a/src/mod_fast.erl +++ b/src/mod_fast.erl @@ -51,25 +51,32 @@ fast() -> children = mechanisms_elems(mechanisms())}. mechanisms_elems(Mechs) -> - [#xmlel{name = <<"fast">>, + [#xmlel{name = <<"mechanism">>, children = [#xmlcdata{content = Mech}]} || Mech <- Mechs]. mechanisms() -> - [<<"HT-SHA-256-ENDP">>, <<"HT-SHA-256-EXPR">>, <<"HT-SHA-256-NONE">>]. + %% Mechanisms described in + %% https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html + [% <<"HT-SHA-256-ENDP">>, + % <<"HT-SHA-256-EXPR">>, + %% Channel binding: none + <<"HT-SHA-256-NONE">>]. -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). sasl2_start(SaslAcc, #{stanza := El}, _) -> + AgentId = exml_query:path(El, [{element, <<"user-agent">>}, {attr, <<"id">>}]), + SaslAcc2 = mongoose_acc:set(?MODULE, agent_id, AgentId, SaslAcc), case exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}]) of undefined -> - {ok, SaslAcc}; + {ok, SaslAcc2}; Request -> Mech = exml_query:attr(Request, <<"mechanism">>), case Mech of - <<"HT-SHA-256-ENDP">> -> - {ok, mod_sasl2:put_inline_request(SaslAcc, ?MODULE, Request)}; + <<"HT-SHA-256-NONE">> -> + {ok, mod_sasl2:put_inline_request(SaslAcc2, ?MODULE, Request)}; _ -> - {ok, SaslAcc} + {ok, SaslAcc2} end end. @@ -80,12 +87,14 @@ sasl2_success(SaslAcc, _, #{host_type := HostType}) -> undefined -> {ok, SaslAcc}; #{request := Request} -> - Response = make_fast_token_response(Request), + AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc), + %% Attach Token to the response to be used to authentificate + Response = make_fast_token_response(Request, AgentId), SaslAcc2 = mod_sasl2:update_inline_request(SaslAcc, ?MODULE, Response, success), {ok, SaslAcc2} end. -make_fast_token_response(Request) -> +make_fast_token_response(Request, AgentId) -> Expire = <<"2020-03-12T14:36:15Z">>, Token = <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, #xmlel{name = <<"token">>, diff --git a/src/mod_sasl2.erl b/src/mod_sasl2.erl index 4765d2ba451..12a83a841c0 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. diff --git a/src/sasl/cyrsasl.erl b/src/sasl/cyrsasl.erl index 80d6418390e..80a40651404 100644 --- a/src/sasl/cyrsasl.erl +++ b/src/sasl/cyrsasl.erl @@ -119,6 +119,8 @@ 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, cyrsasl_ht_sha256_none) -> + true; is_module_supported(HostType, Module) -> mongoose_fips:supports_sasl_module(Module) andalso ejabberd_auth:supports_sasl_module(HostType, Module). @@ -156,4 +158,5 @@ default_modules() -> cyrsasl_scram_sha1, cyrsasl_plain, cyrsasl_anonymous, - cyrsasl_oauth]. + cyrsasl_oauth, + cyrsasl_ht_sha256_none]. diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl new file mode 100644 index 00000000000..7fe31ad6250 --- /dev/null +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -0,0 +1,58 @@ +-module(cyrsasl_ht_sha256_none). + +-export([mechanism/0, mech_new/3, mech_step/2]). + +-ignore_xref([mech_new/3]). + +-behaviour(cyrsasl). + +-record(state, {creds, agent_id}). + +-include("mongoose.hrl"). + +-spec mechanism() -> cyrsasl:mechanism(). +mechanism() -> + <<"HT-SHA-256-NONE">>. + +-spec mech_new(Host :: jid:server(), + Creds :: mongoose_credentials:t(), + SocketData :: term()) -> {ok, tuple()}. +mech_new(_Host, Creds, SocketData = #{sasl_state := SaslState}) -> + SaslModState = mod_sasl2:get_mod_state(SaslState), + case SaslModState of + #{id := AgentId} -> + {ok, #state{creds = Creds, agent_id = AgentId}}; + _ -> + {error, <<"not-sasl2">>} + end; +mech_new(_Host, _Creds, _SocketData) -> + {error, <<"not-sasl2">>}. + +format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). + +-spec mech_step(State :: #state{}, + ClientIn :: binary()) -> {ok, mongoose_credentials:t()} + | {error, binary()}. +mech_step(#state{creds = Creds, agent_id = AgentId}, SerializedToken) -> + %% SerializedToken is base64 decoded. + Parts = binary:split(SerializedToken, <<0>>), + ?LOG_ERROR(#{what => cyrsasl_ht_sha256_none, creds => Creds, ser_token => SerializedToken, parts => Parts, agent_id => AgentId}), + [Username, InitiatorHashedToken] = Parts, + HostType = mongoose_credentials:host_type(Creds), + case mod_auth_token:authenticate(HostType, SerializedToken) of + % Validating access token + {ok, AuthModule, User} -> + {ok, mongoose_credentials:extend(Creds, + [{username, User}, + {auth_module, AuthModule}])}; + % Validating refresh token and returning new tokens + {ok, AuthModule, User, AccessToken} -> + {ok, mongoose_credentials:extend(Creds, + [{username, User}, + {auth_module, AuthModule}, + {sasl_success_response, AccessToken}])}; + {error, {Username, _}} -> + {error, <<"not-authorized">>, Username}; + {error, _Reason} -> + {error, <<"not-authorized">>} + end. From 375118903c6780132ddadf965cdfd5b0ce1c91df Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Fri, 27 Sep 2024 15:01:52 +0200 Subject: [PATCH 03/46] Add store_new_token function --- priv/pg.sql | 21 +++++++++++++++++++++ src/mod_fast.erl | 42 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/priv/pg.sql b/priv/pg.sql index 66874f4eff5..ade9567d30c 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -499,3 +499,24 @@ CREATE TABLE caps ( features text NOT NULL, PRIMARY KEY (node, sub_node) ); + +-- XEP-0484: Fast Authentication Streamlining Tokens +-- Module: mod_fast +CREATE TABLE fast_tokens( + 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, + + -- slots + current_token VARCHAR(250) NOT NULL, + current_expire BIGINT NOT NULL, -- seconds unix timestamp + current_count INT NOT NULL, -- replay counter + + new_token VARCHAR(250) NOT NULL, + new_expire BIGINT NOT NULL, -- seconds unix timestamp + new_count INT NOT NULL, + PRIMARY KEY(server, username, user_agent_id) +); diff --git a/src/mod_fast.erl b/src/mod_fast.erl index 85c46f6ee20..c57738ee063 100644 --- a/src/mod_fast.erl +++ b/src/mod_fast.erl @@ -65,6 +65,7 @@ mechanisms() -> -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). sasl2_start(SaslAcc, #{stanza := El}, _) -> + ?LOG_ERROR(#{what => sasl2_startttt, elleee => El, sasla_acc => SaslAcc}), AgentId = exml_query:path(El, [{element, <<"user-agent">>}, {attr, <<"id">>}]), SaslAcc2 = mongoose_acc:set(?MODULE, agent_id, AgentId, SaslAcc), case exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}]) of @@ -82,21 +83,52 @@ sasl2_start(SaslAcc, #{stanza := El}, _) -> -spec sasl2_success(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). -sasl2_success(SaslAcc, _, #{host_type := HostType}) -> +sasl2_success(SaslAcc, C2SStateData, #{host_type := HostType}) -> + #{c2s_data := C2SData} = C2SStateData, + #jid{luser = LUser, lserver = LServer} = mongoose_c2s:get_jid(C2SData), case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of undefined -> {ok, SaslAcc}; #{request := Request} -> AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc), %% Attach Token to the response to be used to authentificate - Response = make_fast_token_response(Request, AgentId), + Response = make_fast_token_response(HostType, LServer, LUser, Request, AgentId), SaslAcc2 = mod_sasl2:update_inline_request(SaslAcc, ?MODULE, Response, success), {ok, SaslAcc2} end. -make_fast_token_response(Request, AgentId) -> - Expire = <<"2020-03-12T14:36:15Z">>, - Token = <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, +%% Generate expirable auth token and store it in DB +make_fast_token_response(HostType, LServer, LUser, Request, AgentId) -> + TTLSeconds = 100000, + NowTS = utc_now_as_seconds(), + ExpireTS = NowTS + TTLSeconds, + Expire = seconds_to_datetime(ExpireTS), + Token = base64:encode(crypto:strong_rand_bytes(25)), + store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token), #xmlel{name = <<"token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"expire">>, Expire}, {<<"token">>, Token}]}. + +utc_now_as_seconds() -> + datetime_to_seconds(calendar:universal_time()). + +-spec datetime_to_seconds(calendar:datetime()) -> non_neg_integer(). +datetime_to_seconds(DateTime) -> + calendar:datetime_to_gregorian_seconds(DateTime). + +-spec seconds_to_datetime(non_neg_integer()) -> calendar:datetime(). +seconds_to_datetime(Seconds) -> + calendar:gregorian_seconds_to_datetime(Seconds). + +-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: binary(), + ExpireTS :: integer(), + Token :: binary(). +store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> + ?LOG_ERROR(#{what => store_new_token, host_type => HostType, + lserver => LServer, luser => LUser, + expire_ts => ExpireTS, token => Token, agent_id => AgentId}), + ok. From eaa74f0e497e45fca338e207686eda0285cdd69c Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Fri, 27 Sep 2024 15:54:40 +0200 Subject: [PATCH 04/46] Define read_tokens function --- src/mod_fast.erl | 26 ++++++++++++++++---- src/sasl/cyrsasl_ht_sha256_none.erl | 37 ++++++++++++++++++----------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/mod_fast.erl b/src/mod_fast.erl index c57738ee063..a864297dfca 100644 --- a/src/mod_fast.erl +++ b/src/mod_fast.erl @@ -18,6 +18,8 @@ sasl2_start/3, sasl2_success/3]). +-export([read_tokens/4]). + -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) -> ok. @@ -102,13 +104,18 @@ make_fast_token_response(HostType, LServer, LUser, Request, AgentId) -> TTLSeconds = 100000, NowTS = utc_now_as_seconds(), ExpireTS = NowTS + TTLSeconds, - Expire = seconds_to_datetime(ExpireTS), + Expire = seconds_to_binary(ExpireTS), Token = base64:encode(crypto:strong_rand_bytes(25)), store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token), #xmlel{name = <<"token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"expire">>, Expire}, {<<"token">>, Token}]}. +-spec seconds_to_binary(integer()) -> binary(). +seconds_to_binary(Secs) -> + Opts = [{offset, "Z"}, {unit, second}], + list_to_binary(calendar:system_time_to_rfc3339(Secs, Opts)). + utc_now_as_seconds() -> datetime_to_seconds(calendar:universal_time()). @@ -116,10 +123,6 @@ utc_now_as_seconds() -> datetime_to_seconds(DateTime) -> calendar:datetime_to_gregorian_seconds(DateTime). --spec seconds_to_datetime(non_neg_integer()) -> calendar:datetime(). -seconds_to_datetime(Seconds) -> - calendar:gregorian_seconds_to_datetime(Seconds). - -spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), @@ -132,3 +135,16 @@ store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> lserver => LServer, luser => LUser, expire_ts => ExpireTS, token => Token, agent_id => AgentId}), ok. + +read_tokens(HostType, LServer, LUser, AgentId) -> + ?LOG_ERROR(#{what => read_tokens, host_type => HostType, + lserver => LServer, luser => LUser, agent_id => AgentId}), + Data = #{ + current_token => <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, + current_expire => utc_now_as_seconds() + 1000000, + current_count => 0, + new_token => <<"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF">>, + new_expire => utc_now_as_seconds() + 1000000, + new_count => 0 + }, + {ok, Data}. diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index 7fe31ad6250..7da3c38f47e 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -39,20 +39,29 @@ mech_step(#state{creds = Creds, agent_id = AgentId}, SerializedToken) -> ?LOG_ERROR(#{what => cyrsasl_ht_sha256_none, creds => Creds, ser_token => SerializedToken, parts => Parts, agent_id => AgentId}), [Username, InitiatorHashedToken] = Parts, HostType = mongoose_credentials:host_type(Creds), - case mod_auth_token:authenticate(HostType, SerializedToken) of - % Validating access token - {ok, AuthModule, User} -> - {ok, mongoose_credentials:extend(Creds, - [{username, User}, - {auth_module, AuthModule}])}; - % Validating refresh token and returning new tokens - {ok, AuthModule, User, AccessToken} -> - {ok, mongoose_credentials:extend(Creds, - [{username, User}, - {auth_module, AuthModule}, - {sasl_success_response, AccessToken}])}; - {error, {Username, _}} -> - {error, <<"not-authorized">>, Username}; + LServer = mongoose_credentials:lserver(Creds), + LUser = jid:nodeprep(Username), + case mod_fast:read_tokens(HostType, LServer, LUser, AgentId) of + {ok, TokenData} -> + ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), + {error, <<"not-authorized">>}; {error, _Reason} -> {error, <<"not-authorized">>} end. +% case mod_auth_token:authenticate(HostType, SerializedToken) of +% % Validating access token +% {ok, AuthModule, User} -> +% {ok, mongoose_credentials:extend(Creds, +% [{username, User}, +% {auth_module, AuthModule}])}; +% % Validating refresh token and returning new tokens +% {ok, AuthModule, User, AccessToken} -> +% {ok, mongoose_credentials:extend(Creds, +% [{username, User}, +% {auth_module, AuthModule}, +% {sasl_success_response, AccessToken}])}; +% {error, {Username, _}} -> +% {error, <<"not-authorized">>, Username}; +% {error, _Reason} -> +% {error, <<"not-authorized">>} +% end. From 91c484daee805abe75fbfe800890f57c2550447f Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Fri, 27 Sep 2024 16:13:47 +0200 Subject: [PATCH 05/46] Hardcoded token auth works TODO DB backend, more logic cases --- src/mod_fast.erl | 4 +- src/sasl/cyrsasl_ht_sha256_none.erl | 61 ++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/mod_fast.erl b/src/mod_fast.erl index a864297dfca..ac3311f8889 100644 --- a/src/mod_fast.erl +++ b/src/mod_fast.erl @@ -105,7 +105,8 @@ make_fast_token_response(HostType, LServer, LUser, Request, AgentId) -> NowTS = utc_now_as_seconds(), ExpireTS = NowTS + TTLSeconds, Expire = seconds_to_binary(ExpireTS), - Token = base64:encode(crypto:strong_rand_bytes(25)), +% Token = base64:encode(crypto:strong_rand_bytes(25)), + Token = <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token), #xmlel{name = <<"token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"expire">>, Expire}, @@ -140,6 +141,7 @@ read_tokens(HostType, LServer, LUser, AgentId) -> ?LOG_ERROR(#{what => read_tokens, host_type => HostType, lserver => LServer, luser => LUser, agent_id => AgentId}), Data = #{ + now_timestamp => utc_now_as_seconds(), current_token => <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, current_expire => utc_now_as_seconds() + 1000000, current_count => 0, diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index 7da3c38f47e..21442b9ed0f 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -44,24 +44,49 @@ mech_step(#state{creds = Creds, agent_id = AgentId}, SerializedToken) -> case mod_fast:read_tokens(HostType, LServer, LUser, AgentId) of {ok, TokenData} -> ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), - {error, <<"not-authorized">>}; + CBData = <<>>, + case handle_auth(TokenData, InitiatorHashedToken, CBData) of + true -> + {ok, mongoose_credentials:extend(Creds, + [{username, LUser}, + {auth_module, ?MODULE}])}; + false -> + {error, <<"not-authorized">>} + end; {error, _Reason} -> {error, <<"not-authorized">>} end. -% case mod_auth_token:authenticate(HostType, SerializedToken) of -% % Validating access token -% {ok, AuthModule, User} -> -% {ok, mongoose_credentials:extend(Creds, -% [{username, User}, -% {auth_module, AuthModule}])}; -% % Validating refresh token and returning new tokens -% {ok, AuthModule, User, AccessToken} -> -% {ok, mongoose_credentials:extend(Creds, -% [{username, User}, -% {auth_module, AuthModule}, -% {sasl_success_response, AccessToken}])}; -% {error, {Username, _}} -> -% {error, <<"not-authorized">>, Username}; -% {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). +handle_auth(#{ + now_timestamp := NowTimestamp, + current_token := CurrentToken, + current_expire := CurrentExpire, + current_count := CurrentCount, + new_token := NewToken, + new_expire := NewExpire, + new_count := NewCount + }, InitiatorHashedToken, CBData) -> + ToHash = <<"Initiator", CBData/binary>>, + Token1 = {NewToken, NewExpire, NewCount}, + Token2 = {CurrentToken, CurrentExpire, CurrentCount}, + Shared = {NowTimestamp, ToHash, InitiatorHashedToken}, + case check_token(Token1, Shared) of + true -> + true; + false -> + check_token(Token2, Shared) + end. + +check_token({Token, Expire, Count}, {NowTimestamp, ToHash, InitiatorHashedToken}) -> + crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken. From e60549ef61da6dcfba831b14709a8f28d6b8672b Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 30 Sep 2024 20:31:15 +0200 Subject: [PATCH 06/46] Remove unneaded is_function_supported It is always exported --- src/offline/mod_offline.erl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 From e1b12e78692deabbfcf363372759cff13a34ccee Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 30 Sep 2024 20:48:09 +0200 Subject: [PATCH 07/46] Add logic to call mod_fast_backend Add logic for TTL config --- src/mod_fast.erl | 138 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 30 deletions(-) diff --git a/src/mod_fast.erl b/src/mod_fast.erl index ac3311f8889..f010ca1f31d 100644 --- a/src/mod_fast.erl +++ b/src/mod_fast.erl @@ -16,32 +16,100 @@ %% hooks handlers -export([sasl2_stream_features/3, sasl2_start/3, - sasl2_success/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 validity_type() :: days | hours | minutes | seconds. +-type period() :: #{value := non_neg_integer(), + unit := days | hours | minutes | seconds}. +-type token_type() :: access. + +-export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0]). + +-type tokens_data() :: #{ + now_timestamp := seconds(), + current_token := token() | undefined, + current_expire := seconds() | undefined, + current_count := counter() | undefined, + new_token := token(), + new_expire := seconds(), + new_count := counter() + }. + -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) -> + mod_fast_backend:init(HostType, Opts), ok. -spec stop(mongooseim:host_type()) -> ok. -stop(HostType) -> +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}]. + {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{ - defaults = #{} + 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()}, + defaults = #{<<"access">> => #{value => 1, 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 + }. + 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_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_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, _, _) -> @@ -100,27 +168,46 @@ sasl2_success(SaslAcc, C2SStateData, #{host_type := HostType}) -> end. %% Generate expirable auth token and store it in DB -make_fast_token_response(HostType, LServer, LUser, Request, AgentId) -> - TTLSeconds = 100000, - NowTS = utc_now_as_seconds(), +make_fast_token_response(HostType, LServer, LUser, _Request, AgentId) -> + TTLSeconds = get_ttl_seconds(HostType), + NowTS = ?MODULE:utc_now_as_seconds(), ExpireTS = NowTS + TTLSeconds, Expire = seconds_to_binary(ExpireTS), -% Token = base64:encode(crypto:strong_rand_bytes(25)), - Token = <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, + Token = ?MODULE:generate_unique_token(), store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token), #xmlel{name = <<"token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"expire">>, Expire}, {<<"token">>, Token}]}. --spec seconds_to_binary(integer()) -> binary(). +-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() -> datetime_to_seconds(calendar:universal_time()). --spec datetime_to_seconds(calendar:datetime()) -> non_neg_integer(). +-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_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 datetime_to_seconds(calendar:datetime()) -> seconds(). datetime_to_seconds(DateTime) -> calendar:datetime_to_gregorian_seconds(DateTime). @@ -128,25 +215,16 @@ datetime_to_seconds(DateTime) -> when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: binary(), - ExpireTS :: integer(), - Token :: binary(). + AgentId :: agent_id(), + ExpireTS :: seconds(), + Token :: token(). store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> - ?LOG_ERROR(#{what => store_new_token, host_type => HostType, - lserver => LServer, luser => LUser, - expire_ts => ExpireTS, token => Token, agent_id => AgentId}), - ok. + mod_fast_backend:store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token). +-spec read_tokens(HostType, LServer, LUser, AgentId) -> {ok, tokens_data()} + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: binary(). read_tokens(HostType, LServer, LUser, AgentId) -> - ?LOG_ERROR(#{what => read_tokens, host_type => HostType, - lserver => LServer, luser => LUser, agent_id => AgentId}), - Data = #{ - now_timestamp => utc_now_as_seconds(), - current_token => <<"WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm">>, - current_expire => utc_now_as_seconds() + 1000000, - current_count => 0, - new_token => <<"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF">>, - new_expire => utc_now_as_seconds() + 1000000, - new_count => 0 - }, - {ok, Data}. + mod_fast_backend:read_tokens(HostType, LServer, LUser, AgentId). From c0009775c8ea3835d53f365acf78ebc897dc50a4 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 30 Sep 2024 22:21:13 +0200 Subject: [PATCH 08/46] XEP-0484 Fast token auth using DB works --- big_tests/tests/sasl2_helper.erl | 12 +++- priv/pg.sql | 6 +- src/{ => fast}/mod_fast.erl | 7 +- src/fast/mod_fast_backend.erl | 75 ++++++++++++++++++++++ src/fast/mod_fast_rdbms.erl | 96 ++++++++++++++++++++++++++++ src/mod_sasl2.erl | 7 +- src/sasl/cyrsasl_ht_sha256_none.erl | 2 +- test/common/config_parser_helper.erl | 7 +- 8 files changed, 198 insertions(+), 14 deletions(-) rename src/{ => fast}/mod_fast.erl (97%) create mode 100644 src/fast/mod_fast_backend.erl create mode 100644 src/fast/mod_fast_rdbms.erl diff --git a/big_tests/tests/sasl2_helper.erl b/big_tests/tests/sasl2_helper.erl index 79b6889441e..122ad23c784 100644 --- a/big_tests/tests/sasl2_helper.erl +++ b/big_tests/tests/sasl2_helper.erl @@ -21,10 +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_fast, default_mod_config(mod_fast)}], + {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, mod_config(mod_fast, #{backend => rdbms})}]; + false -> + [] + end. + apply_steps(Steps, Config) -> apply_steps(Steps, Config, undefined, #{}). diff --git a/priv/pg.sql b/priv/pg.sql index ade9567d30c..ea54d66a640 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -511,9 +511,9 @@ CREATE TABLE fast_tokens( user_agent_id VARCHAR(250) NOT NULL, -- slots - current_token VARCHAR(250) NOT NULL, - current_expire BIGINT NOT NULL, -- seconds unix timestamp - current_count INT NOT NULL, -- replay counter + current_token VARCHAR(250), + current_expire BIGINT, -- seconds unix timestamp + current_count INT, -- replay counter new_token VARCHAR(250) NOT NULL, new_expire BIGINT NOT NULL, -- seconds unix timestamp diff --git a/src/mod_fast.erl b/src/fast/mod_fast.erl similarity index 97% rename from src/mod_fast.erl rename to src/fast/mod_fast.erl index f010ca1f31d..75776358cdf 100644 --- a/src/mod_fast.erl +++ b/src/fast/mod_fast.erl @@ -78,7 +78,7 @@ config_spec() -> validity_periods_spec() -> #section{ items = #{<<"access">> => validity_period_spec()}, - defaults = #{<<"access">> => #{value => 1, unit => hours}}, + defaults = #{<<"access">> => #{value => 3, unit => days}}, include = always }. @@ -221,10 +221,11 @@ datetime_to_seconds(DateTime) -> store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> mod_fast_backend:store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token). --spec read_tokens(HostType, LServer, LUser, AgentId) -> {ok, tokens_data()} +-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 :: binary(). + AgentId :: agent_id(). read_tokens(HostType, LServer, LUser, AgentId) -> mod_fast_backend:read_tokens(HostType, LServer, LUser, AgentId). diff --git a/src/fast/mod_fast_backend.erl b/src/fast/mod_fast_backend.erl new file mode 100644 index 00000000000..c87a41d7994 --- /dev/null +++ b/src/fast/mod_fast_backend.erl @@ -0,0 +1,75 @@ +-module(mod_fast_backend). + +-export([init/2, + store_new_token/6, + read_tokens/4, + remove_user/3, + remove_domain/2]). + +-define(MAIN_MODULE, mod_fast). + +-callback init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. + +-callback store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: mod_token:agent_id(), + ExpireTS :: mod_token:seconds(), + Token :: mod_token:token(). + +-callback read_tokens(HostType, LServer, LUser, AgentId) -> + {ok, mod_fast:tokens_data()} | {error, not_found} + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: mod_fast: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], + 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) -> ok + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: mod_token:agent_id(), + ExpireTS :: mod_token:seconds(), + Token :: mod_token:token(). +store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> + Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token], + mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). + +-spec read_tokens(HostType, LServer, LUser, AgentId) -> + {ok, mod_fast:tokens_data()} | {error, not_found} + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: mod_fast:agent_id(). +read_tokens(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/mod_fast_rdbms.erl b/src/fast/mod_fast_rdbms.erl new file mode 100644 index 00000000000..db5e05c62ef --- /dev/null +++ b/src/fast/mod_fast_rdbms.erl @@ -0,0 +1,96 @@ +-module(mod_fast_rdbms). +-behaviour(mod_fast_backend). +-include("mongoose_logger.hrl"). + +-export([init/2, + store_new_token/6, + read_tokens/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) -> + Key = [<<"server">>, <<"username">>, <<"user_agent_id">>], + Upd = [<<"new_token">>, <<"new_expire">>, <<"new_count">>], + Ins = Key ++ Upd, + rdbms_queries:prepare_upsert(HostType, fast_upsert, fast_tokens, Ins, Upd, Key), + prepare(fast_select, fast_tokens, + [current_token, current_expire, current_count, + new_token, new_expire, new_count], + <<"SELECT " + "current_token, current_expire, current_count, " + "new_token, new_expire, new_count " + "FROM fast_tokens " + "WHERE server = ? AND username = ? AND user_agent_id = ?">>), + prepare(fast_remove_user, fast_tokens, + [server, username], + <<"DELETE FROM fast_tokens " + "WHERE server = ? AND username = ?">>), + prepare(fast_remove_domain, fast_tokens, + [server], + <<"DELETE FROM fast_tokens WHERE server = ?">>), + ok. + +-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: mod_token:agent_id(), + ExpireTS :: mod_token:seconds(), + Token :: mod_token:token(). +store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> + Key = [LServer, LUser, AgentId], + Upd = [Token, ExpireTS, 0], + Ins = Key ++ Upd, + rdbms_queries:execute_upsert(HostType, fast_upsert, Ins, Upd, Key), + ok. + +-spec read_tokens(HostType, LServer, LUser, AgentId) -> + {ok, mod_fast:tokens_data()} | {error, not_found} + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: mod_fast: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, + NewToken, NewExpire, NewCount}]} -> + Data = #{ + now_timestamp => mod_fast:utc_now_as_seconds(), + current_token => CurrentToken, + current_expire => maybe_to_integer(CurrentExpire), + current_count => maybe_to_integer(CurrentCount), + new_token => null_as_undefined(NewToken), + new_expire => null_as_undefined(NewExpire), + new_count => null_as_undefined(NewCount) + }, + {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). + +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 remove_domain(mongooseim:host_type(), jid:lserver()) -> ok. +remove_domain(HostType, LServer) -> + execute_successfully(HostType, fast_remove_domain, [LServer]), + ok. diff --git a/src/mod_sasl2.erl b/src/mod_sasl2.erl index 12a83a841c0..8dc44fadd42 100644 --- a/src/mod_sasl2.erl +++ b/src/mod_sasl2.erl @@ -46,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(), @@ -393,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 @@ -402,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/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index 21442b9ed0f..cf3fd9a5ad9 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -20,7 +20,7 @@ mechanism() -> mech_new(_Host, Creds, SocketData = #{sasl_state := SaslState}) -> SaslModState = mod_sasl2:get_mod_state(SaslState), case SaslModState of - #{id := AgentId} -> + #{encoded_id := AgentId} -> {ok, #state{creds = Creds, agent_id = AgentId}}; _ -> {error, <<"not-sasl2">>} diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 921f3273403..8a255160a33 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -637,9 +637,7 @@ all_modules() -> resume_timeout => 600, stale_h => #{enabled => true, geriatric => 3600, - repeat_after => 1800}}), - mod_fast => - mod_config(mod_fast, #{}) + repeat_after => 1800}}) }. custom_mod_event_pusher_http() -> @@ -869,6 +867,9 @@ 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) -> + #{backend => rdbms, + validity_period => #{access => #{unit => days, value => 3}}}; default_mod_config(mod_bind2) -> #{}; default_mod_config(mod_blocking) -> From 9ffd41f3d18d359118541ebba91632b43c71dc70 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 30 Sep 2024 22:32:19 +0200 Subject: [PATCH 09/46] Add fast_tokens table for MySQL/MSSQL --- priv/mssql2012.sql | 18 ++++++++++++++++++ priv/mysql.sql | 18 ++++++++++++++++++ priv/pg.sql | 3 --- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql index e686afe1f4a..a926b66fcdc 100644 --- a/priv/mssql2012.sql +++ b/priv/mssql2012.sql @@ -768,3 +768,21 @@ CREATE TABLE caps ( features text NOT NULL, PRIMARY KEY (node, sub_node) ); + +-- XEP-0484: Fast Authentication Streamlining Tokens +-- Module: mod_fast +CREATE TABLE fast_tokens( + 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 + new_token VARCHAR(250) NOT NULL, + new_expire BIGINT NOT NULL, -- seconds unix timestamp + new_count INT NOT NULL, + PRIMARY KEY(server, username, user_agent_id) +); diff --git a/priv/mysql.sql b/priv/mysql.sql index 664b5bae0a1..eaacd4f332a 100644 --- a/priv/mysql.sql +++ b/priv/mysql.sql @@ -557,3 +557,21 @@ CREATE TABLE caps ( features text NOT NULL, PRIMARY KEY (node, sub_node) ); + +-- XEP-0484: Fast Authentication Streamlining Tokens +-- Module: mod_fast +CREATE TABLE fast_tokens( + 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 + new_token VARCHAR(250) NOT NULL, + new_expire BIGINT NOT NULL, -- seconds unix timestamp + new_count INT NOT NULL, + PRIMARY KEY(server, username, user_agent_id) +); diff --git a/priv/pg.sql b/priv/pg.sql index ea54d66a640..35b698382cf 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -509,12 +509,9 @@ CREATE TABLE fast_tokens( -- Unique for each device -- https://xmpp.org/extensions/xep-0388.html#initiation user_agent_id VARCHAR(250) NOT NULL, - - -- slots current_token VARCHAR(250), current_expire BIGINT, -- seconds unix timestamp current_count INT, -- replay counter - new_token VARCHAR(250) NOT NULL, new_expire BIGINT NOT NULL, -- seconds unix timestamp new_count INT NOT NULL, From d146ee8c5843bc44b74ad78254dfa4eae2d495db Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 30 Sep 2024 22:42:45 +0200 Subject: [PATCH 10/46] Pass fast_SUITE suites --- big_tests/dynamic_domains.spec | 1 + big_tests/tests/fast_SUITE.erl | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index b88a9dc765d..76b539f07bc 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_SUITE}. {suites, "tests", bosh_SUITE}. {suites, "tests", carboncopy_SUITE}. {suites, "tests", connect_SUITE}. diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl index 2fcd035ca3b..b23722da57d 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_SUITE.erl @@ -35,8 +35,13 @@ groups() -> %%-------------------------------------------------------------------- init_per_suite(Config) -> - Config1 = load_modules(Config), - escalus:init_per_suite(Config1). + 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(), @@ -72,7 +77,6 @@ server_announces_fast(Config) -> {element, <<"inline">>}, {element_with_ns, <<"fast">>, ?NS_FAST}]), ?assertNotEqual(undefined, Fast), - ct:fail(Fast), ok. request_token_with_initial_authentication(Config) -> @@ -86,7 +90,6 @@ request_token_with_initial_authentication(Config) -> Token = exml_query:attr(Fast, <<"token">>), auth_with_token(Token, Config, Spec), ok. -% ct:fail({Expire, Token}). auth_and_request_token(Config, Client, Data) -> Extra = [request_token(), user_agent()], From 6ccd56dde8ab2cfb029d73bed3cd1a468c9e0c29 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 1 Oct 2024 00:44:44 +0200 Subject: [PATCH 11/46] Implement failed to login tests --- big_tests/tests/fast_SUITE.erl | 65 +++++++++++++++++++++++------ priv/mssql2012.sql | 6 +-- priv/mysql.sql | 6 +-- priv/pg.sql | 6 +-- src/fast/mod_fast_rdbms.erl | 6 +-- src/sasl/cyrsasl_ht_sha256_none.erl | 7 +++- 6 files changed, 70 insertions(+), 26 deletions(-) diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl index b23722da57d..38cf3d999e5 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_SUITE.erl @@ -26,7 +26,9 @@ groups() -> {basic, [parallel], [ server_announces_fast, - request_token_with_initial_authentication + request_token_with_initial_authentication, + token_auth_fails_when_token_is_wrong, + token_auth_fails_when_token_is_not_found ]} ]. @@ -88,7 +90,26 @@ request_token_with_initial_authentication(Config) -> Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]), Expire = exml_query:attr(Fast, <<"expire">>), Token = exml_query:attr(Fast, <<"token">>), - auth_with_token(Token, Config, Spec), + auth_with_token(true, Token, Config, Spec), + ok. + +token_auth_fails_when_token_is_wrong(Config) -> + %% New token is not set, but we try to login with a wrong one + Steps = [start_new_user, {?MODULE, auth_and_request_token}, + receive_features, has_no_more_stanzas], + #{answer := Success, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), + ?assertMatch(#xmlel{name = <<"success">>, + attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), + Token = <<"wrongtoken">>, + auth_with_token(false, Token, Config, Spec), + ok. + +token_auth_fails_when_token_is_not_found(Config) -> + %% New token is not set + Steps = [start_new_user, receive_features, has_no_more_stanzas], + #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), + Token = <<"wrongtoken">>, + auth_with_token(false, Token, Config, Spec), ok. auth_and_request_token(Config, Client, Data) -> @@ -105,15 +126,30 @@ request_token() -> attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"mechanism">>, <<"HT-SHA-256-NONE">>}]}. -auth_with_token(Token, Config, Spec) -> +auth_with_token(Success, Token, Config, Spec) -> Spec2 = [{secret_token, Token} | Spec], - Steps = [connect_tls, start_stream_get_features, - {?MODULE, auth_using_token}, - receive_features, has_no_more_stanzas], + Steps = steps(Success), Data = #{spec => Spec2}, - #{answer := Success} = sasl2_helper:apply_steps(Steps, Config, undefined, Data), - ?assertMatch(#xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success). + #{answer := Answer} = sasl2_helper:apply_steps(Steps, Config, undefined, Data), + case Success of + true -> + ?assertMatch(#xmlel{name = <<"success">>, + attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Answer); + false -> + ?assertMatch(#xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_SASL_2}], + children = [#xmlel{name = <<"not-authorized">>}]}, + Answer) + end. + +steps(true) -> + [connect_tls, start_stream_get_features, + {?MODULE, auth_using_token}, + receive_features, + has_no_more_stanzas]; +steps(false) -> + [connect_tls, start_stream_get_features, + {?MODULE, auth_using_token}]. user_agent() -> #xmlel{name = <<"user-agent">>, @@ -140,10 +176,15 @@ auth_with_method(_Config, Client, Data, BindElems, Extra, Method) -> Authenticate = auth_elem(Method, [InitEl, BindEl | Extra]), escalus:send(Client, Authenticate), Answer = escalus_client:wait_for_stanza(Client), - ct:pal("Answer ~p", [Answer]), + ct:log("Answer ~p", [Answer]), Identifier = exml_query:path(Answer, [{element, <<"authorization-identifier">>}, cdata]), - #jid{lresource = LResource} = jid:from_binary(Identifier), - {Client, Data#{answer => Answer, client_1_jid => Identifier, bind2_resource => LResource}}. + 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">>, diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql index a926b66fcdc..170f8aa3926 100644 --- a/priv/mssql2012.sql +++ b/priv/mssql2012.sql @@ -781,8 +781,8 @@ CREATE TABLE fast_tokens( current_token VARCHAR(250), current_expire BIGINT, -- seconds unix timestamp current_count INT, -- replay counter - new_token VARCHAR(250) NOT NULL, - new_expire BIGINT NOT NULL, -- seconds unix timestamp - new_count INT NOT NULL, + new_token VARCHAR(250), + new_expire BIGINT, -- seconds unix timestamp + new_count INT, PRIMARY KEY(server, username, user_agent_id) ); diff --git a/priv/mysql.sql b/priv/mysql.sql index eaacd4f332a..645ad397a7b 100644 --- a/priv/mysql.sql +++ b/priv/mysql.sql @@ -570,8 +570,8 @@ CREATE TABLE fast_tokens( current_token VARCHAR(250), current_expire BIGINT, -- seconds unix timestamp current_count INT, -- replay counter - new_token VARCHAR(250) NOT NULL, - new_expire BIGINT NOT NULL, -- seconds unix timestamp - new_count INT NOT NULL, + new_token VARCHAR(250), + new_expire BIGINT, -- seconds unix timestamp + new_count INT, PRIMARY KEY(server, username, user_agent_id) ); diff --git a/priv/pg.sql b/priv/pg.sql index 35b698382cf..9d7d0b28536 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -512,8 +512,8 @@ CREATE TABLE fast_tokens( current_token VARCHAR(250), current_expire BIGINT, -- seconds unix timestamp current_count INT, -- replay counter - new_token VARCHAR(250) NOT NULL, - new_expire BIGINT NOT NULL, -- seconds unix timestamp - new_count INT NOT NULL, + new_token VARCHAR(250), + new_expire BIGINT, -- seconds unix timestamp + new_count INT, PRIMARY KEY(server, username, user_agent_id) ); diff --git a/src/fast/mod_fast_rdbms.erl b/src/fast/mod_fast_rdbms.erl index db5e05c62ef..ea590898495 100644 --- a/src/fast/mod_fast_rdbms.erl +++ b/src/fast/mod_fast_rdbms.erl @@ -61,12 +61,12 @@ read_tokens(HostType, LServer, LUser, AgentId) -> NewToken, NewExpire, NewCount}]} -> Data = #{ now_timestamp => mod_fast:utc_now_as_seconds(), - current_token => CurrentToken, + current_token => null_as_undefined(CurrentToken), current_expire => maybe_to_integer(CurrentExpire), current_count => maybe_to_integer(CurrentCount), new_token => null_as_undefined(NewToken), - new_expire => null_as_undefined(NewExpire), - new_count => null_as_undefined(NewCount) + new_expire => maybe_to_integer(NewExpire), + new_count => maybe_to_integer(NewCount) }, {ok, Data}; Other -> diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index cf3fd9a5ad9..6df5b6ad6ab 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -88,5 +88,8 @@ handle_auth(#{ check_token(Token2, Shared) end. -check_token({Token, Expire, Count}, {NowTimestamp, ToHash, InitiatorHashedToken}) -> - crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken. +check_token({Token, Expire, Count}, {NowTimestamp, ToHash, InitiatorHashedToken}) + when is_binary(Token) -> + crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken; +check_token({undefined, _Expire, _Count}, _) -> + false. From 6bb9b4faf79416e28567cb1edc8a771933dd3900 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 1 Oct 2024 01:54:06 +0200 Subject: [PATCH 12/46] Store mechanism of tokens in DB --- big_tests/tests/fast_SUITE.erl | 19 +++++++++++--- priv/mssql2012.sql | 2 ++ priv/mysql.sql | 2 ++ priv/pg.sql | 2 ++ src/fast/mod_fast.erl | 26 +++++++++++-------- src/fast/mod_fast_backend.erl | 16 +++++++----- src/fast/mod_fast_rdbms.erl | 40 +++++++++++++++++++---------- src/sasl/cyrsasl_ht_sha256_none.erl | 21 +++++++++------ 8 files changed, 87 insertions(+), 41 deletions(-) diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl index 38cf3d999e5..7f016adefdd 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_SUITE.erl @@ -27,6 +27,7 @@ groups() -> [ server_announces_fast, request_token_with_initial_authentication, + request_token_with_unknown_mechanism_type, token_auth_fails_when_token_is_wrong, token_auth_fails_when_token_is_not_found ]} @@ -93,6 +94,17 @@ request_token_with_initial_authentication(Config) -> auth_with_token(true, Token, Config, Spec), ok. +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, has_no_more_stanzas], + #{answer := Success, spec := Spec} = 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), + ok. + token_auth_fails_when_token_is_wrong(Config) -> %% New token is not set, but we try to login with a wrong one Steps = [start_new_user, {?MODULE, auth_and_request_token}, @@ -113,7 +125,8 @@ token_auth_fails_when_token_is_not_found(Config) -> ok. auth_and_request_token(Config, Client, Data) -> - Extra = [request_token(), user_agent()], + 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) -> @@ -121,10 +134,10 @@ auth_using_token(Config, Client, Data) -> auth_with_method(Config, Client, Data, [], Extra, <<"HT-SHA-256-NONE">>). %% -request_token() -> +request_token(Mech) -> #xmlel{name = <<"request-token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, - {<<"mechanism">>, <<"HT-SHA-256-NONE">>}]}. + {<<"mechanism">>, Mech}]}. auth_with_token(Success, Token, Config, Spec) -> Spec2 = [{secret_token, Token} | Spec], diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql index 170f8aa3926..972717b35b9 100644 --- a/priv/mssql2012.sql +++ b/priv/mssql2012.sql @@ -781,8 +781,10 @@ CREATE TABLE fast_tokens( 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/mysql.sql b/priv/mysql.sql index 645ad397a7b..d62cd38a8e0 100644 --- a/priv/mysql.sql +++ b/priv/mysql.sql @@ -570,8 +570,10 @@ CREATE TABLE fast_tokens( 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 9d7d0b28536..8c1aa2c5199 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -512,8 +512,10 @@ CREATE TABLE fast_tokens( 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/fast/mod_fast.erl b/src/fast/mod_fast.erl index 75776358cdf..d30d74ca3f2 100644 --- a/src/fast/mod_fast.erl +++ b/src/fast/mod_fast.erl @@ -31,22 +31,26 @@ %% 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. --export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0]). +-export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0, + mechanism/0]). -type tokens_data() :: #{ now_timestamp := seconds(), current_token := token() | undefined, current_expire := seconds() | undefined, current_count := counter() | undefined, - new_token := token(), - new_expire := seconds(), - new_count := counter() + current_mech := mechanism() | undefined, + new_token := token() | undefined, + new_expire := seconds() | undefined, + new_count := counter() | undefined, + new_mech := mechanism() | undefined }. -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. @@ -168,13 +172,14 @@ sasl2_success(SaslAcc, C2SStateData, #{host_type := HostType}) -> end. %% Generate expirable auth token and store it in DB -make_fast_token_response(HostType, LServer, LUser, _Request, AgentId) -> +make_fast_token_response(HostType, LServer, LUser, Request, AgentId) -> + Mech = exml_query:attr(Request, <<"mechanism">>), TTLSeconds = get_ttl_seconds(HostType), NowTS = ?MODULE:utc_now_as_seconds(), ExpireTS = NowTS + TTLSeconds, Expire = seconds_to_binary(ExpireTS), Token = ?MODULE:generate_unique_token(), - store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token), + store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech), #xmlel{name = <<"token">>, attrs = [{<<"xmlns">>, ?NS_FAST}, {<<"expire">>, Expire}, {<<"token">>, Token}]}. @@ -211,15 +216,16 @@ generate_unique_token() -> datetime_to_seconds(DateTime) -> calendar:datetime_to_gregorian_seconds(DateTime). --spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok +-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), AgentId :: agent_id(), ExpireTS :: seconds(), - Token :: token(). -store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> - mod_fast_backend:store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token). + Token :: token(), + Mech :: mechanism(). +store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> + mod_fast_backend:store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech). -spec read_tokens(HostType, LServer, LUser, AgentId) -> {ok, tokens_data()} | {error, not_found} diff --git a/src/fast/mod_fast_backend.erl b/src/fast/mod_fast_backend.erl index c87a41d7994..b984d71ee23 100644 --- a/src/fast/mod_fast_backend.erl +++ b/src/fast/mod_fast_backend.erl @@ -1,7 +1,7 @@ -module(mod_fast_backend). -export([init/2, - store_new_token/6, + store_new_token/7, read_tokens/4, remove_user/3, remove_domain/2]). @@ -10,13 +10,14 @@ -callback init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. --callback store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok +-callback store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), AgentId :: mod_token:agent_id(), ExpireTS :: mod_token:seconds(), - Token :: mod_token:token(). + Token :: mod_token:token(), + Mech :: mod_token:mechanism(). -callback read_tokens(HostType, LServer, LUser, AgentId) -> {ok, mod_fast:tokens_data()} | {error, not_found} @@ -38,15 +39,16 @@ init(HostType, Opts) -> Args = [HostType, Opts], mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). --spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok +-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), AgentId :: mod_token:agent_id(), ExpireTS :: mod_token:seconds(), - Token :: mod_token:token(). -store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> - Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token], + Token :: mod_token:token(), + Mech :: mod_token:mechanism(). +store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> + Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech], mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). -spec read_tokens(HostType, LServer, LUser, AgentId) -> diff --git a/src/fast/mod_fast_rdbms.erl b/src/fast/mod_fast_rdbms.erl index ea590898495..9d92587909a 100644 --- a/src/fast/mod_fast_rdbms.erl +++ b/src/fast/mod_fast_rdbms.erl @@ -3,7 +3,7 @@ -include("mongoose_logger.hrl"). -export([init/2, - store_new_token/6, + store_new_token/7, read_tokens/4, remove_user/3, remove_domain/2]). @@ -13,15 +13,15 @@ -spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. init(HostType, _Opts) -> Key = [<<"server">>, <<"username">>, <<"user_agent_id">>], - Upd = [<<"new_token">>, <<"new_expire">>, <<"new_count">>], + Upd = [<<"new_token">>, <<"new_expire">>, <<"new_count">>, <<"new_mech_id">>], Ins = Key ++ Upd, rdbms_queries:prepare_upsert(HostType, fast_upsert, fast_tokens, Ins, Upd, Key), prepare(fast_select, fast_tokens, - [current_token, current_expire, current_count, - new_token, new_expire, new_count], + [current_token, current_expire, current_count, current_mech_id, + new_token, new_expire, new_count, new_mech_id], <<"SELECT " - "current_token, current_expire, current_count, " - "new_token, new_expire, new_count " + "current_token, current_expire, current_count, current_mech_id, " + "new_token, new_expire, new_count, new_mech_id " "FROM fast_tokens " "WHERE server = ? AND username = ? AND user_agent_id = ?">>), prepare(fast_remove_user, fast_tokens, @@ -33,16 +33,17 @@ init(HostType, _Opts) -> <<"DELETE FROM fast_tokens WHERE server = ?">>), ok. --spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> ok +-spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), AgentId :: mod_token:agent_id(), ExpireTS :: mod_token:seconds(), - Token :: mod_token:token(). -store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token) -> + Token :: mod_token:token(), + Mech :: mod_token:mechanism(). +store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> Key = [LServer, LUser, AgentId], - Upd = [Token, ExpireTS, 0], + Upd = [Token, ExpireTS, 0, mech_id(Mech)], Ins = Key ++ Upd, rdbms_queries:execute_upsert(HostType, fast_upsert, Ins, Upd, Key), ok. @@ -57,16 +58,18 @@ read_tokens(HostType, LServer, LUser, AgentId) -> case execute(HostType, fast_select, [LServer, LUser, AgentId]) of {selected, []} -> {error, not_found}; - {selected, [{CurrentToken, CurrentExpire, CurrentCount, - NewToken, NewExpire, NewCount}]} -> + {selected, [{CurrentToken, CurrentExpire, CurrentCount, CurrentMechId, + NewToken, NewExpire, NewCount, NewMechId}]} -> Data = #{ now_timestamp => mod_fast: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_count => maybe_to_integer(NewCount), + new_mech => maybe_to_mech(NewMechId) }, {ok, Data}; Other -> @@ -80,6 +83,12 @@ maybe_to_integer(null) -> 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) -> @@ -94,3 +103,8 @@ remove_user(HostType, LUser, LServer) -> remove_domain(HostType, LServer) -> execute_successfully(HostType, fast_remove_domain, [LServer]), ok. + +mech_id(<<"HT-SHA-256-NONE">>) -> 1. + +mech_name(1) -> <<"HT-SHA-256-NONE">>; +mech_name(_) -> <<"UNKNOWN-MECH">>. %% Just in case DB has an unknown mech_id diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index 6df5b6ad6ab..22bcb89dbf3 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -34,6 +34,7 @@ format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). ClientIn :: binary()) -> {ok, mongoose_credentials:t()} | {error, binary()}. mech_step(#state{creds = Creds, agent_id = AgentId}, SerializedToken) -> + Mech = mechanism(), %% SerializedToken is base64 decoded. Parts = binary:split(SerializedToken, <<0>>), ?LOG_ERROR(#{what => cyrsasl_ht_sha256_none, creds => Creds, ser_token => SerializedToken, parts => Parts, agent_id => AgentId}), @@ -45,7 +46,7 @@ mech_step(#state{creds = Creds, agent_id = AgentId}, SerializedToken) -> {ok, TokenData} -> ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), CBData = <<>>, - case handle_auth(TokenData, InitiatorHashedToken, CBData) of + case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of true -> {ok, mongoose_credentials:extend(Creds, [{username, LUser}, @@ -73,14 +74,16 @@ handle_auth(#{ current_token := CurrentToken, current_expire := CurrentExpire, current_count := CurrentCount, + current_mech := CurrentMech, new_token := NewToken, new_expire := NewExpire, - new_count := NewCount - }, InitiatorHashedToken, CBData) -> + new_count := NewCount, + new_mech := NewMech + }, InitiatorHashedToken, CBData, Mech) -> ToHash = <<"Initiator", CBData/binary>>, - Token1 = {NewToken, NewExpire, NewCount}, - Token2 = {CurrentToken, CurrentExpire, CurrentCount}, - Shared = {NowTimestamp, ToHash, InitiatorHashedToken}, + Token1 = {NewToken, NewExpire, NewCount, NewMech}, + Token2 = {CurrentToken, CurrentExpire, CurrentCount, CurrentMech}, + Shared = {NowTimestamp, ToHash, InitiatorHashedToken, Mech}, case check_token(Token1, Shared) of true -> true; @@ -88,8 +91,10 @@ handle_auth(#{ check_token(Token2, Shared) end. -check_token({Token, Expire, Count}, {NowTimestamp, ToHash, InitiatorHashedToken}) +%% 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) -> crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken; -check_token({undefined, _Expire, _Count}, _) -> +check_token(_, _) -> false. From 2c476357633293398ecc36c7c25d167fc03fdc28 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 1 Oct 2024 02:21:31 +0200 Subject: [PATCH 13/46] Move code into mod_fast_generic --- big_tests/tests/fast_SUITE.erl | 8 +-- src/fast/mod_fast_generic.erl | 89 ++++++++++++++++++++++++++++ src/sasl/cyrsasl_ht_sha256_none.erl | 91 +++-------------------------- 3 files changed, 100 insertions(+), 88 deletions(-) create mode 100644 src/fast/mod_fast_generic.erl diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl index 7f016adefdd..bb8c193734e 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_SUITE.erl @@ -84,7 +84,7 @@ server_announces_fast(Config) -> request_token_with_initial_authentication(Config) -> Steps = [start_new_user, {?MODULE, auth_and_request_token}, - receive_features, has_no_more_stanzas], + receive_features], #{answer := Success, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), ?assertMatch(#xmlel{name = <<"success">>, attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), @@ -97,7 +97,7 @@ request_token_with_initial_authentication(Config) -> 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, has_no_more_stanzas], + receive_features], #{answer := Success, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), ?assertMatch(#xmlel{name = <<"success">>, attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), @@ -108,7 +108,7 @@ request_token_with_unknown_mechanism_type(Config0) -> token_auth_fails_when_token_is_wrong(Config) -> %% New token is not set, but we try to login with a wrong one Steps = [start_new_user, {?MODULE, auth_and_request_token}, - receive_features, has_no_more_stanzas], + receive_features], #{answer := Success, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), ?assertMatch(#xmlel{name = <<"success">>, attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), @@ -118,7 +118,7 @@ token_auth_fails_when_token_is_wrong(Config) -> token_auth_fails_when_token_is_not_found(Config) -> %% New token is not set - Steps = [start_new_user, receive_features, has_no_more_stanzas], + Steps = [start_new_user, receive_features], #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), Token = <<"wrongtoken">>, auth_with_token(false, Token, Config, Spec), diff --git a/src/fast/mod_fast_generic.erl b/src/fast/mod_fast_generic.erl new file mode 100644 index 00000000000..867fcf3f362 --- /dev/null +++ b/src/fast/mod_fast_generic.erl @@ -0,0 +1,89 @@ +-module(mod_fast_generic). + +-export([mech_new/4, mech_step/2]). + +-record(state, {creds, agent_id, mechanism}). +-include("mongoose.hrl"). +-type state() :: #state{}. + +-spec mech_new(Host :: jid:server(), + Creds :: mongoose_credentials:t(), + SocketData :: term(), + Mech :: mod_fast:mechanism()) -> {ok, state()} | {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, #state{creds = Creds, agent_id = AgentId, mechanism = Mech}}; + _ -> + {error, <<"not-sasl2">>} + end; +mech_new(_Host, _Creds, _SocketData, _Mech) -> + {error, <<"not-sasl2">>}. + +-spec mech_step(State :: #state{}, + ClientIn :: binary()) -> {ok, mongoose_credentials:t()} + | {error, binary()}. +mech_step(#state{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:read_tokens(HostType, LServer, LUser, AgentId) of + {ok, TokenData} -> + ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), + CBData = <<>>, + case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of + true -> + {ok, mongoose_credentials:extend(Creds, + [{username, LUser}, + {auth_module, ?MODULE}])}; + 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). +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>>, + Token1 = {NewToken, NewExpire, NewCount, NewMech}, + Token2 = {CurrentToken, CurrentExpire, CurrentCount, CurrentMech}, + Shared = {NowTimestamp, ToHash, InitiatorHashedToken, Mech}, + case check_token(Token1, Shared) of + true -> + true; + false -> + check_token(Token2, Shared) + 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) -> + crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken; +check_token(_, _) -> + false. diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index 22bcb89dbf3..f31112bec5d 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -1,13 +1,9 @@ -module(cyrsasl_ht_sha256_none). +-behaviour(cyrsasl). -export([mechanism/0, mech_new/3, mech_step/2]). - -ignore_xref([mech_new/3]). --behaviour(cyrsasl). - --record(state, {creds, agent_id}). - -include("mongoose.hrl"). -spec mechanism() -> cyrsasl:mechanism(). @@ -16,85 +12,12 @@ mechanism() -> -spec mech_new(Host :: jid:server(), Creds :: mongoose_credentials:t(), - SocketData :: term()) -> {ok, tuple()}. -mech_new(_Host, Creds, SocketData = #{sasl_state := SaslState}) -> - SaslModState = mod_sasl2:get_mod_state(SaslState), - case SaslModState of - #{encoded_id := AgentId} -> - {ok, #state{creds = Creds, agent_id = AgentId}}; - _ -> - {error, <<"not-sasl2">>} - end; -mech_new(_Host, _Creds, _SocketData) -> - {error, <<"not-sasl2">>}. + SocketData :: term()) -> {ok, tuple()} | {error, binary()}. +mech_new(Host, Creds, SocketData) -> + mod_fast_generic:mech_new(Host, Creds, SocketData, mechanism()). -format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). - --spec mech_step(State :: #state{}, +-spec mech_step(State :: tuple(), ClientIn :: binary()) -> {ok, mongoose_credentials:t()} | {error, binary()}. -mech_step(#state{creds = Creds, agent_id = AgentId}, SerializedToken) -> - Mech = mechanism(), - %% SerializedToken is base64 decoded. - Parts = binary:split(SerializedToken, <<0>>), - ?LOG_ERROR(#{what => cyrsasl_ht_sha256_none, creds => Creds, ser_token => SerializedToken, parts => Parts, agent_id => AgentId}), - [Username, InitiatorHashedToken] = Parts, - HostType = mongoose_credentials:host_type(Creds), - LServer = mongoose_credentials:lserver(Creds), - LUser = jid:nodeprep(Username), - case mod_fast:read_tokens(HostType, LServer, LUser, AgentId) of - {ok, TokenData} -> - ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), - CBData = <<>>, - case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of - true -> - {ok, mongoose_credentials:extend(Creds, - [{username, LUser}, - {auth_module, ?MODULE}])}; - 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). -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>>, - Token1 = {NewToken, NewExpire, NewCount, NewMech}, - Token2 = {CurrentToken, CurrentExpire, CurrentCount, CurrentMech}, - Shared = {NowTimestamp, ToHash, InitiatorHashedToken, Mech}, - case check_token(Token1, Shared) of - true -> - true; - false -> - check_token(Token2, Shared) - 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) -> - crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken; -check_token(_, _) -> - false. +mech_step(State, SerializedToken) -> + mod_fast_generic:mech_step(State, SerializedToken). From a20a325212fbc8d272f913c2e45d67daccc92308 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 11:13:30 +0100 Subject: [PATCH 14/46] Validate token datetime --- big_tests/tests/fast_SUITE.erl | 39 +++++++++++++++++++++++++++------- big_tests/tests/gdpr_SUITE.erl | 24 +-------------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_SUITE.erl index bb8c193734e..ee85a5ee064 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_SUITE.erl @@ -1,3 +1,4 @@ +%% Tests for XEP-0484: Fast Authentication Streamlining Tokens -module(fast_SUITE). -compile([export_all, nowarn_export_all]). @@ -25,9 +26,10 @@ groups() -> [ {basic, [parallel], [ - server_announces_fast, + server_advertises_support_for_fast, request_token_with_initial_authentication, request_token_with_unknown_mechanism_type, + client_authenticates_using_fast, token_auth_fails_when_token_is_wrong, token_auth_fails_when_token_is_not_found ]} @@ -73,15 +75,20 @@ load_modules(Config) -> %% tests %%-------------------------------------------------------------------- -server_announces_fast(Config) -> +%% 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), - ok. + ?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) -> Steps = [start_new_user, {?MODULE, auth_and_request_token}, receive_features], @@ -91,8 +98,9 @@ request_token_with_initial_authentication(Config) -> Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]), Expire = exml_query:attr(Fast, <<"expire">>), Token = exml_query:attr(Fast, <<"token">>), - auth_with_token(true, Token, Config, Spec), - ok. + ?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], @@ -102,8 +110,19 @@ request_token_with_unknown_mechanism_type(Config0) -> ?assertMatch(#xmlel{name = <<"success">>, attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]), - ?assertEqual(undefined, Fast), - ok. + ?assertEqual(undefined, Fast). + +%% 3.4 Client authenticates using FAST +%% https://xmpp.org/extensions/xep-0484.html#fast-auth +client_authenticates_using_fast(Config) -> + Steps = [start_new_user, {?MODULE, auth_and_request_token}, + receive_features], + #{answer := Success, spec := Spec} = 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}]), + Token = exml_query:attr(Fast, <<"token">>), + auth_with_token(true, 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 @@ -124,6 +143,10 @@ token_auth_fails_when_token_is_not_found(Config) -> auth_with_token(false, Token, Config, Spec), ok. +%%-------------------------------------------------------------------- +%% helpers +%%-------------------------------------------------------------------- + auth_and_request_token(Config, Client, Data) -> Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>), Extra = [request_token(Mech), user_agent()], 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"]. From 580d84a85a261478af0c77f7cbd7bb7124768a01 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 11:15:38 +0100 Subject: [PATCH 15/46] Rename fast_SUITE to fast_token_auth_SUITE Rename mod_fast to mod_fast_token_auth --- big_tests/default.spec | 2 +- big_tests/dynamic_domains.spec | 2 +- .../{fast_SUITE.erl => fast_auth_token_SUITE.erl} | 2 +- big_tests/tests/sasl2_helper.erl | 2 +- .../mod_fast_auth_token.erl} | 13 +++++++------ .../mod_fast_auth_token_backend.erl} | 12 ++++++------ .../mod_fast_auth_token_generic.erl} | 6 +++--- .../mod_fast_auth_token_rdbms.erl} | 10 +++++----- src/sasl/cyrsasl_ht_sha256_none.erl | 4 ++-- test/common/config_parser_helper.erl | 2 +- 10 files changed, 28 insertions(+), 27 deletions(-) rename big_tests/tests/{fast_SUITE.erl => fast_auth_token_SUITE.erl} (99%) rename src/{fast/mod_fast.erl => fast_auth_token/mod_fast_auth_token.erl} (95%) rename src/{fast/mod_fast_backend.erl => fast_auth_token/mod_fast_auth_token_backend.erl} (89%) rename src/{fast/mod_fast_generic.erl => fast_auth_token/mod_fast_auth_token_generic.erl} (94%) rename src/{fast/mod_fast_rdbms.erl => fast_auth_token/mod_fast_auth_token_rdbms.erl} (93%) diff --git a/big_tests/default.spec b/big_tests/default.spec index cbb6e50fa22..cb857b274c6 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -17,7 +17,7 @@ {suites, "tests", amp_big_SUITE}. {suites, "tests", anonymous_SUITE}. {suites, "tests", bind2_SUITE}. -{suites, "tests", fast_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 76b539f07bc..7125ebbfd01 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -16,7 +16,7 @@ {suites, "tests", amp_big_SUITE}. {suites, "tests", anonymous_SUITE}. {suites, "tests", bind2_SUITE}. -{suites, "tests", fast_SUITE}. +{suites, "tests", fast_auth_token_SUITE}. {suites, "tests", bosh_SUITE}. {suites, "tests", carboncopy_SUITE}. {suites, "tests", connect_SUITE}. diff --git a/big_tests/tests/fast_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl similarity index 99% rename from big_tests/tests/fast_SUITE.erl rename to big_tests/tests/fast_auth_token_SUITE.erl index ee85a5ee064..dcd84332da8 100644 --- a/big_tests/tests/fast_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -1,5 +1,5 @@ %% Tests for XEP-0484: Fast Authentication Streamlining Tokens --module(fast_SUITE). +-module(fast_auth_token_SUITE). -compile([export_all, nowarn_export_all]). diff --git a/big_tests/tests/sasl2_helper.erl b/big_tests/tests/sasl2_helper.erl index 122ad23c784..5a669555d8f 100644 --- a/big_tests/tests/sasl2_helper.erl +++ b/big_tests/tests/sasl2_helper.erl @@ -28,7 +28,7 @@ load_all_sasl2_modules(HostType) -> rdbms_mods() -> case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of true -> - [{mod_fast, mod_config(mod_fast, #{backend => rdbms})}]; + [{mod_fast_auth_token, mod_config(mod_fast_auth_token, #{backend => rdbms})}]; false -> [] end. diff --git a/src/fast/mod_fast.erl b/src/fast_auth_token/mod_fast_auth_token.erl similarity index 95% rename from src/fast/mod_fast.erl rename to src/fast_auth_token/mod_fast_auth_token.erl index d30d74ca3f2..b98996c87ac 100644 --- a/src/fast/mod_fast.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -1,4 +1,4 @@ --module(mod_fast). +-module(mod_fast_auth_token). -xep([{xep, 484}, {version, "0.2.0"}]). -behaviour(gen_mod). -include("mongoose_ns.hrl"). @@ -55,7 +55,7 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) -> - mod_fast_backend:init(HostType, Opts), + mod_fast_auth_token_backend:init(HostType, Opts), ok. -spec stop(mongooseim:host_type()) -> ok. @@ -103,7 +103,7 @@ supported_features() -> [dynamic_domains]. Params :: map(), Extra :: gen_hook:extra(). remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type := HostType}) -> - mod_fast_backend:remove_user(HostType, LUser, LServer), + mod_fast_auth_token_backend:remove_user(HostType, LUser, LServer), {ok, Acc}. -spec remove_domain(Acc, Params, Extra) -> {ok, Acc} when @@ -111,7 +111,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}) -> - mod_fast_backend:remove_domain(HostType, Domain), + mod_fast_auth_token_backend:remove_domain(HostType, Domain), {ok, Acc}. -spec sasl2_stream_features(Acc, #{c2s_data := mongoose_c2s:data()}, gen_hook:extra()) -> @@ -225,7 +225,8 @@ datetime_to_seconds(DateTime) -> Token :: token(), Mech :: mechanism(). store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> - mod_fast_backend:store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech). + mod_fast_auth_token_backend:store_new_token( + HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech). -spec read_tokens(HostType, LServer, LUser, AgentId) -> {ok, tokens_data()} | {error, not_found} @@ -234,4 +235,4 @@ store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> LUser :: jid:luser(), AgentId :: agent_id(). read_tokens(HostType, LServer, LUser, AgentId) -> - mod_fast_backend:read_tokens(HostType, LServer, LUser, AgentId). + mod_fast_auth_token_backend:read_tokens(HostType, LServer, LUser, AgentId). diff --git a/src/fast/mod_fast_backend.erl b/src/fast_auth_token/mod_fast_auth_token_backend.erl similarity index 89% rename from src/fast/mod_fast_backend.erl rename to src/fast_auth_token/mod_fast_auth_token_backend.erl index b984d71ee23..b2020ba7e59 100644 --- a/src/fast/mod_fast_backend.erl +++ b/src/fast_auth_token/mod_fast_auth_token_backend.erl @@ -1,4 +1,4 @@ --module(mod_fast_backend). +-module(mod_fast_auth_token_backend). -export([init/2, store_new_token/7, @@ -6,7 +6,7 @@ remove_user/3, remove_domain/2]). --define(MAIN_MODULE, mod_fast). +-define(MAIN_MODULE, mod_fast_auth_token). -callback init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. @@ -20,11 +20,11 @@ Mech :: mod_token:mechanism(). -callback read_tokens(HostType, LServer, LUser, AgentId) -> - {ok, mod_fast:tokens_data()} | {error, not_found} + {ok, mod_fast_auth_token:tokens_data()} | {error, not_found} when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: mod_fast:agent_id(). + AgentId :: mod_fast_auth_token:agent_id(). -callback remove_user(mongooseim:host_type(), jid:luser(), jid:lserver()) -> ok. @@ -52,11 +52,11 @@ store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). -spec read_tokens(HostType, LServer, LUser, AgentId) -> - {ok, mod_fast:tokens_data()} | {error, not_found} + {ok, mod_fast_auth_token:tokens_data()} | {error, not_found} when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: mod_fast:agent_id(). + 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). diff --git a/src/fast/mod_fast_generic.erl b/src/fast_auth_token/mod_fast_auth_token_generic.erl similarity index 94% rename from src/fast/mod_fast_generic.erl rename to src/fast_auth_token/mod_fast_auth_token_generic.erl index 867fcf3f362..358f4f01d46 100644 --- a/src/fast/mod_fast_generic.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic.erl @@ -1,4 +1,4 @@ --module(mod_fast_generic). +-module(mod_fast_auth_token_generic). -export([mech_new/4, mech_step/2]). @@ -9,7 +9,7 @@ -spec mech_new(Host :: jid:server(), Creds :: mongoose_credentials:t(), SocketData :: term(), - Mech :: mod_fast:mechanism()) -> {ok, state()} | {error, binary()}. + Mech :: mod_fast_auth_token:mechanism()) -> {ok, state()} | {error, binary()}. mech_new(_Host, Creds, SocketData = #{sasl_state := SaslState}, Mech) -> SaslModState = mod_sasl2:get_mod_state(SaslState), case SaslModState of @@ -31,7 +31,7 @@ mech_step(#state{creds = Creds, agent_id = AgentId, mechanism = Mech}, Serialize HostType = mongoose_credentials:host_type(Creds), LServer = mongoose_credentials:lserver(Creds), LUser = jid:nodeprep(Username), - case mod_fast:read_tokens(HostType, LServer, LUser, AgentId) of + case mod_fast_auth_token:read_tokens(HostType, LServer, LUser, AgentId) of {ok, TokenData} -> ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), CBData = <<>>, diff --git a/src/fast/mod_fast_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl similarity index 93% rename from src/fast/mod_fast_rdbms.erl rename to src/fast_auth_token/mod_fast_auth_token_rdbms.erl index 9d92587909a..04ed818b715 100644 --- a/src/fast/mod_fast_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -1,5 +1,5 @@ --module(mod_fast_rdbms). --behaviour(mod_fast_backend). +-module(mod_fast_auth_token_rdbms). +-behaviour(mod_fast_auth_token_backend). -include("mongoose_logger.hrl"). -export([init/2, @@ -49,11 +49,11 @@ store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok. -spec read_tokens(HostType, LServer, LUser, AgentId) -> - {ok, mod_fast:tokens_data()} | {error, not_found} + {ok, mod_fast_auth_token:tokens_data()} | {error, not_found} when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: mod_fast:agent_id(). + AgentId :: mod_fast_auth_token:agent_id(). read_tokens(HostType, LServer, LUser, AgentId) -> case execute(HostType, fast_select, [LServer, LUser, AgentId]) of {selected, []} -> @@ -61,7 +61,7 @@ read_tokens(HostType, LServer, LUser, AgentId) -> {selected, [{CurrentToken, CurrentExpire, CurrentCount, CurrentMechId, NewToken, NewExpire, NewCount, NewMechId}]} -> Data = #{ - now_timestamp => mod_fast:utc_now_as_seconds(), + 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), diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index f31112bec5d..37f6564a43b 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -14,10 +14,10 @@ mechanism() -> Creds :: mongoose_credentials:t(), SocketData :: term()) -> {ok, tuple()} | {error, binary()}. mech_new(Host, Creds, SocketData) -> - mod_fast_generic:mech_new(Host, Creds, SocketData, mechanism()). + mod_fast_auth_token_generic: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_generic:mech_step(State, SerializedToken). + mod_fast_auth_token_generic:mech_step(State, SerializedToken). diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 8a255160a33..990b36a027f 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -867,7 +867,7 @@ 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) -> +default_mod_config(mod_fast_auth_token) -> #{backend => rdbms, validity_period => #{access => #{unit => days, value => 3}}}; default_mod_config(mod_bind2) -> From 7aa0bffffd0bd2877b7e0e898da541719cefa251 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 11:39:41 +0100 Subject: [PATCH 16/46] Rename table fast_tokens to fast_auth_token --- priv/mssql2012.sql | 4 ++-- priv/mysql.sql | 4 ++-- priv/pg.sql | 4 ++-- src/fast_auth_token/mod_fast_auth_token_rdbms.erl | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql index 972717b35b9..92e7a442549 100644 --- a/priv/mssql2012.sql +++ b/priv/mssql2012.sql @@ -770,8 +770,8 @@ CREATE TABLE caps ( ); -- XEP-0484: Fast Authentication Streamlining Tokens --- Module: mod_fast -CREATE TABLE fast_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) diff --git a/priv/mysql.sql b/priv/mysql.sql index d62cd38a8e0..e53e70a3f3c 100644 --- a/priv/mysql.sql +++ b/priv/mysql.sql @@ -559,8 +559,8 @@ CREATE TABLE caps ( ); -- XEP-0484: Fast Authentication Streamlining Tokens --- Module: mod_fast -CREATE TABLE fast_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) diff --git a/priv/pg.sql b/priv/pg.sql index 8c1aa2c5199..9de3017fb06 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -501,8 +501,8 @@ CREATE TABLE caps ( ); -- XEP-0484: Fast Authentication Streamlining Tokens --- Module: mod_fast -CREATE TABLE fast_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) diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl index 04ed818b715..2970681afb7 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -15,22 +15,22 @@ init(HostType, _Opts) -> 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_tokens, Ins, Upd, Key), - prepare(fast_select, fast_tokens, + rdbms_queries:prepare_upsert(HostType, fast_upsert, fast_auth_token, Ins, Upd, Key), + prepare(fast_select, fast_auth_token, [current_token, current_expire, current_count, current_mech_id, new_token, new_expire, new_count, new_mech_id], <<"SELECT " "current_token, current_expire, current_count, current_mech_id, " "new_token, new_expire, new_count, new_mech_id " - "FROM fast_tokens " + "FROM fast_auth_token " "WHERE server = ? AND username = ? AND user_agent_id = ?">>), - prepare(fast_remove_user, fast_tokens, + prepare(fast_remove_user, fast_auth_token, [server, username], - <<"DELETE FROM fast_tokens " + <<"DELETE FROM fast_auth_token " "WHERE server = ? AND username = ?">>), - prepare(fast_remove_domain, fast_tokens, + prepare(fast_remove_domain, fast_auth_token, [server], - <<"DELETE FROM fast_tokens WHERE server = ?">>), + <<"DELETE FROM fast_auth_token WHERE server = ?">>), ok. -spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok From b496a330b3a1b9b824c86878da927fadd00cf98a Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 20:30:50 +0100 Subject: [PATCH 17/46] Add time_helper --- big_tests/src/time_helper.erl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 big_tests/src/time_helper.erl 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). From 2c5f7c59f8bcd5d0c0e6459824a162d47cbf6d43 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 20:46:12 +0100 Subject: [PATCH 18/46] Fix expire date is way too much in future --- src/fast_auth_token/mod_fast_auth_token.erl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index b98996c87ac..1c8ddf864ce 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -191,7 +191,7 @@ seconds_to_binary(Secs) -> -spec utc_now_as_seconds() -> seconds(). utc_now_as_seconds() -> - datetime_to_seconds(calendar:universal_time()). + erlang:system_time(second). -spec get_ttl_seconds(mongooseim:host_type()) -> seconds(). get_ttl_seconds(HostType) -> @@ -212,10 +212,6 @@ period_to_seconds(Seconds, seconds) -> Seconds. generate_unique_token() -> base64:encode(crypto:strong_rand_bytes(25)). --spec datetime_to_seconds(calendar:datetime()) -> seconds(). -datetime_to_seconds(DateTime) -> - calendar:datetime_to_gregorian_seconds(DateTime). - -spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), From 85f44984355e3ce5be9ee4369aa8924567d89e6c Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 21:00:30 +0100 Subject: [PATCH 19/46] Add client_authenticate_several_times_with_the_same_token testcase Add connect_and_ask_for_token helper --- big_tests/tests/fast_auth_token_SUITE.erl | 44 ++++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index dcd84332da8..6c6d46577ea 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -30,6 +30,7 @@ groups() -> 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 ]} @@ -90,14 +91,7 @@ server_advertises_support_for_fast(Config) -> %% 3.3 Server provides token to client %% https://xmpp.org/extensions/xep-0484.html#token-response request_token_with_initial_authentication(Config) -> - Steps = [start_new_user, {?MODULE, auth_and_request_token}, - receive_features], - #{answer := Success, spec := Spec} = 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}]), - Expire = exml_query:attr(Fast, <<"expire">>), - Token = exml_query:attr(Fast, <<"token">>), + #{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))). @@ -106,7 +100,7 @@ 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, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), + #{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}]), @@ -115,22 +109,19 @@ request_token_with_unknown_mechanism_type(Config0) -> %% 3.4 Client authenticates using FAST %% https://xmpp.org/extensions/xep-0484.html#fast-auth client_authenticates_using_fast(Config) -> - Steps = [start_new_user, {?MODULE, auth_and_request_token}, - receive_features], - #{answer := Success, spec := Spec} = 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}]), - Token = exml_query:attr(Fast, <<"token">>), + #{token := Token, spec := Spec} = connect_and_ask_for_token(Config), + auth_with_token(true, 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(true, Token, Config, Spec), + auth_with_token(true, Token, Config, Spec), auth_with_token(true, 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 - Steps = [start_new_user, {?MODULE, auth_and_request_token}, - receive_features], - #{answer := Success, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), - ?assertMatch(#xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), + #{spec := Spec} = connect_and_ask_for_token(Config), Token = <<"wrongtoken">>, auth_with_token(false, Token, Config, Spec), ok. @@ -147,6 +138,17 @@ token_auth_fails_when_token_is_not_found(Config) -> %% helpers %%-------------------------------------------------------------------- +connect_and_ask_for_token(Config) -> + Steps = [start_new_user, {?MODULE, auth_and_request_token}, + receive_features], + #{answer := Success, spec := Spec} = 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}]), + 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()], From 1891b4b83d8ef3c0791fc314c1bd970fce5ef7d8 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 7 Jan 2025 22:41:59 +0100 Subject: [PATCH 20/46] Pass creds into sasl2_success Pass token_type_used, token_data into mod_fast_auth_token:sasl2_success --- big_tests/tests/fast_auth_token_SUITE.erl | 45 ++++++++++++++++--- src/c2s/mongoose_c2s_sasl.erl | 5 ++- src/fast_auth_token/mod_fast_auth_token.erl | 8 +++- .../mod_fast_auth_token_generic.erl | 22 ++++++--- src/hooks/mongoose_hooks.erl | 9 ++-- src/mod_sasl2.erl | 4 +- 6 files changed, 70 insertions(+), 23 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 6c6d46577ea..018b92c99b8 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -32,7 +32,8 @@ groups() -> 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 + token_auth_fails_when_token_is_not_found, + server_initiates_token_rotation ]} ]. @@ -123,15 +124,36 @@ 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(false, Token, Config, Spec), - ok. + auth_with_token(false, Token, Config, Spec). token_auth_fails_when_token_is_not_found(Config) -> %% New token is not set Steps = [start_new_user, receive_features], #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), Token = <<"wrongtoken">>, - auth_with_token(false, Token, Config, Spec), + auth_with_token(false, 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. +server_initiates_token_rotation(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 = <<"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], + ok = distributed_helper:rpc(distributed_helper:mim(), mod_fast_auth_token_backend, store_new_token, Args), + Res = auth_with_token(true, Token, Config, Spec), + %% TODO finish writing this, when we have a mechanism to set token into the current slot ok. %%-------------------------------------------------------------------- @@ -168,7 +190,8 @@ auth_with_token(Success, Token, Config, Spec) -> Spec2 = [{secret_token, Token} | Spec], Steps = steps(Success), Data = #{spec => Spec2}, - #{answer := Answer} = sasl2_helper:apply_steps(Steps, Config, undefined, Data), + Res = sasl2_helper:apply_steps(Steps, Config, undefined, Data), + #{answer := Answer} = Res, case Success of true -> ?assertMatch(#xmlel{name = <<"success">>, @@ -178,7 +201,8 @@ auth_with_token(Success, Token, Config, Spec) -> attrs = [{<<"xmlns">>, ?NS_SASL_2}], children = [#xmlel{name = <<"not-authorized">>}]}, Answer) - end. + end, + Res. steps(true) -> [connect_tls, start_stream_get_features, @@ -189,9 +213,12 @@ steps(false) -> [connect_tls, start_stream_get_features, {?MODULE, auth_using_token}]. +user_agent_id() -> + <<"d4565fa7-4d72-4749-b3d3-740edbf87770">>. + user_agent() -> #xmlel{name = <<"user-agent">>, - attrs = [{<<"id">>, <<"d4565fa7-4d72-4749-b3d3-740edbf87770">>}], + attrs = [{<<"id">>, user_agent_id()}], children = [cdata_elem(<<"software">>, <<"AwesomeXMPP">>), cdata_elem(<<"device">>, <<"Kiva's Phone">>)]}. @@ -255,3 +282,7 @@ 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/src/c2s/mongoose_c2s_sasl.erl b/src/c2s/mongoose_c2s_sasl.erl index 4dea9d830ea..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()}. @@ -81,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/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 1c8ddf864ce..125896d098a 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -139,6 +139,7 @@ mechanisms() -> -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). sasl2_start(SaslAcc, #{stanza := El}, _) -> + %% TODO remove this log ?LOG_ERROR(#{what => sasl2_startttt, elleee => El, sasla_acc => SaslAcc}), AgentId = exml_query:path(El, [{element, <<"user-agent">>}, {attr, <<"id">>}]), SaslAcc2 = mongoose_acc:set(?MODULE, agent_id, AgentId, SaslAcc), @@ -155,9 +156,14 @@ sasl2_start(SaslAcc, #{stanza := El}, _) -> end end. +format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). + -spec sasl2_success(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). -sasl2_success(SaslAcc, C2SStateData, #{host_type := HostType}) -> +sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType}) -> + %% TODO remove this log + ?LOG_ERROR(#{what => sasl2_success_debug, sasl_acc => format_term(SaslAcc), c2s_state_data => format_term(C2SStateData), + creds => format_term(Creds)}), #{c2s_data := C2SData} = C2SStateData, #jid{luser = LUser, lserver = LServer} = mongoose_c2s:get_jid(C2SData), case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of diff --git a/src/fast_auth_token/mod_fast_auth_token_generic.erl b/src/fast_auth_token/mod_fast_auth_token_generic.erl index 358f4f01d46..b28dc2d7f26 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic.erl @@ -33,13 +33,16 @@ mech_step(#state{creds = Creds, agent_id = AgentId, mechanism = Mech}, Serialize LUser = jid:nodeprep(Username), case mod_fast_auth_token:read_tokens(HostType, LServer, LUser, AgentId) of {ok, TokenData} -> + %% TODO remove this log when done coding ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), CBData = <<>>, case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of - true -> + {true, TokenType} -> {ok, mongoose_credentials:extend(Creds, [{username, LUser}, - {auth_module, ?MODULE}])}; + {auth_module, ?MODULE}, + {token_type_used, TokenType}, + {token_data, TokenData}])}; false -> {error, <<"not-authorized">>} end; @@ -70,14 +73,19 @@ handle_auth(#{ new_mech := NewMech }, InitiatorHashedToken, CBData, Mech) -> ToHash = <<"Initiator", CBData/binary>>, - Token1 = {NewToken, NewExpire, NewCount, NewMech}, - Token2 = {CurrentToken, CurrentExpire, CurrentCount, CurrentMech}, + TokenNew = {NewToken, NewExpire, NewCount, NewMech}, + TokenCur = {CurrentToken, CurrentExpire, CurrentCount, CurrentMech}, Shared = {NowTimestamp, ToHash, InitiatorHashedToken, Mech}, - case check_token(Token1, Shared) of + case check_token(TokenNew, Shared) of true -> - true; + {true, new}; false -> - check_token(Token2, Shared) + 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. 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 8dc44fadd42..b543be0dfdc 100644 --- a/src/mod_sasl2.erl +++ b/src/mod_sasl2.erl @@ -229,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}, @@ -237,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( From 2212854a222a0a862720af258f8296df1d4060a9 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Thu, 9 Jan 2025 21:01:14 +0100 Subject: [PATCH 21/46] Add Server initiates token rotation --- big_tests/tests/fast_auth_token_SUITE.erl | 2 + src/fast_auth_token/mod_fast_auth_token.erl | 99 +++++++++++++++++-- .../mod_fast_auth_token_backend.erl | 16 +-- .../mod_fast_auth_token_generic.erl | 11 ++- .../mod_fast_auth_token_rdbms.erl | 8 +- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 018b92c99b8..127d83d7f6b 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -140,6 +140,8 @@ token_auth_fails_when_token_is_not_found(Config) -> %% %% 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) -> Steps = [start_new_user], #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 125896d098a..8f2c97dfdc6 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -36,10 +36,11 @@ -type validity_type() :: days | hours | minutes | seconds. -type period() :: #{value := non_neg_integer(), unit := days | hours | minutes | seconds}. --type token_type() :: access. +-type token_type() :: access | rotate_before_expire. +-type token_slot() :: new | current. -export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0, - mechanism/0]). + mechanism/0, token_slot/0]). -type tokens_data() :: #{ now_timestamp := seconds(), @@ -81,8 +82,10 @@ config_spec() -> validity_periods_spec() -> #section{ - items = #{<<"access">> => validity_period_spec()}, - defaults = #{<<"access">> => #{value => 3, unit => days}}, + 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 }. @@ -166,20 +169,91 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType creds => format_term(Creds)}), #{c2s_data := C2SData} = C2SStateData, #jid{luser = LUser, lserver = LServer} = mongoose_c2s:get_jid(C2SData), - case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of - undefined -> + case check_if_should_add_token(HostType, SaslAcc, Creds) of + skip -> {ok, SaslAcc}; - #{request := Request} -> + {ok, Mech} -> 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, Request, AgentId), + Response = make_fast_token_response(HostType, LServer, LUser, Mech, AgentId), SaslAcc2 = mod_sasl2:update_inline_request(SaslAcc, ?MODULE, Response, success), {ok, SaslAcc2} end. +-spec check_if_should_add_token(HostType :: mongooseim:host_type(), + SaslAcc :: mongoose_acc:t(), + Creds :: mongoose_credentials:t()) -> + skip | {ok, mechanism()}. +check_if_should_add_token(HostType, SaslAcc, Creds) -> + case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of + undefined -> + maybe_auto_rotate(HostType, SaslAcc, Creds); + #{request := Request} -> + AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc), + Mech = exml_query:attr(Request, <<"mechanism">>), + {ok, Mech} + end. + +-spec maybe_auto_rotate(HostType :: mongooseim:host_type(), + SaslAcc :: mongoose_acc:t(), + Creds :: mongoose_credentials:t()) -> + skip | {ok, mechanism()}. +maybe_auto_rotate(HostType, SaslAcc, Creds) -> + %% Creds could contain data from mod_fast_auth_token_generic + 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)}; + 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(), + TimeBeforRotate = get_time_to_rotate_before_expire_seconds(HostType), + SecondsBeforeExpire = Timestamp - Now, + SecondsBeforeExpire =< TimeBeforRotate. + +-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. + %% Generate expirable auth token and store it in DB -make_fast_token_response(HostType, LServer, LUser, Request, AgentId) -> - Mech = exml_query:attr(Request, <<"mechanism">>), +-spec make_fast_token_response(HostType, LServer, LUser, Mech, AgentId) -> exml:element() + when HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + LUser :: jid:luser(), + AgentId :: agent_id(), + Mech :: mechanism(). +make_fast_token_response(HostType, LServer, LUser, Mech, AgentId) -> TTLSeconds = get_ttl_seconds(HostType), NowTS = ?MODULE:utc_now_as_seconds(), ExpireTS = NowTS + TTLSeconds, @@ -204,6 +278,11 @@ 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]). diff --git a/src/fast_auth_token/mod_fast_auth_token_backend.erl b/src/fast_auth_token/mod_fast_auth_token_backend.erl index b2020ba7e59..d1194708af0 100644 --- a/src/fast_auth_token/mod_fast_auth_token_backend.erl +++ b/src/fast_auth_token/mod_fast_auth_token_backend.erl @@ -14,10 +14,10 @@ when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: mod_token:agent_id(), - ExpireTS :: mod_token:seconds(), - Token :: mod_token:token(), - Mech :: mod_token:mechanism(). + 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(). -callback read_tokens(HostType, LServer, LUser, AgentId) -> {ok, mod_fast_auth_token:tokens_data()} | {error, not_found} @@ -43,10 +43,10 @@ init(HostType, Opts) -> when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: mod_token:agent_id(), - ExpireTS :: mod_token:seconds(), - Token :: mod_token:token(), - Mech :: mod_token:mechanism(). + 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(). store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech], mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). diff --git a/src/fast_auth_token/mod_fast_auth_token_generic.erl b/src/fast_auth_token/mod_fast_auth_token_generic.erl index b28dc2d7f26..944a83d5905 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic.erl @@ -37,12 +37,12 @@ mech_step(#state{creds = Creds, agent_id = AgentId, mechanism = Mech}, Serialize ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), CBData = <<>>, case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of - {true, TokenType} -> + {true, TokenSlot} -> {ok, mongoose_credentials:extend(Creds, [{username, LUser}, {auth_module, ?MODULE}, - {token_type_used, TokenType}, - {token_data, TokenData}])}; + {fast_token_slot_used, TokenSlot}, + {fast_token_data, TokenData}])}; false -> {error, <<"not-authorized">>} end; @@ -61,6 +61,11 @@ mech_step(#state{creds = Creds, agent_id = AgentId, mechanism = Mech}, Serialize %% 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, diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl index 2970681afb7..f079f8bcb28 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -37,10 +37,10 @@ init(HostType, _Opts) -> when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), LUser :: jid:luser(), - AgentId :: mod_token:agent_id(), - ExpireTS :: mod_token:seconds(), - Token :: mod_token:token(), - Mech :: mod_token:mechanism(). + 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(). store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> Key = [LServer, LUser, AgentId], Upd = [Token, ExpireTS, 0, mech_id(Mech)], From 7c5ade662045f0052b6f282818531664069fbcf4 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Thu, 9 Jan 2025 22:36:16 +0100 Subject: [PATCH 22/46] Add rotate_before_expire option Use success|failure in auth_with_token instead of bookean Add needed maybe_init_inline_request --- big_tests/tests/fast_auth_token_SUITE.erl | 33 ++++++++++++--------- src/fast_auth_token/mod_fast_auth_token.erl | 32 ++++++++++++++------ test/common/config_parser_helper.erl | 3 +- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 127d83d7f6b..8d0ad11d02d 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -111,27 +111,27 @@ request_token_with_unknown_mechanism_type(Config0) -> %% 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(true, Token, Config, Spec). + 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(true, Token, Config, Spec), - auth_with_token(true, Token, Config, Spec), - auth_with_token(true, Token, Config, Spec). + 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(false, Token, Config, Spec). + 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, receive_features], #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), Token = <<"wrongtoken">>, - auth_with_token(false, Token, Config, Spec). + 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 @@ -154,9 +154,11 @@ server_initiates_token_rotation(Config) -> %% Set almost expiring token into the new slot Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech], ok = distributed_helper:rpc(distributed_helper:mim(), mod_fast_auth_token_backend, store_new_token, Args), - Res = auth_with_token(true, Token, Config, Spec), - %% TODO finish writing this, when we have a mechanism to set token into the current slot - ok. + ConnectRes = auth_with_token(success, Token, Config, Spec), + #{token := NewToken} = parse_connect_result(ConnectRes), + ?assertNotEqual(Token, NewToken), + %% Can use new token + auth_with_token(success, NewToken, Config, Spec). %%-------------------------------------------------------------------- %% helpers @@ -165,7 +167,10 @@ server_initiates_token_rotation(Config) -> connect_and_ask_for_token(Config) -> Steps = [start_new_user, {?MODULE, auth_and_request_token}, receive_features], - #{answer := Success, spec := Spec} = sasl2_helper:apply_steps(Steps, Config), + 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}]), @@ -195,10 +200,10 @@ auth_with_token(Success, Token, Config, Spec) -> Res = sasl2_helper:apply_steps(Steps, Config, undefined, Data), #{answer := Answer} = Res, case Success of - true -> + success -> ?assertMatch(#xmlel{name = <<"success">>, attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Answer); - false -> + failure -> ?assertMatch(#xmlel{name = <<"failure">>, attrs = [{<<"xmlns">>, ?NS_SASL_2}], children = [#xmlel{name = <<"not-authorized">>}]}, @@ -206,12 +211,12 @@ auth_with_token(Success, Token, Config, Spec) -> end, Res. -steps(true) -> +steps(success) -> [connect_tls, start_stream_get_features, {?MODULE, auth_using_token}, receive_features, has_no_more_stanzas]; -steps(false) -> +steps(failure) -> [connect_tls, start_stream_get_features, {?MODULE, auth_using_token}]. diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 8f2c97dfdc6..cf36b53d3cb 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -38,6 +38,7 @@ unit := days | hours | minutes | seconds}. -type token_type() :: access | rotate_before_expire. -type token_slot() :: new | current. +-type add_reason() :: requested | auto_rotate. -export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0, mechanism/0, token_slot/0]). @@ -172,18 +173,19 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType case check_if_should_add_token(HostType, SaslAcc, Creds) of skip -> {ok, SaslAcc}; - {ok, Mech} -> + {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), - SaslAcc2 = mod_sasl2:update_inline_request(SaslAcc, ?MODULE, Response, success), - {ok, SaslAcc2} + SaslAcc2 = maybe_init_inline_request(AddReason, SaslAcc), + SaslAcc3 = mod_sasl2:update_inline_request(SaslAcc2, ?MODULE, Response, success), + {ok, SaslAcc3} end. -spec check_if_should_add_token(HostType :: mongooseim:host_type(), SaslAcc :: mongoose_acc:t(), Creds :: mongoose_credentials:t()) -> - skip | {ok, mechanism()}. + skip | {ok, mechanism(), Reason :: add_reason()}. check_if_should_add_token(HostType, SaslAcc, Creds) -> case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of undefined -> @@ -191,22 +193,24 @@ check_if_should_add_token(HostType, SaslAcc, Creds) -> #{request := Request} -> AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc), Mech = exml_query:attr(Request, <<"mechanism">>), - {ok, Mech} + {ok, Mech, requested} end. -spec maybe_auto_rotate(HostType :: mongooseim:host_type(), SaslAcc :: mongoose_acc:t(), Creds :: mongoose_credentials:t()) -> - skip | {ok, mechanism()}. + skip | {ok, mechanism(), Reason :: add_reason()}. maybe_auto_rotate(HostType, SaslAcc, Creds) -> %% Creds could contain data from mod_fast_auth_token_generic SlotUsed = mongoose_credentials:get(Creds, fast_token_slot_used, undefined), DataUsed = mongoose_credentials:get(Creds, fast_token_data, undefined), + ?LOG_ERROR(#{what => maybe_auto_rotate, slot => SlotUsed, data_used => format_term(DataUsed)}), 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)}; +?LOG_ERROR(#{what => rotate_rotate}), + {ok, data_used_to_mech_type(SlotUsed, DataUsed), auto_rotate}; false -> skip end; @@ -225,9 +229,10 @@ is_used_token_about_to_expire(HostType, SlotUsed, DataUsed) -> Timestamp :: seconds()) -> boolean(). is_timestamp_about_to_expire(HostType, Timestamp) -> Now = utc_now_as_seconds(), - TimeBeforRotate = get_time_to_rotate_before_expire_seconds(HostType), + TimeBeforeRotate = get_time_to_rotate_before_expire_seconds(HostType), SecondsBeforeExpire = Timestamp - Now, - SecondsBeforeExpire =< TimeBeforRotate. +?LOG_ERROR(#{what => is_timestamp_about_to_expire, seconds_before => SecondsBeforeExpire, befor_rot => TimeBeforeRotate}), + SecondsBeforeExpire =< TimeBeforeRotate. -spec user_used_token_to_login(token_slot() | undefined) -> boolean(). user_used_token_to_login(SlotUsed) -> @@ -246,6 +251,15 @@ data_used_to_mech_type(new, #{new_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, ?MODULE, #xmlel{name = <<"auto">>}). + %% Generate expirable auth token and store it in DB -spec make_fast_token_response(HostType, LServer, LUser, Mech, AgentId) -> exml:element() when HostType :: mongooseim:host_type(), diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 990b36a027f..4a37fda9d6e 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -869,7 +869,8 @@ default_mod_config(mod_auth_token) -> refresh => #{unit => days, value => 25}}}; default_mod_config(mod_fast_auth_token) -> #{backend => rdbms, - validity_period => #{access => #{unit => days, value => 3}}}; + validity_period => #{access => #{unit => days, value => 3}, + rotate_before_expire => #{unit => hours, value => 6}}}; default_mod_config(mod_bind2) -> #{}; default_mod_config(mod_blocking) -> From 43f2f94b32e92fa2dc2b2ae34d09241a576d6064 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Thu, 9 Jan 2025 23:14:30 +0100 Subject: [PATCH 23/46] Move token to the current slot --- big_tests/tests/fast_auth_token_SUITE.erl | 26 ++++++--- src/fast_auth_token/mod_fast_auth_token.erl | 55 +++++++++++++++---- .../mod_fast_auth_token_backend.erl | 18 +++--- .../mod_fast_auth_token_rdbms.erl | 43 ++++++++++++--- 4 files changed, 109 insertions(+), 33 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 8d0ad11d02d..0166deb066f 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -33,7 +33,8 @@ groups() -> 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 + server_initiates_token_rotation, + could_still_use_old_token_when_server_initiates_token_rotation ]} ]. @@ -143,6 +144,20 @@ token_auth_fails_when_token_is_not_found(Config) -> %% %% 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). + +%%-------------------------------------------------------------------- +%% helpers +%%-------------------------------------------------------------------- + +connect_with_almost_expired_token(Config) -> Steps = [start_new_user], #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), HostType = domain_helper:host_type(), @@ -152,17 +167,12 @@ server_initiates_token_rotation(Config) -> Mech = <<"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], + 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), - %% Can use new token - auth_with_token(success, NewToken, Config, Spec). - -%%-------------------------------------------------------------------- -%% helpers -%%-------------------------------------------------------------------- + #{new_token => NewToken, spec => Spec, old_token => Token}. connect_and_ask_for_token(Config) -> Steps = [start_new_user, {?MODULE, auth_and_request_token}, diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index cf36b53d3cb..20cadd6071a 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -41,7 +41,7 @@ -type add_reason() :: requested | auto_rotate. -export_type([tokens_data/0, seconds/0, counter/0, token/0, agent_id/0, - mechanism/0, token_slot/0]). + mechanism/0, token_slot/0, set_current/0]). -type tokens_data() :: #{ now_timestamp := seconds(), @@ -55,6 +55,13 @@ 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), @@ -176,7 +183,7 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType {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), + 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, ?MODULE, Response, success), {ok, SaslAcc3} @@ -261,23 +268,49 @@ maybe_init_inline_request(auto_rotate, SaslAcc) -> mod_sasl2:put_inline_request(SaslAcc, ?MODULE, #xmlel{name = <<"auto">>}). %% Generate expirable auth token and store it in DB --spec make_fast_token_response(HostType, LServer, LUser, Mech, AgentId) -> exml:element() +-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(). -make_fast_token_response(HostType, LServer, LUser, Mech, AgentId) -> + 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(), - store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech), + 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 + 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}], @@ -311,17 +344,19 @@ period_to_seconds(Seconds, seconds) -> Seconds. generate_unique_token() -> base64:encode(crypto:strong_rand_bytes(25)). --spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok +-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(). -store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> + 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). + HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCurrent). -spec read_tokens(HostType, LServer, LUser, AgentId) -> {ok, tokens_data()} | {error, not_found} diff --git a/src/fast_auth_token/mod_fast_auth_token_backend.erl b/src/fast_auth_token/mod_fast_auth_token_backend.erl index d1194708af0..280e3f847c3 100644 --- a/src/fast_auth_token/mod_fast_auth_token_backend.erl +++ b/src/fast_auth_token/mod_fast_auth_token_backend.erl @@ -1,7 +1,7 @@ -module(mod_fast_auth_token_backend). -export([init/2, - store_new_token/7, + store_new_token/8, read_tokens/4, remove_user/3, remove_domain/2]). @@ -10,14 +10,16 @@ -callback init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. --callback store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> 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(). + 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} @@ -39,16 +41,18 @@ init(HostType, Opts) -> Args = [HostType, Opts], mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). --spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> ok +-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(). -store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> - Args = [HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech], + 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) -> diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl index f079f8bcb28..44e93d5d6ed 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -3,7 +3,7 @@ -include("mongoose_logger.hrl"). -export([init/2, - store_new_token/7, + store_new_token/8, read_tokens/4, remove_user/3, remove_domain/2]). @@ -12,10 +12,8 @@ -spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. init(HostType, _Opts) -> - 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(HostType), + prepare_upsert_and_set_current(HostType), prepare(fast_select, fast_auth_token, [current_token, current_expire, current_count, current_mech_id, new_token, new_expire, new_count, new_mech_id], @@ -33,19 +31,48 @@ init(HostType, _Opts) -> <<"DELETE FROM fast_auth_token WHERE server = ?">>), ok. --spec store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> 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(). -store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech) -> + 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) -> From 1890f6ea713a954b4dcef0eace48b07b7982c85d Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Thu, 9 Jan 2025 23:32:19 +0100 Subject: [PATCH 24/46] Add tests for server-initiated token rotation in the current slot --- big_tests/tests/fast_auth_token_SUITE.erl | 41 ++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 0166deb066f..058ba608b2e 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -34,7 +34,9 @@ groups() -> 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 + 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 ]} ]. @@ -153,6 +155,17 @@ could_still_use_old_token_when_server_initiates_token_rotation(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). + %%-------------------------------------------------------------------- %% helpers %%-------------------------------------------------------------------- @@ -174,6 +187,32 @@ connect_with_almost_expired_token(Config) -> ?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 = <<"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], From dd66cadde247d2f3ab81160b8aad439b7e26a170 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Thu, 9 Jan 2025 23:55:36 +0100 Subject: [PATCH 25/46] Add rerequest_token_with_initial_authentication Add can_use_new_token_after_rerequest_token_with_initial_authentication Add can_use_current_token_after_rerequest_token_with_initial_authentication --- big_tests/tests/fast_auth_token_SUITE.erl | 46 +++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 058ba608b2e..e80790ac032 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -36,7 +36,10 @@ groups() -> 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 + could_still_use_old_token_when_server_initiates_token_rotation_for_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 ]} ]. @@ -166,6 +169,21 @@ could_still_use_old_token_when_server_initiates_token_rotation_for_the_current_s %% Can still use old token auth_with_token(success, OldToken, 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). + %%-------------------------------------------------------------------- %% helpers %%-------------------------------------------------------------------- @@ -233,8 +251,14 @@ auth_and_request_token(Config, Client, Data) -> 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, <<"HT-SHA-256-NONE">>). + 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). %% request_token(Mech) -> @@ -243,8 +267,11 @@ request_token(Mech) -> {<<"mechanism">>, Mech}]}. 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), + Steps = steps(Success, auth_function(RequestToken)), Data = #{spec => Spec2}, Res = sasl2_helper:apply_steps(Steps, Config, undefined, Data), #{answer := Answer} = Res, @@ -260,14 +287,19 @@ auth_with_token(Success, Token, Config, Spec) -> end, Res. -steps(success) -> +auth_function(dont_request_token) -> + auth_using_token; +auth_function(request_token) -> + auth_using_token_and_request_token. + +steps(success, AuthFun) -> [connect_tls, start_stream_get_features, - {?MODULE, auth_using_token}, + {?MODULE, AuthFun}, receive_features, has_no_more_stanzas]; -steps(failure) -> +steps(failure, AuthFun) -> [connect_tls, start_stream_get_features, - {?MODULE, auth_using_token}]. + {?MODULE, AuthFun}]. user_agent_id() -> <<"d4565fa7-4d72-4749-b3d3-740edbf87770">>. From b56752f9b9a66b5cfadc8bca4dbb6d41559309d7 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 13 Jan 2025 23:54:25 +0100 Subject: [PATCH 26/46] Add token invalidation --- big_tests/tests/fast_auth_token_SUITE.erl | 52 ++++++++++++++- src/fast_auth_token/mod_fast_auth_token.erl | 65 +++++++++++++++---- .../mod_fast_auth_token_backend.erl | 18 ++++- .../mod_fast_auth_token_rdbms.erl | 15 +++++ 4 files changed, 136 insertions(+), 14 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index e80790ac032..2bb586868b9 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -39,7 +39,10 @@ groups() -> could_still_use_old_token_when_server_initiates_token_rotation_for_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 + 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 ]} ]. @@ -184,6 +187,23 @@ 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), + ConnectRes = 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), + ConnectRes = 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). + %%-------------------------------------------------------------------- %% helpers %%-------------------------------------------------------------------- @@ -260,12 +280,36 @@ auth_using_token_and_request_token(Config, Client, Data) -> 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). @@ -290,7 +334,11 @@ auth_with_token(Success, Token, Config, Spec, RequestToken) -> auth_function(dont_request_token) -> auth_using_token; auth_function(request_token) -> - auth_using_token_and_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, diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 20cadd6071a..5558c04a8ce 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -154,19 +154,46 @@ sasl2_start(SaslAcc, #{stanza := El}, _) -> ?LOG_ERROR(#{what => sasl2_startttt, elleee => El, sasla_acc => SaslAcc}), AgentId = exml_query:path(El, [{element, <<"user-agent">>}, {attr, <<"id">>}]), SaslAcc2 = mongoose_acc:set(?MODULE, agent_id, AgentId, SaslAcc), - case exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}]) of - undefined -> + case {should_invalidate(El), + exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}])} of + {#xmlel{} = Fast, _} -> + {ok, mod_sasl2:put_inline_request(SaslAcc2, ?MODULE, Fast)}; + {undefined, undefined} -> {ok, SaslAcc2}; - Request -> + {undefined, Request} -> Mech = exml_query:attr(Request, <<"mechanism">>), case Mech of <<"HT-SHA-256-NONE">> -> {ok, mod_sasl2:put_inline_request(SaslAcc2, ?MODULE, Request)}; _ -> - {ok, SaslAcc2} + {ok, SaslAcc2} end end. +-spec should_invalidate(exml:element()) -> exml:element() | undefined. +should_invalidate(El) -> + ElemPath = {element_with_ns, <<"fast">>, ?NS_FAST}, + Val = exml_query:path(El, [ElemPath, {attr, <<"invalidate">>}]), + case is_true(Val) of + true -> + exml_query:path(El, [ElemPath]); + false -> + undefined + end. + +is_true(<<"true">>) -> true; +is_true(<<"1">>) -> true; +is_true(_) -> false. + +-spec request_type(exml:element()) -> invalidate | request_token. +request_type(Request) -> + case Request of + #xmlel{name = <<"fast">>} -> + invalidate; + #xmlel{name = <<"request-token">>} -> + request_token + end. + format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). -spec sasl2_success(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) -> @@ -180,6 +207,10 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType 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 @@ -192,22 +223,25 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType -spec check_if_should_add_token(HostType :: mongooseim:host_type(), SaslAcc :: mongoose_acc:t(), Creds :: mongoose_credentials:t()) -> - skip | {ok, mechanism(), Reason :: add_reason()}. + skip | invalidate | {ok, mechanism(), Reason :: add_reason()}. check_if_should_add_token(HostType, SaslAcc, Creds) -> case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of undefined -> - maybe_auto_rotate(HostType, SaslAcc, Creds); + maybe_auto_rotate(HostType, Creds); #{request := Request} -> - AgentId = mongoose_acc:get(?MODULE, agent_id, undefined, SaslAcc), - Mech = exml_query:attr(Request, <<"mechanism">>), - {ok, Mech, requested} + case request_type(Request) of + invalidate -> + invalidate; + request_token -> + Mech = exml_query:attr(Request, <<"mechanism">>), + {ok, Mech, requested} + end end. -spec maybe_auto_rotate(HostType :: mongooseim:host_type(), - SaslAcc :: mongoose_acc:t(), Creds :: mongoose_credentials:t()) -> skip | {ok, mechanism(), Reason :: add_reason()}. -maybe_auto_rotate(HostType, SaslAcc, Creds) -> +maybe_auto_rotate(HostType, Creds) -> %% Creds could contain data from mod_fast_auth_token_generic SlotUsed = mongoose_credentials:get(Creds, fast_token_slot_used, undefined), DataUsed = mongoose_credentials:get(Creds, fast_token_data, undefined), @@ -358,6 +392,15 @@ store_new_token(HostType, LServer, LUser, AgentId, ExpireTS, Token, Mech, SetCur 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(), diff --git a/src/fast_auth_token/mod_fast_auth_token_backend.erl b/src/fast_auth_token/mod_fast_auth_token_backend.erl index 280e3f847c3..111c9c9e880 100644 --- a/src/fast_auth_token/mod_fast_auth_token_backend.erl +++ b/src/fast_auth_token/mod_fast_auth_token_backend.erl @@ -3,6 +3,7 @@ -export([init/2, store_new_token/8, read_tokens/4, + invalidate_token/4, remove_user/3, remove_domain/2]). @@ -28,6 +29,12 @@ 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. @@ -36,7 +43,7 @@ -spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. init(HostType, Opts) -> - Tracked = [store_new_token, read_tokens], + 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). @@ -65,6 +72,15 @@ 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], diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl index 44e93d5d6ed..c2bb436533f 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -5,6 +5,7 @@ -export([init/2, store_new_token/8, read_tokens/4, + invalidate_token/4, remove_user/3, remove_domain/2]). @@ -22,6 +23,10 @@ init(HostType, _Opts) -> "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 " @@ -126,6 +131,16 @@ 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]), From 6d9a49789510692296a5e48e0fe4ef8d3e903e32 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 00:45:09 +0100 Subject: [PATCH 27/46] Use map to parse request --- src/fast_auth_token/mod_fast_auth_token.erl | 109 +++++++++++--------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 5558c04a8ce..4ba75bd4737 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -40,6 +40,9 @@ -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]). @@ -152,47 +155,17 @@ mechanisms() -> sasl2_start(SaslAcc, #{stanza := El}, _) -> %% TODO remove this log ?LOG_ERROR(#{what => sasl2_startttt, elleee => El, sasla_acc => SaslAcc}), + 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), - case {should_invalidate(El), - exml_query:path(El, [{element_with_ns, <<"request-token">>, ?NS_FAST}])} of - {#xmlel{} = Fast, _} -> - {ok, mod_sasl2:put_inline_request(SaslAcc2, ?MODULE, Fast)}; - {undefined, undefined} -> - {ok, SaslAcc2}; - {undefined, Request} -> - Mech = exml_query:attr(Request, <<"mechanism">>), - case Mech of - <<"HT-SHA-256-NONE">> -> - {ok, mod_sasl2:put_inline_request(SaslAcc2, ?MODULE, Request)}; - _ -> - {ok, SaslAcc2} - end - end. - --spec should_invalidate(exml:element()) -> exml:element() | undefined. -should_invalidate(El) -> - ElemPath = {element_with_ns, <<"fast">>, ?NS_FAST}, - Val = exml_query:path(El, [ElemPath, {attr, <<"invalidate">>}]), - case is_true(Val) of - true -> - exml_query:path(El, [ElemPath]); - false -> - undefined - end. + SaslAcc3 = maybe_put_inline_request(SaslAcc2, ?REQ, Req), + {ok, maybe_put_inline_request(SaslAcc3, ?FAST, Fast)}. -is_true(<<"true">>) -> true; -is_true(<<"1">>) -> true; -is_true(_) -> false. - --spec request_type(exml:element()) -> invalidate | request_token. -request_type(Request) -> - case Request of - #xmlel{name = <<"fast">>} -> - invalidate; - #xmlel{name = <<"request-token">>} -> - request_token - end. +maybe_put_inline_request(SaslAcc, _Module, undefined) -> + SaslAcc; +maybe_put_inline_request(SaslAcc, Module, Request) -> + mod_sasl2:put_inline_request(SaslAcc, Module, Request). format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). @@ -216,7 +189,7 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType %% 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, ?MODULE, Response, success), + SaslAcc3 = mod_sasl2:update_inline_request(SaslAcc2, ?REQ, Response, success), {ok, SaslAcc3} end. @@ -225,19 +198,53 @@ sasl2_success(SaslAcc, C2SStateData = #{creds := Creds}, #{host_type := HostType Creds :: mongoose_credentials:t()) -> skip | invalidate | {ok, mechanism(), Reason :: add_reason()}. check_if_should_add_token(HostType, SaslAcc, Creds) -> - case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of - undefined -> - maybe_auto_rotate(HostType, Creds); - #{request := Request} -> - case request_type(Request) of - invalidate -> - invalidate; - request_token -> - Mech = exml_query:attr(Request, <<"mechanism">>), - {ok, Mech, requested} - end + 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()}. @@ -299,7 +306,7 @@ 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, ?MODULE, #xmlel{name = <<"auto">>}). + 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() From 87862a34a6b99a7745c717638faee0721b6c9a30 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 01:19:12 +0100 Subject: [PATCH 28/46] Add HT-SHA-3-512-NONE method --- big_tests/tests/fast_auth_token_SUITE.erl | 63 +++++++++++-------- src/c2s/mongoose_c2s.erl | 3 + src/fast_auth_token/mod_fast_auth_token.erl | 3 +- .../mod_fast_auth_token_generic.erl | 6 +- .../mod_fast_auth_token_rdbms.erl | 4 +- src/sasl/cyrsasl.erl | 5 +- src/sasl/cyrsasl_ht_sha3_512_none.erl | 23 +++++++ 7 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 src/sasl/cyrsasl_ht_sha3_512_none.erl diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 2bb586868b9..1ed196e2736 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -19,33 +19,36 @@ all() -> [ - {group, basic} + {group, ht_sha_256_none}, + {group, ht_sha_3_512_none} ]. groups() -> [ - {basic, [parallel], - [ - 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, - 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 - ]} + {ht_sha_256_none, [parallel], tests()}, + {ht_sha_3_512_none, [parallel], tests()} ]. +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, + 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 + ]. + %%-------------------------------------------------------------------- %% Init & teardown %%-------------------------------------------------------------------- @@ -64,6 +67,8 @@ end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). +init_per_group(ht_sha_3_512_none, Config) -> + [{ht_mech, <<"HT-SHA-3-512-NONE">>} | Config]; init_per_group(_GroupName, Config) -> Config. @@ -215,7 +220,7 @@ connect_with_almost_expired_token(Config) -> {LUser, LServer} = spec_to_lus(Spec), AgentId = user_agent_id(), Token = <<"verysecret">>, - Mech = <<"HT-SHA-256-NONE">>, + 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], @@ -234,7 +239,7 @@ connect_with_almost_expired_token_in_the_current_slot(Config) -> AgentId = user_agent_id(), Token = <<"verysecret">>, CurrentToken = <<"currentsecret">>, - Mech = <<"HT-SHA-256-NONE">>, + Mech = proplists:get_value(ht_mech, Config, <<"HT-SHA-256-NONE">>), ExpireTS = Now + 86400, %% 24 hours into the future SetCurrent = #{ current_token => CurrentToken, @@ -368,8 +373,8 @@ auth_with_method(_Config, Client, Data, BindElems, Extra, Method) -> InitEl = case Method of <<"PLAIN">> -> sasl2_helper:plain_auth_initial_response(Client); - <<"HT-SHA-256-NONE">> -> - ht_auth_initial_response(Client) + <<"HT-", _/binary>> -> + ht_auth_initial_response(Client, Method) end, BindEl = #xmlel{name = <<"bind">>, attrs = [{<<"xmlns">>, ?NS_BIND_2}], @@ -404,16 +409,20 @@ auth_elem(Mech, Children) -> %% %% NUL = %0x00 ; The null octet %% SAFE = UTF8-encoded string -ht_auth_initial_response(#client{props = Props}) -> +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>>, - InitiatorHashedToken = crypto:mac(hmac, sha256, Token, ToHash), + Algo = mech_to_algo(Method), + InitiatorHashedToken = crypto:mac(hmac, Algo, Token, ToHash), Payload = <>, initial_response_elem(Payload). +mech_to_algo(<<"HT-SHA-256-NONE">>) -> sha256; +mech_to_algo(<<"HT-SHA-3-512-NONE">>) -> sha3_512. + initial_response_elem(Payload) -> Encoded = base64:encode(Payload), #xmlel{name = <<"initial-response">>, diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl index 3437c87f233..4fbf3136bb7 100644 --- a/src/c2s/mongoose_c2s.erl +++ b/src/c2s/mongoose_c2s.erl @@ -1169,6 +1169,9 @@ get_auth_mechs_to_announce(StateData) -> skip_announce_mechanism(<<"HT-SHA-256-ENDP">>) -> true; skip_announce_mechanism(<<"HT-SHA-256-EXPR">>) -> true; skip_announce_mechanism(<<"HT-SHA-256-NONE">>) -> true; +skip_announce_mechanism(<<"HT-SHA-3-512-ENDP">>) -> true; +skip_announce_mechanism(<<"HT-SHA-3-512-EXPR">>) -> true; +skip_announce_mechanism(<<"HT-SHA-3-512-NONE">>) -> true; skip_announce_mechanism(_) -> false. -spec filter_mechanism(data(), binary()) -> boolean(). diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 4ba75bd4737..a7408f15dae 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -148,7 +148,8 @@ mechanisms() -> [% <<"HT-SHA-256-ENDP">>, % <<"HT-SHA-256-EXPR">>, %% Channel binding: none - <<"HT-SHA-256-NONE">>]. + <<"HT-SHA-256-NONE">>, + <<"HT-SHA-3-512-NONE">>]. -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). diff --git a/src/fast_auth_token/mod_fast_auth_token_generic.erl b/src/fast_auth_token/mod_fast_auth_token_generic.erl index 944a83d5905..cdd05146979 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic.erl @@ -97,6 +97,10 @@ handle_auth(#{ check_token({Token, Expire, Count, Mech}, {NowTimestamp, ToHash, InitiatorHashedToken, Mech}) when is_binary(Token) -> - crypto:mac(hmac, sha256, Token, ToHash) =:= InitiatorHashedToken; + Algo = mech_to_algo(Mech), + crypto:mac(hmac, Algo, Token, ToHash) =:= InitiatorHashedToken; check_token(_, _) -> false. + +mech_to_algo(<<"HT-SHA-256-NONE">>) -> sha256; +mech_to_algo(<<"HT-SHA-3-512-NONE">>) -> sha3_512. diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl index c2bb436533f..a9754a6cf8f 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -146,7 +146,9 @@ remove_domain(HostType, LServer) -> execute_successfully(HostType, fast_remove_domain, [LServer]), ok. -mech_id(<<"HT-SHA-256-NONE">>) -> 1. +mech_id(<<"HT-SHA-256-NONE">>) -> 1; +mech_id(<<"HT-SHA-3-512-NONE">>) -> 2. mech_name(1) -> <<"HT-SHA-256-NONE">>; +mech_name(2) -> <<"HT-SHA-3-512-NONE">>; mech_name(_) -> <<"UNKNOWN-MECH">>. %% Just in case DB has an unknown mech_id diff --git a/src/sasl/cyrsasl.erl b/src/sasl/cyrsasl.erl index 80a40651404..222218385c0 100644 --- a/src/sasl/cyrsasl.erl +++ b/src/sasl/cyrsasl.erl @@ -121,6 +121,8 @@ is_module_supported(HostType, cyrsasl_oauth) -> gen_mod:is_loaded(HostType, mod_auth_token); is_module_supported(HostType, cyrsasl_ht_sha256_none) -> true; +is_module_supported(HostType, cyrsasl_ht_sha3_512_none) -> + true; is_module_supported(HostType, Module) -> mongoose_fips:supports_sasl_module(Module) andalso ejabberd_auth:supports_sasl_module(HostType, Module). @@ -159,4 +161,5 @@ default_modules() -> cyrsasl_plain, cyrsasl_anonymous, cyrsasl_oauth, - cyrsasl_ht_sha256_none]. + cyrsasl_ht_sha256_none, + cyrsasl_ht_sha3_512_none]. 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..a5df14bd6bb --- /dev/null +++ b/src/sasl/cyrsasl_ht_sha3_512_none.erl @@ -0,0 +1,23 @@ +-module(cyrsasl_ht_sha3_512_none). +-behaviour(cyrsasl). + +-export([mechanism/0, mech_new/3, mech_step/2]). +-ignore_xref([mech_new/3]). + +-include("mongoose.hrl"). + +-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_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_step(State, SerializedToken). From 203ddf9ce6ca1b0005dee40fb7ee9f2e9150f536 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 10:03:17 +0100 Subject: [PATCH 29/46] Add token_auth_fails_when_mechanism_does_not_match testcase --- big_tests/tests/fast_auth_token_SUITE.erl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 1ed196e2736..c614796b52f 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -46,7 +46,8 @@ tests() -> 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 + both_tokens_do_not_work_after_invalidation, + token_auth_fails_when_mechanism_does_not_match ]. %%-------------------------------------------------------------------- @@ -209,6 +210,17 @@ both_tokens_do_not_work_after_invalidation(Config) -> 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 %%-------------------------------------------------------------------- @@ -423,6 +435,9 @@ ht_auth_initial_response(#client{props = Props}, Method) -> mech_to_algo(<<"HT-SHA-256-NONE">>) -> sha256; 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-3-512-NONE">>) -> <<"HT-SHA-256-NONE">>. + initial_response_elem(Payload) -> Encoded = base64:encode(Payload), #xmlel{name = <<"initial-response">>, From 6d0e659f5a241a083e94aec379df4b9ba60d6c61 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 10:12:59 +0100 Subject: [PATCH 30/46] Rename mod_fast_auth_token_generic to mod_fast_auth_token_generic_mech.erl --- src/fast_auth_token/mod_fast_auth_token.erl | 4 ++-- ...token_generic.erl => mod_fast_auth_token_generic_mech.erl} | 2 +- src/sasl/cyrsasl_ht_sha256_none.erl | 4 ++-- src/sasl/cyrsasl_ht_sha3_512_none.erl | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/fast_auth_token/{mod_fast_auth_token_generic.erl => mod_fast_auth_token_generic_mech.erl} (99%) diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index a7408f15dae..3a1df8d74f9 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -250,7 +250,7 @@ maybe_parse_integer(undefined) -> 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 + %% 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), ?LOG_ERROR(#{what => maybe_auto_rotate, slot => SlotUsed, data_used => format_term(DataUsed)}), @@ -332,7 +332,7 @@ make_fast_token_response(HostType, LServer, LUser, Mech, AgentId, Creds) -> -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 + %% 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 diff --git a/src/fast_auth_token/mod_fast_auth_token_generic.erl b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl similarity index 99% rename from src/fast_auth_token/mod_fast_auth_token_generic.erl rename to src/fast_auth_token/mod_fast_auth_token_generic_mech.erl index cdd05146979..067d308d767 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -1,4 +1,4 @@ --module(mod_fast_auth_token_generic). +-module(mod_fast_auth_token_generic_mech). -export([mech_new/4, mech_step/2]). diff --git a/src/sasl/cyrsasl_ht_sha256_none.erl b/src/sasl/cyrsasl_ht_sha256_none.erl index 37f6564a43b..f5aaa4c1125 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -14,10 +14,10 @@ mechanism() -> Creds :: mongoose_credentials:t(), SocketData :: term()) -> {ok, tuple()} | {error, binary()}. mech_new(Host, Creds, SocketData) -> - mod_fast_auth_token_generic:mech_new(Host, Creds, SocketData, mechanism()). + 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_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 index a5df14bd6bb..1b2c6d6c76e 100644 --- a/src/sasl/cyrsasl_ht_sha3_512_none.erl +++ b/src/sasl/cyrsasl_ht_sha3_512_none.erl @@ -14,10 +14,10 @@ mechanism() -> Creds :: mongoose_credentials:t(), SocketData :: term()) -> {ok, tuple()} | {error, binary()}. mech_new(Host, Creds, SocketData) -> - mod_fast_auth_token_generic:mech_new(Host, Creds, SocketData, mechanism()). + 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_step(State, SerializedToken). + mod_fast_auth_token_generic_mech:mech_step(State, SerializedToken). From 58005dc056af49fa712520f63000936250e80a7e Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 22:32:55 +0100 Subject: [PATCH 31/46] Move list of mechanisms into mod_fast_auth_token_generic_mech --- src/c2s/mongoose_c2s.erl | 9 +- src/fast_auth_token/mod_fast_auth_token.erl | 9 +- .../mod_fast_auth_token_generic_mech.erl | 89 ++++++++++++++++++- .../mod_fast_auth_token_rdbms.erl | 11 +-- src/sasl/cyrsasl.erl | 13 ++- src/sasl/cyrsasl_ht_sha256_none.erl | 2 - src/sasl/cyrsasl_ht_sha384_none.erl | 21 +++++ src/sasl/cyrsasl_ht_sha3_256_none.erl | 21 +++++ src/sasl/cyrsasl_ht_sha3_384_none.erl | 21 +++++ src/sasl/cyrsasl_ht_sha3_512_none.erl | 2 - src/sasl/cyrsasl_ht_sha512_none.erl | 21 +++++ 11 files changed, 187 insertions(+), 32 deletions(-) create mode 100644 src/sasl/cyrsasl_ht_sha384_none.erl create mode 100644 src/sasl/cyrsasl_ht_sha3_256_none.erl create mode 100644 src/sasl/cyrsasl_ht_sha3_384_none.erl create mode 100644 src/sasl/cyrsasl_ht_sha512_none.erl diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl index 4fbf3136bb7..c9c4671336e 100644 --- a/src/c2s/mongoose_c2s.erl +++ b/src/c2s/mongoose_c2s.erl @@ -1166,13 +1166,8 @@ 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(<<"HT-SHA-256-ENDP">>) -> true; -skip_announce_mechanism(<<"HT-SHA-256-EXPR">>) -> true; -skip_announce_mechanism(<<"HT-SHA-256-NONE">>) -> true; -skip_announce_mechanism(<<"HT-SHA-3-512-ENDP">>) -> true; -skip_announce_mechanism(<<"HT-SHA-3-512-EXPR">>) -> true; -skip_announce_mechanism(<<"HT-SHA-3-512-NONE">>) -> true; -skip_announce_mechanism(_) -> false. +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">>) -> diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 3a1df8d74f9..9bcaaf40473 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -142,14 +142,9 @@ mechanisms_elems(Mechs) -> [#xmlel{name = <<"mechanism">>, children = [#xmlcdata{content = Mech}]} || Mech <- Mechs]. +-spec mechanisms() -> [mechanism()]. mechanisms() -> - %% Mechanisms described in - %% https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html - [% <<"HT-SHA-256-ENDP">>, - % <<"HT-SHA-256-EXPR">>, - %% Channel binding: none - <<"HT-SHA-256-NONE">>, - <<"HT-SHA-3-512-NONE">>]. + mod_fast_auth_token_generic_mech:mechanisms(). -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). 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 index 067d308d767..f20eb1ec2f3 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -1,6 +1,14 @@ -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(state, {creds, agent_id, mechanism}). -include("mongoose.hrl"). @@ -102,5 +110,84 @@ check_token({Token, Expire, Count, Mech}, 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-3-512-NONE">>) -> sha3_512. +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 index a9754a6cf8f..89afcbf410e 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -146,9 +146,10 @@ remove_domain(HostType, LServer) -> execute_successfully(HostType, fast_remove_domain, [LServer]), ok. -mech_id(<<"HT-SHA-256-NONE">>) -> 1; -mech_id(<<"HT-SHA-3-512-NONE">>) -> 2. +-spec mech_id(mod_fast_auth_token:mechanism()) -> non_neg_integer(). +mech_id(Mech) -> + mod_fast_auth_token_generic_mech:mech_id(Mech). -mech_name(1) -> <<"HT-SHA-256-NONE">>; -mech_name(2) -> <<"HT-SHA-3-512-NONE">>; -mech_name(_) -> <<"UNKNOWN-MECH">>. %% Just in case DB has an unknown mech_id +-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/sasl/cyrsasl.erl b/src/sasl/cyrsasl.erl index 222218385c0..a944b2efdc3 100644 --- a/src/sasl/cyrsasl.erl +++ b/src/sasl/cyrsasl.erl @@ -119,12 +119,10 @@ 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, cyrsasl_ht_sha256_none) -> - true; -is_module_supported(HostType, cyrsasl_ht_sha3_512_none) -> - true; 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(). @@ -160,6 +158,5 @@ default_modules() -> cyrsasl_scram_sha1, cyrsasl_plain, cyrsasl_anonymous, - cyrsasl_oauth, - cyrsasl_ht_sha256_none, - cyrsasl_ht_sha3_512_none]. + 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 index f5aaa4c1125..afbc8e9c852 100644 --- a/src/sasl/cyrsasl_ht_sha256_none.erl +++ b/src/sasl/cyrsasl_ht_sha256_none.erl @@ -4,8 +4,6 @@ -export([mechanism/0, mech_new/3, mech_step/2]). -ignore_xref([mech_new/3]). --include("mongoose.hrl"). - -spec mechanism() -> cyrsasl:mechanism(). mechanism() -> <<"HT-SHA-256-NONE">>. 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 index 1b2c6d6c76e..40cd28f9ddd 100644 --- a/src/sasl/cyrsasl_ht_sha3_512_none.erl +++ b/src/sasl/cyrsasl_ht_sha3_512_none.erl @@ -4,8 +4,6 @@ -export([mechanism/0, mech_new/3, mech_step/2]). -ignore_xref([mech_new/3]). --include("mongoose.hrl"). - -spec mechanism() -> cyrsasl:mechanism(). mechanism() -> <<"HT-SHA-3-512-NONE">>. 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). From ec527b14088ac36e369a45e7832cee8df50b1c4d Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 22:47:25 +0100 Subject: [PATCH 32/46] Add more mechanisms for fast_auth_token_SUITE --- big_tests/tests/fast_auth_token_SUITE.erl | 49 ++++++++++++++--------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index c614796b52f..d5f10f60e2c 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -18,16 +18,10 @@ %%-------------------------------------------------------------------- all() -> - [ - {group, ht_sha_256_none}, - {group, ht_sha_3_512_none} - ]. + [{group, Group} || {Group, _, _} <- groups()]. groups() -> - [ - {ht_sha_256_none, [parallel], tests()}, - {ht_sha_3_512_none, [parallel], tests()} - ]. + [{ht_sha_256_none, [parallel], tests()} || {Group, _Mech} <- mechanisms()]. tests() -> [server_advertises_support_for_fast, @@ -50,6 +44,28 @@ tests() -> 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 %%-------------------------------------------------------------------- @@ -68,10 +84,13 @@ end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). -init_per_group(ht_sha_3_512_none, Config) -> - [{ht_mech, <<"HT-SHA-3-512-NONE">>} | Config]; -init_per_group(_GroupName, Config) -> - Config. +init_per_group(Group, Config) -> + case lists:keyfind(Group, 1, mechanisms()) of + Mech when is_binary(Mech) -> + [{ht_mech, Mech} | Config]; + _ -> + Config + end. end_per_group(_GroupName, Config) -> Config. @@ -432,12 +451,6 @@ ht_auth_initial_response(#client{props = Props}, Method) -> Payload = <>, initial_response_elem(Payload). -mech_to_algo(<<"HT-SHA-256-NONE">>) -> sha256; -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-3-512-NONE">>) -> <<"HT-SHA-256-NONE">>. - initial_response_elem(Payload) -> Encoded = base64:encode(Payload), #xmlel{name = <<"initial-response">>, From 3882e01ee1d35d02d8d964ebedd7d1111697c9c7 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 14 Jan 2025 22:57:13 +0100 Subject: [PATCH 33/46] Fix token_auth_fails_when_token_is_not_found testcase timing out --- big_tests/tests/fast_auth_token_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index d5f10f60e2c..3613c87a533 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -162,7 +162,7 @@ token_auth_fails_when_token_is_wrong(Config) -> token_auth_fails_when_token_is_not_found(Config) -> %% New token is not set - Steps = [start_new_user, receive_features], + Steps = [start_new_user], #{spec := Spec} = sasl2_helper:apply_steps(Steps, Config), Token = <<"wrongtoken">>, auth_with_token(failure, Token, Config, Spec). From 550705a1c1b453458ad5d8764048bed1ce6dbe40 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 20 Jan 2025 09:32:34 +0100 Subject: [PATCH 34/46] Add fast_auth_token into priv/cockroachdb.sql --- priv/cockroachdb.sql | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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) +); From 37c781b7f4a36c56690ee3c407e5c3bb0a31d4dd Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 20 Jan 2025 10:10:00 +0100 Subject: [PATCH 35/46] Fixed mssql schema for mod_fast_auth_token --- priv/mssql2012.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql index 92e7a442549..f35ef92351e 100644 --- a/priv/mssql2012.sql +++ b/priv/mssql2012.sql @@ -781,10 +781,10 @@ CREATE TABLE fast_auth_token( current_token VARCHAR(250), current_expire BIGINT, -- seconds unix timestamp current_count INT, -- replay counter - current_mech_id TINYINT UNSIGNED, + current_mech_id TINYINT, new_token VARCHAR(250), new_expire BIGINT, -- seconds unix timestamp new_count INT, - new_mech_id TINYINT UNSIGNED, + new_mech_id TINYINT, PRIMARY KEY(server, username, user_agent_id) ); From 725df2dcb3e363f6923eb8df0cd51d23a782b348 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 20 Jan 2025 10:10:34 +0100 Subject: [PATCH 36/46] Remove debug logging for mod_fast_auth_token --- src/fast_auth_token/mod_fast_auth_token.erl | 8 -------- src/fast_auth_token/mod_fast_auth_token_generic_mech.erl | 2 -- src/fast_auth_token/mod_fast_auth_token_rdbms.erl | 3 +-- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 9bcaaf40473..888f6ea3206 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -149,8 +149,6 @@ mechanisms() -> -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) -> {ok, SaslAcc} when SaslAcc :: mongoose_acc:t(). sasl2_start(SaslAcc, #{stanza := El}, _) -> - %% TODO remove this log - ?LOG_ERROR(#{what => sasl2_startttt, elleee => El, sasla_acc => SaslAcc}), 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">>}]), @@ -168,9 +166,6 @@ format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). -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}) -> - %% TODO remove this log - ?LOG_ERROR(#{what => sasl2_success_debug, sasl_acc => format_term(SaslAcc), c2s_state_data => format_term(C2SStateData), - creds => format_term(Creds)}), #{c2s_data := C2SData} = C2SStateData, #jid{luser = LUser, lserver = LServer} = mongoose_c2s:get_jid(C2SData), case check_if_should_add_token(HostType, SaslAcc, Creds) of @@ -248,12 +243,10 @@ 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), - ?LOG_ERROR(#{what => maybe_auto_rotate, slot => SlotUsed, data_used => format_term(DataUsed)}), case user_used_token_to_login(SlotUsed) of true -> case is_used_token_about_to_expire(HostType, SlotUsed, DataUsed) of true -> -?LOG_ERROR(#{what => rotate_rotate}), {ok, data_used_to_mech_type(SlotUsed, DataUsed), auto_rotate}; false -> skip @@ -275,7 +268,6 @@ is_timestamp_about_to_expire(HostType, Timestamp) -> Now = utc_now_as_seconds(), TimeBeforeRotate = get_time_to_rotate_before_expire_seconds(HostType), SecondsBeforeExpire = Timestamp - Now, -?LOG_ERROR(#{what => is_timestamp_about_to_expire, seconds_before => SecondsBeforeExpire, befor_rot => TimeBeforeRotate}), SecondsBeforeExpire =< TimeBeforeRotate. -spec user_used_token_to_login(token_slot() | undefined) -> boolean(). 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 index f20eb1ec2f3..aebf895d598 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -41,8 +41,6 @@ mech_step(#state{creds = Creds, agent_id = AgentId, mechanism = Mech}, Serialize LUser = jid:nodeprep(Username), case mod_fast_auth_token:read_tokens(HostType, LServer, LUser, AgentId) of {ok, TokenData} -> - %% TODO remove this log when done coding - ?LOG_ERROR(#{what => mech_step, token_data => TokenData}), CBData = <<>>, case handle_auth(TokenData, InitiatorHashedToken, CBData, Mech) of {true, TokenSlot} -> diff --git a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl index 89afcbf410e..9cd4c4f181a 100644 --- a/src/fast_auth_token/mod_fast_auth_token_rdbms.erl +++ b/src/fast_auth_token/mod_fast_auth_token_rdbms.erl @@ -16,8 +16,7 @@ init(HostType, _Opts) -> prepare_upsert(HostType), prepare_upsert_and_set_current(HostType), prepare(fast_select, fast_auth_token, - [current_token, current_expire, current_count, current_mech_id, - new_token, new_expire, new_count, new_mech_id], + [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 958e6c081a8f070890a4c40a26fcaa973f8137f1 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 20 Jan 2025 10:34:55 +0100 Subject: [PATCH 37/46] Add expiration logic for tokens and tests --- big_tests/tests/fast_auth_token_SUITE.erl | 57 ++++++++++++++++++- src/fast_auth_token/mod_fast_auth_token.erl | 2 - .../mod_fast_auth_token_generic_mech.erl | 4 +- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 3613c87a533..aff8b6af289 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -21,7 +21,7 @@ all() -> [{group, Group} || {Group, _, _} <- groups()]. groups() -> - [{ht_sha_256_none, [parallel], tests()} || {Group, _Mech} <- mechanisms()]. + [{Group, [parallel], tests()} || {Group, _Mech} <- mechanisms()]. tests() -> [server_advertises_support_for_fast, @@ -35,6 +35,8 @@ tests() -> 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, @@ -197,6 +199,19 @@ could_still_use_old_token_when_server_initiates_token_rotation_for_the_current_s %% 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), @@ -214,12 +229,12 @@ can_use_current_token_after_rerequest_token_with_initial_authentication(Config) client_requests_token_invalidation(Config) -> #{token := Token, spec := Spec} = connect_and_ask_for_token(Config), - ConnectRes = auth_with_token(success, Token, Config, Spec, request_invalidation), + 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), - ConnectRes = auth_with_token(success, Token, Config, Spec, request_invalidation_1), + 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) -> @@ -244,6 +259,42 @@ token_auth_fails_when_mechanism_does_not_match(Config) -> %% 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), diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 888f6ea3206..2ab1d3b5e41 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -161,8 +161,6 @@ maybe_put_inline_request(SaslAcc, _Module, undefined) -> maybe_put_inline_request(SaslAcc, Module, Request) -> mod_sasl2:put_inline_request(SaslAcc, Module, Request). -format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). - -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}) -> 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 index aebf895d598..c954a543989 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -18,7 +18,7 @@ Creds :: mongoose_credentials:t(), SocketData :: term(), Mech :: mod_fast_auth_token:mechanism()) -> {ok, state()} | {error, binary()}. -mech_new(_Host, Creds, SocketData = #{sasl_state := SaslState}, Mech) -> +mech_new(_Host, Creds, _SocketData = #{sasl_state := SaslState}, Mech) -> SaslModState = mod_sasl2:get_mod_state(SaslState), case SaslModState of #{encoded_id := AgentId} -> @@ -102,7 +102,7 @@ handle_auth(#{ %% 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) -> + when is_binary(Token), Expire > NowTimestamp -> Algo = mech_to_algo(Mech), crypto:mac(hmac, Algo, Token, ToHash) =:= InitiatorHashedToken; check_token(_, _) -> From 15442e7675b28ec3c71051f057ca732839e7ff4c Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Mon, 20 Jan 2025 12:07:59 +0100 Subject: [PATCH 38/46] Fix setting ht_mech in tests for groups --- big_tests/tests/fast_auth_token_SUITE.erl | 4 ++-- src/fast_auth_token/mod_fast_auth_token_generic_mech.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index aff8b6af289..c7d512ceb0a 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -88,9 +88,9 @@ end_per_suite(Config) -> init_per_group(Group, Config) -> case lists:keyfind(Group, 1, mechanisms()) of - Mech when is_binary(Mech) -> + {Group, Mech} when is_binary(Mech) -> [{ht_mech, Mech} | Config]; - _ -> + false -> Config 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 index c954a543989..c07145526f9 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -100,7 +100,7 @@ handle_auth(#{ end. %% Mech of the token in DB should match the mech the client is using. -check_token({Token, Expire, Count, Mech}, +check_token({Token, Expire, _Count, Mech}, {NowTimestamp, ToHash, InitiatorHashedToken, Mech}) when is_binary(Token), Expire > NowTimestamp -> Algo = mech_to_algo(Mech), From 87d904688b6d33bafbd47cf216e42246f76c4ff6 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 01:27:29 +0100 Subject: [PATCH 39/46] Rename mod_fast_auth_token_generic_mech:state() to fast_info() --- .../mod_fast_auth_token_generic_mech.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index c07145526f9..622b7b91cb0 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -10,29 +10,29 @@ %% Called from cyrsasl -export([supports_sasl_module/2, sasl_modules/0]). --record(state, {creds, agent_id, mechanism}). +-record(fast_info, {creds, agent_id, mechanism}). -include("mongoose.hrl"). --type state() :: #state{}. +-type fast_info() :: #fast_info{}. -spec mech_new(Host :: jid:server(), Creds :: mongoose_credentials:t(), SocketData :: term(), - Mech :: mod_fast_auth_token:mechanism()) -> {ok, state()} | {error, binary()}. + 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, #state{creds = Creds, agent_id = AgentId, mechanism = Mech}}; + {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 :: #state{}, +-spec mech_step(State :: fast_info(), ClientIn :: binary()) -> {ok, mongoose_credentials:t()} | {error, binary()}. -mech_step(#state{creds = Creds, agent_id = AgentId, mechanism = Mech}, SerializedToken) -> +mech_step(#fast_info{creds = Creds, agent_id = AgentId, mechanism = Mech}, SerializedToken) -> %% SerializedToken is base64 decoded. Parts = binary:split(SerializedToken, <<0>>), [Username, InitiatorHashedToken] = Parts, From 960b9419e825b170565b80ca4f74d0c42b136094 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 01:34:06 +0100 Subject: [PATCH 40/46] Add tests for mod_fast_auth_token module in test/config_parser_SUITE.erl --- test/config_parser_SUITE.erl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 90c6ce5eaa2..24fe29bf0b5 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). From 1b0441a6c9a0ef18579dd305bebef08b8e851f0a Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 01:39:15 +0100 Subject: [PATCH 41/46] Use map attributes --- big_tests/tests/fast_auth_token_SUITE.erl | 24 ++++++++++----------- src/fast_auth_token/mod_fast_auth_token.erl | 7 +++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index c7d512ceb0a..2321bef5409 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -139,7 +139,7 @@ request_token_with_unknown_mechanism_type(Config0) -> receive_features], #{answer := Success} = sasl2_helper:apply_steps(Steps, Config), ?assertMatch(#xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Success), + attrs = #{<<"xmlns">> => ?NS_SASL_2}}, Success), Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]), ?assertEqual(undefined, Fast). @@ -346,7 +346,7 @@ connect_and_ask_for_token(Config) -> parse_connect_result(#{answer := Success, spec := Spec}) -> ?assertMatch(#xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, 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">>), @@ -382,20 +382,20 @@ auth_using_token_and_request_invalidation_1(Config, Client, Data) -> %% request_token(Mech) -> #xmlel{name = <<"request-token">>, - attrs = [{<<"xmlns">>, ?NS_FAST}, + attrs = #{<<"xmlns">> => ?NS_FAST}, {<<"mechanism">>, Mech}]}. %% request_invalidation() -> #xmlel{name = <<"fast">>, - attrs = [{<<"xmlns">>, ?NS_FAST}, - {<<"invalidate">>, <<"true">>}]}. + attrs = #{<<"xmlns">> => ?NS_FAST, + <<"invalidate">> => <<"true">>}}. %% or request_invalidation_1() -> #xmlel{name = <<"fast">>, - attrs = [{<<"xmlns">>, ?NS_FAST}, - {<<"invalidate">>, <<"1">>}]}. + attrs = #{<<"xmlns">> => ?NS_FAST, + <<"invalidate">> => <<"1">>}}. auth_with_token(Success, Token, Config, Spec) -> auth_with_token(Success, Token, Config, Spec, dont_request_token). @@ -409,10 +409,10 @@ auth_with_token(Success, Token, Config, Spec, RequestToken) -> case Success of success -> ?assertMatch(#xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}]}, Answer); + attrs = #{<<"xmlns">> => ?NS_SASL_2}}, Answer); failure -> ?assertMatch(#xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}], + attrs = #{<<"xmlns">> => ?NS_SASL_2}, children = [#xmlel{name = <<"not-authorized">>}]}, Answer) end, @@ -441,7 +441,7 @@ user_agent_id() -> user_agent() -> #xmlel{name = <<"user-agent">>, - attrs = [{<<"id">>, user_agent_id()}], + attrs = #{<<"id">> => user_agent_id()}, children = [cdata_elem(<<"software">>, <<"AwesomeXMPP">>), cdata_elem(<<"device">>, <<"Kiva's Phone">>)]}. @@ -459,7 +459,7 @@ auth_with_method(_Config, Client, Data, BindElems, Extra, Method) -> ht_auth_initial_response(Client, Method) end, BindEl = #xmlel{name = <<"bind">>, - attrs = [{<<"xmlns">>, ?NS_BIND_2}], + attrs = #{<<"xmlns">> => ?NS_BIND_2}, children = BindElems}, Authenticate = auth_elem(Method, [InitEl, BindEl | Extra]), escalus:send(Client, Authenticate), @@ -476,7 +476,7 @@ auth_with_method(_Config, Client, Data, BindElems, Extra, Method) -> auth_elem(Mech, Children) -> #xmlel{name = <<"authenticate">>, - attrs = [{<<"xmlns">>, ?NS_SASL_2}, {<<"mechanism">>, Mech}], + attrs = #{<<"xmlns">> => ?NS_SASL_2, <<"mechanism">> => Mech], children = Children}. %% Creates "Initiator First Message" diff --git a/src/fast_auth_token/mod_fast_auth_token.erl b/src/fast_auth_token/mod_fast_auth_token.erl index 2ab1d3b5e41..ffb91528be2 100644 --- a/src/fast_auth_token/mod_fast_auth_token.erl +++ b/src/fast_auth_token/mod_fast_auth_token.erl @@ -110,6 +110,7 @@ validity_period_spec() -> required = all }. +-spec supported_features() -> [atom()]. supported_features() -> [dynamic_domains]. -spec remove_user(Acc, Params, Extra) -> {ok, Acc} when @@ -135,7 +136,7 @@ sasl2_stream_features(Acc, _, _) -> fast() -> #xmlel{name = <<"fast">>, - attrs = [{<<"xmlns">>, ?NS_FAST}], + attrs = #{<<"xmlns">> => ?NS_FAST}, children = mechanisms_elems(mechanisms())}. mechanisms_elems(Mechs) -> @@ -311,8 +312,8 @@ make_fast_token_response(HostType, LServer, LUser, Mech, AgentId, Creds) -> 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}]}. + attrs = #{<<"xmlns">> => ?NS_FAST, <<"expire">> => Expire, + <<"token">> => Token}}. -spec maybe_set_current_slot(Creds :: mongoose_credentials:t()) -> SetCurrent :: set_current(). From 61ed944927365e8bc9299920bff5ac9628b0b333 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 01:46:02 +0100 Subject: [PATCH 42/46] Use crypto:hash_equals to compare hashes --- src/fast_auth_token/mod_fast_auth_token_generic_mech.erl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 index 622b7b91cb0..67a62f29c80 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -104,7 +104,12 @@ check_token({Token, Expire, _Count, Mech}, {NowTimestamp, ToHash, InitiatorHashedToken, Mech}) when is_binary(Token), Expire > NowTimestamp -> Algo = mech_to_algo(Mech), - crypto:mac(hmac, Algo, Token, ToHash) =:= InitiatorHashedToken; + 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. From 2fba63760c1eef54a6e640c2f633ecf3af7771f6 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 01:54:36 +0100 Subject: [PATCH 43/46] Fix syntax typos --- big_tests/tests/fast_auth_token_SUITE.erl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/big_tests/tests/fast_auth_token_SUITE.erl b/big_tests/tests/fast_auth_token_SUITE.erl index 2321bef5409..d4509b2ffc6 100644 --- a/big_tests/tests/fast_auth_token_SUITE.erl +++ b/big_tests/tests/fast_auth_token_SUITE.erl @@ -139,7 +139,7 @@ request_token_with_unknown_mechanism_type(Config0) -> receive_features], #{answer := Success} = sasl2_helper:apply_steps(Steps, Config), ?assertMatch(#xmlel{name = <<"success">>, - attrs = #{<<"xmlns">> => ?NS_SASL_2}}, Success), + attrs = #{<<"xmlns">> := ?NS_SASL_2}}, Success), Fast = exml_query:path(Success, [{element_with_ns, <<"token">>, ?NS_FAST}]), ?assertEqual(undefined, Fast). @@ -346,7 +346,7 @@ connect_and_ask_for_token(Config) -> parse_connect_result(#{answer := Success, spec := Spec}) -> ?assertMatch(#xmlel{name = <<"success">>, - attrs = #{<<"xmlns">> => ?NS_SASL_2}}, 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">>), @@ -382,8 +382,8 @@ auth_using_token_and_request_invalidation_1(Config, Client, Data) -> %% request_token(Mech) -> #xmlel{name = <<"request-token">>, - attrs = #{<<"xmlns">> => ?NS_FAST}, - {<<"mechanism">>, Mech}]}. + attrs = #{<<"xmlns">> => ?NS_FAST, + <<"mechanism">> => Mech}}. %% request_invalidation() -> @@ -409,10 +409,10 @@ auth_with_token(Success, Token, Config, Spec, RequestToken) -> case Success of success -> ?assertMatch(#xmlel{name = <<"success">>, - attrs = #{<<"xmlns">> => ?NS_SASL_2}}, Answer); + attrs = #{<<"xmlns">> := ?NS_SASL_2}}, Answer); failure -> ?assertMatch(#xmlel{name = <<"failure">>, - attrs = #{<<"xmlns">> => ?NS_SASL_2}, + attrs = #{<<"xmlns">> := ?NS_SASL_2}, children = [#xmlel{name = <<"not-authorized">>}]}, Answer) end, @@ -476,7 +476,7 @@ auth_with_method(_Config, Client, Data, BindElems, Extra, Method) -> auth_elem(Mech, Children) -> #xmlel{name = <<"authenticate">>, - attrs = #{<<"xmlns">> => ?NS_SASL_2, <<"mechanism">> => Mech], + attrs = #{<<"xmlns">> => ?NS_SASL_2, <<"mechanism">> => Mech}, children = Children}. %% Creates "Initiator First Message" From 162f1410e7c0707d9751212a371fa80e57d3176b Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 03:15:00 +0100 Subject: [PATCH 44/46] Add types to the record fast_info --- src/fast_auth_token/mod_fast_auth_token_generic_mech.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index 67a62f29c80..04475613469 100644 --- a/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl +++ b/src/fast_auth_token/mod_fast_auth_token_generic_mech.erl @@ -10,7 +10,11 @@ %% Called from cyrsasl -export([supports_sasl_module/2, sasl_modules/0]). --record(fast_info, {creds, agent_id, mechanism}). +-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{}. From 72f29f8d89afea81b122e4f8da6e9c534a0e825d Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 06:01:54 +0100 Subject: [PATCH 45/46] Add assert_configurable_module info config_parse_SUITE It will save a lot of time next time people would add a new module Previous behaviour would just ignore any parameters and make an error in deeply nested code. That would confuse people a lot. Default behaviour is to ignore config_spec even if it is defined. Unless module is listed in a separate module list. Fix mod_fast_auth_token config tests. --- src/config/mongoose_config_spec.erl | 5 +++++ test/config_parser_SUITE.erl | 14 ++++++++++++++ 2 files changed, 19 insertions(+) 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/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 24fe29bf0b5..577348b7847 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -3118,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) -> From a48bb5105c1faec0788dc938ff122be528c73ca4 Mon Sep 17 00:00:00 2001 From: Mikhail Uvarov Date: Tue, 21 Jan 2025 06:17:49 +0100 Subject: [PATCH 46/46] Add docs for mod_fast_auth_token --- doc/configuration/Modules.md | 3 +++ doc/modules/mod_fast_auth_token.md | 39 ++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 43 insertions(+) create mode 100644 doc/modules/mod_fast_auth_token.md 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/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'