From 2caaa09c99880deb1c959e8af43729a286feebbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Tue, 17 Dec 2024 10:56:11 +0100 Subject: [PATCH] Add support for XEP-0484: Fast Authentication Streamlining Tokens --- mix.exs | 4 +- mix.lock | 2 +- rebar.config | 2 +- rebar.lock | 13 +-- src/ejabberd_c2s.erl | 17 +++- src/mod_auth_fast.erl | 167 +++++++++++++++++++++++++++++++++++ src/mod_auth_fast_mnesia.erl | 123 ++++++++++++++++++++++++++ src/mod_auth_fast_opt.erl | 27 ++++++ 8 files changed, 344 insertions(+), 11 deletions(-) create mode 100644 src/mod_auth_fast.erl create mode 100644 src/mod_auth_fast_mnesia.erl create mode 100644 src/mod_auth_fast_opt.erl diff --git a/mix.exs b/mix.exs index 516aa9e49..14f96f653 100644 --- a/mix.exs +++ b/mix.exs @@ -135,7 +135,7 @@ defmodule Ejabberd.MixProject do {:eimp, "~> 1.0"}, {:ex_doc, "~> 0.31", only: [:dev, :edoc], runtime: false}, {:fast_tls, "~> 1.1.22"}, - {:fast_xml, "~> 1.1.53"}, + {:fast_xml, "~> 1.1.53", override: true}, {:fast_yaml, "~> 1.0"}, {:idna, "~> 6.0"}, {:mqtree, "~> 1.0"}, @@ -144,7 +144,7 @@ defmodule Ejabberd.MixProject do {:p1_utils, "~> 1.0"}, {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, - {:xmpp, "~> 1.9"}, + {:xmpp, git: "https://github.com/processone/xmpp", ref: "333f688da2f52c73f374a46df139789a48c45395", override: true}, {:yconf, git: "https://github.com/processone/yconf.git", ref: "9898754f16cbd4585a1c2061d72fa441ecb2e938", override: true}] ++ cond_deps() end diff --git a/mix.lock b/mix.lock index e59fa341a..f1fdafa90 100644 --- a/mix.lock +++ b/mix.lock @@ -34,6 +34,6 @@ "stringprep": {:hex, :stringprep, "1.0.30", "46cf0ff631b3e7328f61f20b454d59428d87738f25d709798b5dcbb9b83c23f1", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "f6fc9b3384a03877830f89b2f38580caf3f4a27448a4a333d6a8c3975c220b9a"}, "stun": {:hex, :stun, "1.2.15", "eec510af6509201ff97f1f2c87b7977c833bf29c04e985383370ec21f04e4ccf", [:rebar3], [{:fast_tls, "1.1.22", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "f6d8a541a29fd13f2ce658b676c0cc661262b96e045b52def1644b75ebc0edef"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "xmpp": {:hex, :xmpp, "1.9.0", "d92446bf51d36adda02db63b963fe6d4a1ede33e59b38a43d9b90afd20c25b74", [:rebar3], [{:ezlib, "~> 1.0.12", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "~> 1.1.19", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "~> 1.1.51", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "~> 1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "~> 1.0.29", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "c1b91be74a9a9503afa6766f756477516920ffbfeea0c260c2fa171355f53c27"}, + "xmpp": {:git, "https://github.com/processone/xmpp", "333f688da2f52c73f374a46df139789a48c45395", [ref: "333f688da2f52c73f374a46df139789a48c45395"]}, "yconf": {:git, "https://github.com/processone/yconf.git", "9898754f16cbd4585a1c2061d72fa441ecb2e938", [ref: "9898754f16cbd4585a1c2061d72fa441ecb2e938"]}, } diff --git a/rebar.config b/rebar.config index 75615f529..7009d5311 100644 --- a/rebar.config +++ b/rebar.config @@ -77,7 +77,7 @@ {stringprep, "~> 1.0.29", {git, "https://github.com/processone/stringprep", {tag, "1.0.30"}}}, {if_var_true, stun, {stun, "~> 1.2.12", {git, "https://github.com/processone/stun", {tag, "1.2.15"}}}}, - {xmpp, "~> 1.9.0", {git, "https://github.com/processone/xmpp", {tag, "1.9.0"}}}, + {xmpp, "~> 1.9.0", {git, "https://github.com/processone/xmpp", "333f688da2f52c73f374a46df139789a48c45395"}}, {yconf, ".*", {git, "https://github.com/processone/yconf", "9898754f16cbd4585a1c2061d72fa441ecb2e938"}} ]}. diff --git a/rebar.lock b/rebar.lock index 926234015..f622208d0 100644 --- a/rebar.lock +++ b/rebar.lock @@ -10,7 +10,7 @@ {<<"fast_xml">>,{pkg,<<"fast_xml">>,<<"1.1.53">>},0}, {<<"fast_yaml">>,{pkg,<<"fast_yaml">>,<<"1.0.37">>},0}, {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},0}, - {<<"jiffy">>,{pkg,<<"jiffy">>,<<"1.1.2">>},1}, + {<<"jiffy">>,{pkg,<<"jiffy">>,<<"1.1.2">>},0}, {<<"jose">>,{pkg,<<"jose">>,<<"1.11.10">>},0}, {<<"luerl">>,{pkg,<<"luerl">>,<<"1.2.0">>},0}, {<<"mqtree">>,{pkg,<<"mqtree">>,<<"1.0.17">>},0}, @@ -24,7 +24,10 @@ {<<"stringprep">>,{pkg,<<"stringprep">>,<<"1.0.30">>},0}, {<<"stun">>,{pkg,<<"stun">>,<<"1.2.15">>},0}, {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}, - {<<"xmpp">>,{pkg,<<"xmpp">>,<<"1.9.0">>},0}, + {<<"xmpp">>, + {git,"https://github.com/processone/xmpp", + {ref,"333f688da2f52c73f374a46df139789a48c45395"}}, + 0}, {<<"yconf">>, {git,"https://github.com/processone/yconf", {ref,"9898754f16cbd4585a1c2061d72fa441ecb2e938"}}, @@ -55,8 +58,7 @@ {<<"sqlite3">>, <<"E819DEFD280145C328457D7AF897D2E45E8E5270E18812EE30B607C99CDD21AF">>}, {<<"stringprep">>, <<"46CF0FF631B3E7328F61F20B454D59428D87738F25D709798B5DCBB9B83C23F1">>}, {<<"stun">>, <<"EEC510AF6509201FF97F1F2C87B7977C833BF29C04E985383370EC21F04E4CCF">>}, - {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}, - {<<"xmpp">>, <<"D92446BF51D36ADDA02DB63B963FE6D4A1EDE33E59B38A43D9B90AFD20C25B74">>}]}, + {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, {pkg_hash_ext,[ {<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>}, {<<"cache_tab">>, <<"8582B60A4A09B247EF86355BA9E07FCE9E11EDC0345A775C9171F971C72B6351">>}, @@ -82,6 +84,5 @@ {<<"sqlite3">>, <<"3C0BA4E13322C2AD49DE4E2DDD28311366ADDE54BEAE8DBA9D9E3888F69D2857">>}, {<<"stringprep">>, <<"F6FC9B3384A03877830F89B2F38580CAF3F4A27448A4A333D6A8C3975C220B9A">>}, {<<"stun">>, <<"F6D8A541A29FD13F2CE658B676C0CC661262B96E045B52DEF1644B75EBC0EDEF">>}, - {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}, - {<<"xmpp">>, <<"C1B91BE74A9A9503AFA6766F756477516920FFBFEEA0C260C2FA171355F53C27">>}]} + {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} ]. diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 55d9cebec..06ba6fb02 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -45,7 +45,8 @@ handle_unbinded_packet/2, inline_stream_features/1, handle_sasl2_inline/2, handle_sasl2_inline_post/3, handle_bind2_inline/2, handle_bind2_inline_post/3, sasl_options/1, - handle_sasl2_task_next/4, handle_sasl2_task_data/3]). + handle_sasl2_task_next/4, handle_sasl2_task_data/3, + get_fast_tokens_fun/2, fast_mechanisms/1]). %% Hooks -export([handle_unexpected_cast/2, handle_unexpected_call/3, process_auth_result/3, reject_unauthenticated_packet/2, @@ -465,6 +466,20 @@ check_password_digest_fun(_Mech, #{lserver := LServer}) -> ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG) end. +get_fast_tokens_fun(_Mech, #{lserver := LServer}) -> + fun(User, UA) -> + case gen_mod:is_loaded(LServer, mod_auth_fast) of + false -> false; + _ -> mod_auth_fast:get_tokens(LServer, User, UA) + end + end. + +fast_mechanisms(#{lserver := LServer}) -> + case gen_mod:is_loaded(LServer, mod_auth_fast) of + false -> []; + _ -> mod_auth_fast:get_mechanisms(LServer) + end. + bind(<<"">>, State) -> bind(new_uniq_id(), State); bind(R, #{user := U, server := S, access := Access, lang := Lang, diff --git a/src/mod_auth_fast.erl b/src/mod_auth_fast.erl new file mode 100644 index 000000000..6b15aedbb --- /dev/null +++ b/src/mod_auth_fast.erl @@ -0,0 +1,167 @@ +%%%------------------------------------------------------------------- +%%% Author : Pawel Chmielowski +%%% Created : 1 Dec 2024 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2024 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- +-module(mod_auth_fast). +-behaviour(gen_mod). +-protocol({xep, 484, '0.2.0', '24.12', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). +-export([mod_doc/0]). +%% Hooks +-export([c2s_inline_features/2, c2s_handle_sasl2_inline/1, + get_tokens/3, get_mechanisms/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("xmpp/include/scram.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-callback get_tokens(binary(), binary(), binary()) -> + [{current | next, binary(), non_neg_integer()}]. +-callback rotate_token(binary(), binary(), binary()) -> + ok | {error, atom()}. +-callback del_token(binary(), binary(), binary(), current | next) -> + ok | {error, atom()}. +-callback set_token(binary(), binary(), binary(), current | next, binary(), non_neg_integer()) -> + ok | {error, atom()}. + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec start(binary(), gen_mod:opts()) -> {ok, [gen_mod:registration()]}. +start(Host, Opts) -> + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + {ok, [{hook, c2s_inline_features, c2s_inline_features, 50}, + {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10}]}. + +-spec stop(binary()) -> ok. +stop(_Host) -> + ok. + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + ok. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(token_lifetime) -> + econf:timeout(second); +mod_opt_type(token_refresh_age) -> + econf:timeout(second). + +-spec mod_options(binary()) -> [{atom(), any()}]. +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {token_lifetime, timer:hours(30*24)}, + {token_refresh_age, timer:hours(24)}]. + +mod_doc() -> + #{desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0484.html" + "[XEP-0480: Fast Authentication Streamlining Tokens] that allows users to authenticate " + "using self managed tokens.")], + note => "added in 24.12", + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {token_lifetime, + #{value => "timeout()", + desc => ?T("Time that tokens will be keept, measured from it's creation time. " + "Default value set to 30 days")}}, + {token_refresh_age, + #{value => "timeout()", + desc => ?T("This time determines age of token, that qualifies for automatic refresh. " + "Default value set to 1 day")}}], + example => + ["modules:", + " mod_auth_fast:", + " token_timeout: 14days"]}. + +get_mechanisms(_LServer) -> + [<<"HT-SHA-256-NONE">>, <<"HT-SHA-256-UNIQ">>, <<"HT-SHA-256-EXPR">>, <<"HT-SHA-256-ENDP">>]. + +ua_hash(UA) -> + crypto:hash(sha256, UA). + +get_tokens(LServer, LUser, UA) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + ToRefresh = erlang:system_time(second) - mod_auth_fast_opt:token_refresh_age(LServer), + lists:map( + fun({Type, Token, CreatedAt}) -> + {{Type, CreatedAt < ToRefresh}, Token} + end, Mod:get_tokens(LServer, LUser, ua_hash(UA))). + +c2s_inline_features({Sasl, Bind, Extra}, Host) -> + {Sasl ++ [#fast{mechs = get_mechanisms(Host)}], Bind, Extra}. + +gen_token(#{sasl2_ua_id := UA, server := Server, user := User}) -> + Mod = gen_mod:db_mod(Server, ?MODULE), + Token = base64url:encode(ua_hash(<>)), + ExpiresAt = erlang:system_time(second) + mod_auth_fast_opt:token_lifetime(Server), + Mod:set_token(Server, User, ua_hash(UA), next, Token, ExpiresAt), + #fast_token{token = Token, expiry = misc:usec_to_now(ExpiresAt)}. + +c2s_handle_sasl2_inline({#{server := Server, user := User, sasl2_ua_id := UA, + sasl2_axtra_auth_info := Extra} = State, Els, Results} = Acc) -> + Mod = gen_mod:db_mod(Server, ?MODULE), + ?ERROR_MSG("inl ~p", [Extra]), + NeedRegen = + case Extra of + {token, {next, Rotate}} -> + Mod:rotate_token(Server, User, ua_hash(UA)), + Rotate; + {token, {_, true}} -> + true; + _ -> + false + end, + case {lists:keyfind(fast_request_token, 1, Els), lists:keyfind(fast, 1, Els)} of + {#fast_request_token{mech = _Mech}, #fast{invalidate = true}} -> + Mod:del_token(Server, User, ua_hash(UA), current), + {State, Els, [gen_token(State) | Results]}; + {_, #fast{invalidate = true}} -> + Mod:del_token(Server, User, ua_hash(UA), current), + Acc; + {#fast_request_token{mech = _Mech}, _} -> + {State, Els, [gen_token(State) | Results]}; + _ when NeedRegen -> + {State, Els, [gen_token(State) | Results]}; + _ -> + Acc + end. diff --git a/src/mod_auth_fast_mnesia.erl b/src/mod_auth_fast_mnesia.erl new file mode 100644 index 000000000..17793e10d --- /dev/null +++ b/src/mod_auth_fast_mnesia.erl @@ -0,0 +1,123 @@ +%%%------------------------------------------------------------------- +%%% File : mod_announce_mnesia.erl +%%% Author : Pawel Chmielowski +%%% Created : 1 Dec 2024 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2024 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_auth_fast_mnesia). + +-behaviour(mod_auth_fast). + +%% API +-export([init/2]). +-export([get_tokens/3, del_token/4, set_token/6, rotate_token/3]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). + +-record(mod_auth_fast, {key = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary()} | '$1', + token = <<>> :: binary() | '_', + created_at = 0 :: non_neg_integer() | '_', + expires_at = 0 :: non_neg_integer() | '_'}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, mod_auth_fast, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, mod_auth_fast)}]). + +-spec get_tokens(binary(), binary(), binary()) -> + [{current | next, binary(), non_neg_integer()}]. +get_tokens(LServer, LUser, UA) -> + Now = erlang:system_time(second), + case mnesia:dirty_read(mod_auth_fast, {LServer, LUser, token_id(UA, next)}) of + [#mod_auth_fast{token = Token, created_at = Created, expires_at = Expires}] when Expires > Now -> + [{next, Token, Created}]; + [#mod_auth_fast{}] -> + del_token(LServer, LUser, UA, next), + []; + _ -> + [] + end ++ + case mnesia:dirty_read(mod_auth_fast, {LServer, LUser, token_id(UA, current)}) of + [#mod_auth_fast{token = Token, created_at = Created, expires_at = Expires}] when Expires > Now -> + [{current, Token, Created}]; + [#mod_auth_fast{}] -> + del_token(LServer, LUser, UA, current), + []; + _ -> + [] + end. + +-spec rotate_token(binary(), binary(), binary()) -> + ok | {error, atom()}. +rotate_token(LServer, LUser, UA) -> + F = fun() -> + case mnesia:dirty_read(mod_auth_fast, {LServer, LUser, token_id(UA, next)}) of + [#mod_auth_fast{token = Token, created_at = Created, expires_at = Expires}] -> + mnesia:write(#mod_auth_fast{key = {LServer, LUser, token_id(UA, current)}, + token = Token, created_at = Created, + expires_at = Expires}), + mnesia:delete({mod_auth_fast, {LServer, LUser, token_id(UA, next)}}); + _ -> + ok + end + end, + transaction(F). + +-spec del_token(binary(), binary(), binary(), current | next) -> + ok | {error, atom()}. +del_token(LServer, LUser, UA, Type) -> + F = fun() -> + mnesia:delete({mod_auth_fast, {LServer, LUser, token_id(UA, Type)}}) + end, + transaction(F). + +-spec set_token(binary(), binary(), binary(), current | next, binary(), non_neg_integer()) -> + ok | {error, atom()}. +set_token(LServer, LUser, UA, Type, Token, Expires) -> + F = fun() -> + mnesia:write(#mod_auth_fast{key = {LServer, LUser, token_id(UA, Type)}, + token = Token, created_at = erlang:system_time(second), + expires_at = Expires}) + end, + transaction(F). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +token_id(UA, current) -> + <<"c:", UA/binary>>; +token_id(UA, _) -> + <<"n:", UA/binary>>. + +transaction(F) -> + case mnesia:transaction(F) of + {atomic, Res} -> + Res; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. diff --git a/src/mod_auth_fast_opt.erl b/src/mod_auth_fast_opt.erl new file mode 100644 index 000000000..19578aa2c --- /dev/null +++ b/src/mod_auth_fast_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_auth_fast_opt). + +-export([db_type/1]). +-export([token_lifetime/1]). +-export([token_refresh_age/1]). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_auth_fast, db_type). + +-spec token_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). +token_lifetime(Opts) when is_map(Opts) -> + gen_mod:get_opt(token_lifetime, Opts); +token_lifetime(Host) -> + gen_mod:get_module_opt(Host, mod_auth_fast, token_lifetime). + +-spec token_refresh_age(gen_mod:opts() | global | binary()) -> pos_integer(). +token_refresh_age(Opts) when is_map(Opts) -> + gen_mod:get_opt(token_refresh_age, Opts); +token_refresh_age(Host) -> + gen_mod:get_module_opt(Host, mod_auth_fast, token_refresh_age). +