25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-12-24 17:29:28 +01:00

Add support for XEP-0484: Fast Authentication Streamlining Tokens

This commit is contained in:
Paweł Chmielowski 2024-12-17 10:56:11 +01:00
parent da06a50072
commit 2caaa09c99
8 changed files with 344 additions and 11 deletions

View File

@ -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

View File

@ -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"]},
}

View File

@ -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"}}
]}.

View File

@ -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">>}]}
].

View File

@ -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,

167
src/mod_auth_fast.erl Normal file
View File

@ -0,0 +1,167 @@
%%%-------------------------------------------------------------------
%%% Author : Pawel Chmielowski <pawel@process-one.net>
%%% Created : 1 Dec 2024 by Pawel Chmielowski <pawel@process-one.net>
%%%
%%%
%%% 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(<<UA/binary, (p1_rand:get_string())/binary>>)),
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.

View File

@ -0,0 +1,123 @@
%%%-------------------------------------------------------------------
%%% File : mod_announce_mnesia.erl
%%% Author : Pawel Chmielowski <pawel@process-one.net>
%%% Created : 1 Dec 2024 by Pawel Chmielowski <pawel@process-one.net>
%%%
%%%
%%% 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.

27
src/mod_auth_fast_opt.erl Normal file
View File

@ -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).