diff --git a/include/mod_invites.hrl b/include/mod_invites.hrl index 104fd61ae94..8ea7a83f86c 100644 --- a/include/mod_invites.hrl +++ b/include/mod_invites.hrl @@ -15,7 +15,7 @@ invitee = <<>> :: binary(), created_at = calendar:now_to_datetime(erlang:timestamp()) :: calendar:datetime(), expires = calendar:gregorian_seconds_to_datetime(calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(erlang:timestamp())) + ?INVITE_TOKEN_EXPIRE_SECONDS_DEFAULT) :: calendar:datetime(), - type = roster_only :: roster_only | account_only | account_subscription, + type = roster_only :: roster_only | account_only | account_subscription | reset_token, %% If type is 'roster_only' then we indicate a token has been used to create %% an account (if allowed) by setting `account_name` to the name of the user %% (which should match `invitee`). diff --git a/priv/mod_invites/reset_error.html b/priv/mod_invites/reset_error.html new file mode 100644 index 00000000000..a1137cf83bb --- /dev/null +++ b/priv/mod_invites/reset_error.html @@ -0,0 +1,7 @@ +{% extends "base_min.html" %} +{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %} +{% block content %} +

{% trans "Password reset error" %}

+ +

{% if message %}{{ message }}{% else %}{% trans "Sorry, there was a problem changing your account password." %}{% endif %}

+{% endblock%} diff --git a/priv/mod_invites/reset_success.html b/priv/mod_invites/reset_success.html new file mode 100644 index 00000000000..e9fce15073c --- /dev/null +++ b/priv/mod_invites/reset_success.html @@ -0,0 +1,41 @@ +{% extends "base_min.html" %} +{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %} +{% block title %}{% trans "Password Change Success" %} | {{site_name}}{% endblock %} +{% block h1 %}{{site_name}}{% endblock %} +{% block content %} +

{% trans "Congratulations!" %}

+ +

{% blocktrans %}You have changed your password on {{ site_name }}.{% endblocktrans %}

+ +

{% trans "To continue chatting, you need to enter your new account credentials into your chosen XMPP software." %}

+ + +

{% trans "As a final reminder, your account details are shown below:" %}

+ +
+
+ +
+ +
+
+ {% if password %} +
+ +
+
+ +
+ +
+
+
+
+ {% endif %} +
+ + {% if password %} +

{% trans "Your password is stored encrypted on the server and will not be accessible after you close this page. Keep it safe and never share it with anyone." %}

+ {% endif %} +{% endblock %} diff --git a/priv/mod_invites/reset_token.html b/priv/mod_invites/reset_token.html new file mode 100644 index 00000000000..db00c8f079b --- /dev/null +++ b/priv/mod_invites/reset_token.html @@ -0,0 +1,48 @@ +{% extends "base_min.html" %} + +{% block title %}{% if error %}{% trans "Form Error" %}{% else %}{% trans "Reset Password" %}{% endif %} | {{ site_name }}{% endblock %} +{% block h1 %}{% if error %}{% trans "Form Error" %}{% else %}{% blocktrans %}Reset Password on {{ site_name }}{% endblocktrans %}{% endif %}{% endblock %} + +{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %} + +{% block content %} +

{% blocktrans %}Hey, {{ username }}, if you'd like to change your password, maybe because you forgot your old one, you've come to the right place!{% endblocktrans %}

+

{% blocktrans %}If this is not you, please disregard!{% endblocktrans %}

+
If you have a compatible XMPP/Jabber client installed on your system you can try using this button to reset your password instead. This way you don't need to copy your password. Otherwise proceed with the form below!
+
+ Change Password using your installed client
+ +

{% trans "Reset Password" %}

+ {%if error and error.class == 'undefined' %}{% endif %} +
+
+ +
+
+ +
+ @{{ domain }} +
+
+
+
+
+ +
+ +
+ {% if error.class == 'password' %}{{ error.text }}{% else %} + {% blocktrans %}Please provide a password! Minimum length: {{ password_min_length }}{% endblocktrans %}{% endif %} +
+ {% trans "Enter a secure password that you do not use anywhere else." %} +
+
+
+ + + +
+
+{% endblock %} diff --git a/src/mod_invites.erl b/src/mod_invites.erl index 51e72817c88..a0495c36097 100644 --- a/src/mod_invites.erl +++ b/src/mod_invites.erl @@ -44,7 +44,7 @@ %% commands -export([cleanup_expired/0, delete_invite_by_token/2, expire_invites/2, expire_invite_by_token/2, generate_invite/1, - generate_invite/2, list_invites/1]). + generate_invite/2, generate_reset_token/2, list_invites/1]). %% helpers -export([create_account_allowed/2, create_account_invite/4, get_invite/2, get_invites_tree_t/2, @@ -65,7 +65,7 @@ -include("ejabberd_web_admin.hrl"). -ifdef(TEST). --export([create_roster_invite/2, find_invites_tree_root_t/4, gen_invite/1, +-export([create_roster_invite/2, create_reset_token/2, find_invites_tree_root_t/4, gen_invite/1, gen_invite/2, get_invites/2, get_invites_tree_as_root_t/2, is_token_valid/3]). -endif. @@ -301,7 +301,7 @@ start(Host, Opts) -> %% note the sequence below is important {hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 10}, %% webadmin - {hook, webadmin_menu_system_post, web_menu_system, 1000-$i, global}, + {hook, webadmin_menu_system_post, web_menu_system, 1000 - $i, global}, {hook, webadmin_menu_main, webadmin_menu_main, 50, global}, {hook, webadmin_page_main, webadmin_page_main, 50, global}, {hook, webadmin_menu_host, webadmin_menu_host, 50}, @@ -392,6 +392,7 @@ webadmin_page_host(_, BaseArguments, [{force_execution, false}]), make_command(expire_invite_tokens, Request, BaseArguments, []), + make_command(generate_reset_token, Request, BaseArguments, [{force_execution, false}]), make_command(cleanup_expired_invite_tokens, Request, [], @@ -497,6 +498,20 @@ get_commands_spec() -> {<<"xmpp:juliet@example.com?register;preauth=4bsdpwVrRDQYnF9aQQKXGbF7">>, <<"https://example.com/invites/4bsdpwVrRDQYnF9aQQKXGbF7">>}, result = {invite, {tuple, [{invite_uri, string}, {landing_page, string}]}}}, + #ejabberd_commands{name = generate_reset_token, + tags = [accounts], + desc = + "Create a password reset token for user with given name on given host.", + module = ?MODULE, + function = generate_reset_token, + note = "added in 26.05", + args = [{username, binary}, {host, binary}], + args_desc = ["Username", "Hostname"], + args_example = [<<"juliet">>, <<"example.com">>], + result_example = + {<<"xmpp:juliet@example.com?register;preauth=4bsdpwVrRDQYnF9aQQKXGbF7">>, + <<"https://example.com/invites/4bsdpwVrRDQYnF9aQQKXGbF7">>}, + result = {invite, {tuple, [{invite_uri, string}, {landing_page, string}]}}}, #ejabberd_commands{name = list_invites, tags = [accounts], desc = "List invite tokens", @@ -549,11 +564,11 @@ expire_invites(User0, Server0) -> expire_invite_by_token(Host, Token) -> pretty_format_command_result(try_db_call(Host, expire_invite_by_token, [Host, Token])). --spec generate_invite(binary()) -> binary() | {error, any()}. +-spec generate_invite(binary()) -> {binary(), binary()} | {error, any()}. generate_invite(Host) -> generate_invite(<<>>, Host). --spec generate_invite(binary(), binary()) -> binary() | {error, any()}. +-spec generate_invite(binary(), binary()) -> {binary(), binary()} | {error, any()}. generate_invite(AccountName, Host) -> pretty_format_command_result(gen_invite(AccountName, Host)). @@ -565,7 +580,7 @@ gen_invite(Host) -> -endif. --spec gen_invite(binary(), binary()) -> binary() | {error, any()}. +-spec gen_invite(binary(), binary()) -> {binary(), binary()} | {error, any()}. gen_invite(AccountName, Host0) -> Host = jid:nameprep(Host0), case create_account_invite(Host, {<<>>, Host}, AccountName, false) of @@ -575,6 +590,16 @@ gen_invite(AccountName, Host0) -> {token_uri(Invite), landing_page(Host, Invite)} end. +-spec generate_reset_token(binary(), binary()) -> {binary(), binary()} | {error, any()}. +generate_reset_token(User, Host) -> + Res = case create_reset_token(User, Host) of + {error, _Reason} = Error -> + Error; + Invite -> + {token_uri(Invite), landing_page(Host, Invite)} + end, + pretty_format_command_result(Res). + list_invites(Host) -> Res = maybe {ok, Invites} ?= try_db_call(Host, list_invites, [Host]), @@ -1128,11 +1153,37 @@ invite_token_t(Type, Host, Inviter, AccountName0) -> account_name = AccountName}, mod_invites_opt:token_expire_seconds(Host)). -token_uri(#invite_token{type = Type, +-spec create_reset_token(binary(), binary()) -> invite_token() | {error, any()}. +create_reset_token(User, Host) -> + maybe + (#invite_token{} = ResetToken) ?= reset_token(User, Host), + F = fun() -> db_call(Host, create_invite_t, [ResetToken]) end, + transaction(Host, F) + end. + +reset_token(User, Host) -> + maybe + true ?= lists:member(Host, ejabberd_option:hosts()) orelse {error, host_unknown}, + true ?= ejabberd_auth:user_exists(User, Host) orelse {error, user_not_exists}, + set_token_expires(#invite_token{token = + p1_rand:get_alphanum_string(?INVITE_TOKEN_LENGTH_DEFAULT), + inviter = {<<>>, Host}, + type = reset_token, + account_name = User}, + mod_invites_opt:token_expire_seconds(Host)) + end. + +token_uri(#invite_token{type = roster_only, token = Token, + inviter = {User, Host}}) -> + IBR = maybe_add_ibr_allowed(User, Host), + Inviter = + jid:encode( + jid:make(User, Host)), + <<"xmpp:", Inviter/binary, "?roster;preauth=", Token/binary, IBR/binary>>; +token_uri(#invite_token{token = Token, account_name = AccountName, - inviter = {_User, Host}}) - when Type =:= account_only; Type =:= account_subscription -> + inviter = {_User, Host}}) -> Invitee = case AccountName of <<>> -> @@ -1140,15 +1191,7 @@ token_uri(#invite_token{type = Type, _ -> <> end, - <<"xmpp:", Invitee/binary, "?register;preauth=", Token/binary>>; -token_uri(#invite_token{type = roster_only, - token = Token, - inviter = {User, Host}}) -> - IBR = maybe_add_ibr_allowed(User, Host), - Inviter = - jid:encode( - jid:make(User, Host)), - <<"xmpp:", Inviter/binary, "?roster;preauth=", Token/binary, IBR/binary>>. + <<"xmpp:", Invitee/binary, "?register;preauth=", Token/binary>>. maybe_add_ibr_allowed(User, Host) -> case create_account_allowed(Host, jid:make(User, Host)) of @@ -1312,6 +1355,8 @@ pretty_format_command_result({error, host_unknown}) -> {error, "Virtual host not known"}; pretty_format_command_result({error, user_exists}) -> {error, "Username already taken"}; +pretty_format_command_result({error, user_not_exists}) -> + {error, "User does not exist"}; pretty_format_command_result({ok, Result}) -> Result; pretty_format_command_result(Result) -> diff --git a/src/mod_invites_http.erl b/src/mod_invites_http.erl index de6f8826ab6..b3f4284e123 100644 --- a/src/mod_invites_http.erl +++ b/src/mod_invites_http.erl @@ -121,6 +121,8 @@ process([Token | _] = LocalPath, try mod_invites:is_token_valid(Host, Token) of true -> case mod_invites:get_invite(Host, Token) of + #invite_token{type = reset_token} = Invite -> + process_reset_token(Request, Invite, LocalPath); #invite_token{type = roster_only} = Invite -> process_roster_token(LocalPath, Request, Invite); Invite -> @@ -155,7 +157,10 @@ process([] = LocalPath, {ok, true, ok} -> AccountName = proplists:get_value(<<"account_name">>, Q, <<>>), Subscribe = proplists:get_value(<<"subscribe">>, Q, <<"no">>) == <<"yes">>, - case mod_invites:create_account_invite(Host, {Username, Host}, AccountName, Subscribe) + case mod_invites:create_account_invite(Host, + {jid:nodeprep(Username), Host}, + AccountName, + Subscribe) of #invite_token{} = Invite -> Ctx = [{uri, mod_invites:token_uri(Invite)}, @@ -233,25 +238,34 @@ process_valid_token([_Token] = LocalPath, process_valid_token(_, _, _) -> ?NOT_FOUND. -process_register_form(Invite, - AppID, - #request{host = Host, lang = Lang} = Request, - LocalPath) -> +process_reset_token(#request{method = 'POST'} = Request, Invite, LocalPath) -> + process_post(reset_token, Invite, <<>>, Request, LocalPath); +process_reset_token(Request, Invite, LocalPath) -> + process_form(reset_token, Invite, <<>>, Request, LocalPath). + +process_register_form(Invite, AppID, Request, LocalPath) -> + process_form(register, Invite, AppID, Request, LocalPath). + +process_form(Form, + Invite, + AppID, + #request{host = Host, lang = Lang} = Request, + LocalPath) -> try app_ctx(Host, AppID, Lang, ctx(Invite, Request, LocalPath)) of AppCtx -> CSRFCookie = gen_rand_id(), Ctx = [{csrf_token, csrf_token(CSRFCookie)} | maybe_add_username(AppCtx, Invite)], - Body = render_register_form(Request, Ctx), + Body = render_form(Form, Request, Ctx), Headers = add_cookie_header(maybe_hsts_header(is_https_lp(Host, Invite)), - csrf_cookie_string(<<"register-id">>, CSRFCookie)), + csrf_cookie_string(form_id(Form), CSRFCookie)), ?HTTP_OK(Headers, Body) catch _:not_found -> ?NOT_FOUND end. -render_register_form(#request{host = Host, lang = Lang}, Ctx) -> +render_form(Form, #request{host = Host, lang = Lang}, Ctx) -> MinLength = case mod_register_opt:password_strength(Host) of 0 -> @@ -259,23 +273,47 @@ render_register_form(#request{host = Host, lang = Lang}, Ctx) -> _ -> 6 end, - render(Host, Lang, <<"register.html">>, [{password_min_length, MinLength} | Ctx]). - -process_register_post(Invite, - AppID, - #request{host = Host, - q = Q, - lang = Lang, - path = Path, - ip = {Source, _}, - headers = Headers} = - Request, - LocalPath) -> + render(Host, Lang, form(Form), [{password_min_length, MinLength} | Ctx]). + +form(register) -> + <<"register.html">>; +form(reset_token) -> + <<"reset_token.html">>. + +form_success(register) -> + <<"register_success.html">>; +form_success(reset_token) -> + <<"reset_success.html">>. + +form_error(register) -> + <<"register_error.html">>; +form_error(reset_token) -> + <<"reset_error.html">>. + +form_id(register) -> + <<"register-id">>; +form_id(reset_token) -> + <<"reset-id">>. + +process_register_post(Invite, AppID, Request, LocalPath) -> + process_post(register, Invite, AppID, Request, LocalPath). + +process_post(Form, + Invite, + AppID, + #request{host = Host, + q = Q, + lang = Lang, + path = Path, + ip = {Source, _}, + headers = Headers} = + Request, + LocalPath) -> Username = proplists:get_value(<<"user">>, Q), Password = proplists:get_value(<<"password">>, Q), CSRFToken = proplists:get_value(<<"csrf_token">>, Q), Token = Invite#invite_token.token, - CSRFCookie = get_csrf_cookie(<<"register-id">>, Headers), + CSRFCookie = get_csrf_cookie(form_id(Form), Headers), try {app_ctx(Host, AppID, Lang, ctx(Invite, Request, LocalPath)), ensure_same(Token, proplists:get_value(<<"token">>, Q)), check_csrf(CSRFCookie, CSRFToken)} @@ -284,10 +322,11 @@ process_register_post(Invite, case mod_invites_register:try_register(Invite, Username, Host, Password, Source, Lang) of {ok, _UpdatedInvite} -> - Ctx = maybe_add_webchat_url(Host, + Ctx = maybe_add_webchat_url(Form, + Host, [{username, Username}, {password, Password} | AppCtx]), - render_ok(Host, Invite, Lang, <<"register_success.html">>, Ctx); + render_ok(Host, Invite, Lang, form_success(Form), Ctx); {error, #stanza_error{text = Text, type = Type, @@ -301,12 +340,12 @@ process_register_post(Invite, {csrf_token, CSRFToken}, {error, [{text, Msg}, {class, error_class(Reason)}]}] ++ AppCtx, - Body = render_register_form(Request, Ctx), + Body = render_form(Form, Request, Ctx), ?BAD_REQUEST(Body); _ -> render_bad_request(Host, is_https_lp(Host, Invite), - <<"register_error.html">>, + form_error(Form), [{message, Msg} | base_ctx(Host, Lang, Path, @@ -346,7 +385,7 @@ csrf_token(Msg) when Msg /= <<>> -> crypto:hash(sha256, SecretKey)), Msg)). -maybe_add_webchat_url(Host, Ctx) -> +maybe_add_webchat_url(register, Host, Ctx) -> case mod_invites_opt:webchat_url(Host) of none -> Ctx; @@ -362,7 +401,9 @@ maybe_add_webchat_url(Host, Ctx) -> end; WebchatUrl -> [{webchat_url, WebchatUrl} | Ctx] - end. + end; +maybe_add_webchat_url(_, _, Ctx) -> + Ctx. process_roster_token([_Token] = LocalPath, #request{host = Host, lang = Lang} = Request, @@ -597,7 +638,7 @@ parse_cookie_header(Headers) -> M end, #{}, - [binary:split(S, <<"=">>) || S <- binary:split(C, <<"; ">>)]). + [binary:split(S, <<"=">>) || S <- binary:split(C, <<"; ">>, [global])]). error_class('jid-malformed') -> username; diff --git a/src/mod_invites_register.erl b/src/mod_invites_register.erl index 42f1e978fb0..a52a78caf46 100644 --- a/src/mod_invites_register.erl +++ b/src/mod_invites_register.erl @@ -162,7 +162,7 @@ maybe_create_mutual_subscription(#invite_token{inviter = {User, Server}, process_token(#{server := Host} = State, Token, #iq{lang = Lang} = IQ) -> ?DEBUG("processing token (~s): ~s", [Host, Token]), - case can_create(Host, Token) of + case can_create_account_or_change_pw(Host, Token) of {true, Invite} -> NewState = State#{invite => Invite}, {NewState, xmpp:make_iq_result(IQ)}; @@ -170,19 +170,19 @@ process_token(#{server := Host} = State, Token, #iq{lang = Lang} = IQ) -> {State, preauth_invalid(IQ, Lang)} end. -can_create(Host, Token) -> +can_create_account_or_change_pw(Host, Token) -> try mod_invites:is_token_valid(Host, Token) of true -> case mod_invites:get_invite(Host, Token) of + #invite_token{type = reset_token} = Invite -> + {true, Invite}; #invite_token{type = roster_only, account_name = AccountName} when AccountName /= <<>> -> false; Invite -> - case create_account_allowed(Invite) of - ok -> - {true, Invite}; - {error, not_allowed} -> - false + maybe + true ?= create_account_allowed(Invite), + {true, Invite} end end; false -> @@ -201,35 +201,54 @@ create_account_allowed(#invite_token{type = roster_only} = Invite) -> fun() -> mod_invites:get_invites_tree_t(Host, {User, Host}) end)), - case NumInvites >= ?OVERUSE_LIMIT of - false -> - ok; - true -> - {error, not_allowed} - end; + NumInvites < ?OVERUSE_LIMIT; false -> - {error, not_allowed} + false end; create_account_allowed(#invite_token{inviter = {<<>>, _Host}}) -> - ok; + true; create_account_allowed(#invite_token{inviter = {User, Host}}) -> - mod_invites:create_account_allowed(Host, jid:make(User, Host)). + mod_invites:create_account_allowed(Host, jid:make(User, Host)) == ok. preauth_invalid(IQ, Lang) -> Text = ?T("The token provided is either invalid or expired."), make_stripped_error(IQ, #preauth{}, xmpp:err_item_not_found(Text, Lang)). +-spec try_register(mod_invites:invite_token(), + binary(), + binary(), + binary(), + tuple(), + binary()) -> + {ok, mod_invites:invite_token()} | {error, stanza_error()}. +try_register(#invite_token{type = reset_token} = Invite, + User, + Server, + Password, + _Source, + Lang) -> + case Invite#invite_token.account_name == User of + true -> + ChPwF = fun() -> mod_register:try_set_password(User, Server, Password) end, + NewInvite = + #invite_token{invitee = Invitee} = + maybe_set_invitee(Invite, jid:make(User, Server)), + case mod_invites:set_invitee(ChPwF, Server, Invite#invite_token.token, Invitee, User) of + ok -> + {ok, NewInvite}; + {error, Why} -> + {error, to_xmpp_error(Why, Lang)} + end; + false -> + {error, to_xmpp_error(not_allowed, Lang)} + end; try_register(Invite, User, Server, Password, Source, Lang) -> #invite_token{token = Token} = Invite, case {jid:nodeprep(User), not mod_invites:is_reserved(Server, Token, User)} of {error, _} -> - {error, - xmpp:err_jid_malformed( - mod_register:format_error(invalid_jid), Lang)}; + {error, to_xmpp_error(invalid_jid, Lang)}; {_, false} -> - {error, - xmpp:err_not_allowed( - mod_register:format_error(not_allowed), Lang)}; + {error, to_xmpp_error(not_allowed, Lang)}; {_, true} -> RegF = fun() -> @@ -244,14 +263,31 @@ try_register(Invite, User, Server, Password, Source, Lang) -> {ok, NewInvite}; {error, conflict} -> ?LOG_WARNING("Conflict when redeeming invite token: ~p", [NewInvite]), - {error, - xmpp:err_conflict( - mod_register:format_error(not_allowed), Lang)}; - {error, _Reason} = Error -> - Error + {error, to_xmpp_error(conflict, Lang)}; + {error, Why} -> + {error, to_xmpp_error(Why, Lang)} end end. +to_xmpp_error(Why, Lang) when Why == not_allowed; Why == invalid_password -> + xmpp:err_not_allowed( + mod_register:format_error(Why), Lang); +to_xmpp_error(weak_password = Why, Lang) -> + xmpp:err_not_acceptable( + mod_register:format_error(Why), Lang); +to_xmpp_error(invalid_jid = Why, Lang) -> + xmpp:err_jid_malformed( + mod_register:format_error(Why), Lang); +to_xmpp_error(db_failure = Why, Lang) -> + xmpp:err_internal_server_error( + mod_register:format_error(Why), Lang); +to_xmpp_error(conflict, Lang) -> + xmpp:err_conflict( + mod_register:format_error(not_allowed), Lang); +to_xmpp_error(Unexpected, Lang) -> + xmpp:err_internal_server_error( + mod_register:format_error(Unexpected), Lang). + check_captcha(true, #register{xdata = X}, #iq{lang = Lang} = IQ) -> XdataC = xmpp_util:set_xdata_field(#xdata_field{var = <<"FORM_TYPE">>, diff --git a/src/mod_invites_sql.erl b/src/mod_invites_sql.erl index 900be6bf3f4..bd72367a6e1 100644 --- a/src/mod_invites_sql.erl +++ b/src/mod_invites_sql.erl @@ -268,11 +268,12 @@ is_token_valid(Host, Token, {User, Host}) -> list_invites(Host) -> {selected, Rows} = ejabberd_sql:sql_query(Host, - ?SQL("SELECT @(token)s, @(username)s, @(type)s, @(account_name)s, " + ?SQL("SELECT @(token)s, @(username)s, @(invitee)s, @(type)s, @(account_name)s, " "@(expires)t, @(created_at)t FROM invite_token WHERE %(Host)H")), - lists:map(fun({Token, User, Type, AccountName, Expires, CreatedAt}) -> + lists:map(fun({Token, User, Invitee, Type, AccountName, Expires, CreatedAt}) -> #invite_token{token = Token, inviter = {User, Host}, + invitee = Invitee, type = dec_type(Type), account_name = AccountName, expires = Expires, @@ -314,11 +315,15 @@ enc_type(roster_only) -> enc_type(account_subscription) -> <<"S">>; enc_type(account_only) -> - <<"A">>. + <<"A">>; +enc_type(reset_token) -> + <<"T">>. dec_type(<<"R">>) -> roster_only; dec_type(<<"S">>) -> account_subscription; dec_type(<<"A">>) -> - account_only. + account_only; +dec_type(<<"T">>) -> + reset_token. diff --git a/src/mod_register.erl b/src/mod_register.erl index fb6e289ff57..90baba24358 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -33,7 +33,7 @@ -export([start/2, stop/1, reload/3, stream_feature_register/2, c2s_unauthenticated_packet/2, try_register/4, try_register/5, - try_register/6, process_iq/1, send_registration_notifications/3, + try_register/6, try_set_password/3, process_iq/1, send_registration_notifications/3, mod_opt_type/1, mod_options/1, depends/2, format_error/1, mod_doc/0]). diff --git a/test/invites_tests.erl b/test/invites_tests.erl index e035dec422a..0925d072a53 100644 --- a/test/invites_tests.erl +++ b/test/invites_tests.erl @@ -148,7 +148,8 @@ single_cases() -> single_test(ibr_subscription), single_test(ibr_conflict), single_test(http), - single_test(create_invite_page)]}. + single_test(create_invite_page), + single_test(reset_token)]}. %%%=================================================================== @@ -412,10 +413,7 @@ max_invites(Config0) -> Inviter = {User, Server}, OldOpts = gen_mod:get_module_opts(Server, mod_invites), NewOpts = - gen_mod_set_opts(OldOpts, - [{max_invites, 3}, - {access_create_account, account_invite}, - {allow_modules, [mod_invites]}]), + gen_mod_set_opts(OldOpts, [{max_invites, 3}, {access_create_account, account_invite}]), update_module_opts(Server, mod_invites, NewOpts), Config = reconnect(Config0), @@ -446,9 +444,7 @@ overuse(Config0) -> Inviter = {User, Server}, InviteeJID = jid:make(User, Server), OldOpts = gen_mod:get_module_opts(Server, mod_invites), - NewOpts = - gen_mod_set_opts(OldOpts, - [{access_create_account, account_invite}, {allow_modules, [mod_invites]}]), + NewOpts = gen_mod_set_opts(OldOpts, [{access_create_account, account_invite}]), update_module_opts(Server, mod_invites, NewOpts), Config1 = reconnect(Config0), @@ -973,6 +969,57 @@ create_invite_page(Config) -> update_module_opts(Server, mod_invites, OldOpts), disconnect(Config). +reset_token(Config0) -> + Server = ?config(server, Config0), + User = ?config(user, Config0), + Password = ?config(password, Config0), + + httpc:set_options([{cookies, enabled}]), + + OldRegisterOpts = gen_mod:get_module_opts(Server, mod_register), + NewRegisterOpts = gen_mod:set_opt(allow_modules, [mod_invites], OldRegisterOpts), + update_module_opts(Server, mod_register, NewRegisterOpts), + + Config1 = reconnect(Config0), + + #invite_token{token = Token} = mod_invites:create_reset_token(User, Server), + + BaseURL = mod_invites_http:landing_page(Server, mod_invites:get_invite(Server, Token)), + CSRFToken = get_csrf_token(BaseURL), + + ?match(true, ejabberd_auth:check_password(User, <<"plain">>, Server, Password)), + + ?match(#iq{type = error}, send_iq_register(Config1, User, <<"newPassword">>)), + ?match(#iq{type = result}, send_pars(Config1, Token)), + ?match(#iq{type = error}, send_iq_register(Config1, <<"wrong_user">>, <<"newPassword">>)), + ?match(#iq{type = result}, send_iq_register(Config1, User, <<"newPassword">>)), + + ?match(true, ejabberd_auth:check_password(User, <<"plain">>, Server, <<"newPassword">>)), + ?match(false, ejabberd_auth:check_password(User, <<"plain">>, Server, Password)), + + ?match(false, mod_invites:is_token_valid(Server, Token)), + + {ok, {{_, 404, _}, _, _}} = post(BaseURL, Token, CSRFToken, User, <<"anotherPassword">>), + + #invite_token{token = Token2} = mod_invites:create_reset_token(User, Server), + BaseURL2 = mod_invites_http:landing_page(Server, mod_invites:get_invite(Server, Token2)), + CSRFToken2 = get_csrf_token(BaseURL2), + + {ok, {{_, 400, _}, _, _}} = + post(BaseURL2, Token2, CSRFToken, User, <<"anotherPassword">>), + {ok, {{_, 400, _}, _, _}} = + post(BaseURL2, Token2, CSRFToken2, <<"wronguser">>, <<"anotherPassword">>), + {ok, {{_, 200, _}, _, _}} = + post(BaseURL2, Token2, CSRFToken2, User, <<"anotherPassword">>), + ?match(true, + ejabberd_auth:check_password(User, <<"plain">>, Server, <<"anotherPassword">>)), + + ok = mod_register:try_set_password(User, Server, Password), + update_module_opts(Server, mod_register, OldRegisterOpts), + mod_invites:expire_invites(<<>>, Server), + ?match(2, mod_invites:cleanup_expired()), + disconnect(Config1). + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -1076,12 +1123,15 @@ send_get_iq_register(Config) -> to = ServerJID, sub_els = [#register{}]}). -send_iq_register(Config, AccountName) -> +send_iq_register(Config, Username) -> + send_iq_register(Config, Username, <<"mySecret">>). + +send_iq_register(Config, Username, Password) -> ServerJID = jid:from_string(?config(server, Config)), send_recv(Config, #iq{type = set, to = ServerJID, - sub_els = [#register{username = AccountName, password = <<"mySecret">>}]}). + sub_els = [#register{username = Username, password = Password}]}). send_pars(Config, Token) -> ServerJID = jid:from_string(?config(server, Config)),