You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by di...@apache.org on 2026/05/01 18:20:56 UTC
(allura) 01/07: [#8601] use link instead of code for email auth, and longer code
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git
commit b14f36cf8f7a27b35138a4506275f628b48b45de
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue Apr 21 11:52:52 2026 -0400
[#8601] use link instead of code for email auth, and longer code
---
Allura/allura/controllers/auth.py | 71 +++++++++----
Allura/allura/lib/multifactor.py | 34 +++----
Allura/allura/lib/plugin.py | 13 ++-
Allura/allura/model/auth.py | 18 +++-
...thentication_code.txt => login_email_sent.html} | 21 ++--
Allura/allura/templates/login_multifactor.html | 66 +++++-------
.../allura/templates/mail/authentication_code.txt | 6 +-
Allura/allura/tests/functional/test_auth.py | 112 ++++++++++-----------
8 files changed, 190 insertions(+), 151 deletions(-)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index fee01427a..6a3a3bc51 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -16,6 +16,7 @@
# under the License.
from __future__ import annotations
+import hmac
import logging
import json
import os
@@ -27,7 +28,7 @@
import bson
import formencode as fe
import tg
-from tg import expose, flash, redirect, validate, config, session
+from tg import expose, flash, redirect, validate, config, session, override_template
from tg.decorators import with_trailing_slash, without_trailing_slash
from tg import tmpl_context as c, app_globals as g
from tg import request, response
@@ -404,8 +405,10 @@ def _verify_return_to(return_to):
@utils.AntiSpam.validate('Spambot protection engaged')
def do_login(self, return_to=None, **kw):
location = '/'
- if session.get('multifactor-username'):
- location = tg.url('/auth/multifactor', dict(return_to=return_to))
+ if session.get('mode') == 'email_code':
+ location = tg.url('/auth/login_email_sent', dict(return_to=return_to) if return_to else {})
+ elif session.get('multifactor-username'):
+ location = tg.url('/auth/multifactor', dict(return_to=return_to) if return_to else {})
elif session.get('expired-username'):
if return_to and return_to not in plugin.AuthenticationProvider.pwd_expired_allowed_urls:
location = tg.url(plugin.AuthenticationProvider.pwd_expired_allowed_urls[0], dict(return_to=return_to))
@@ -418,15 +421,17 @@ def do_login(self, return_to=None, **kw):
@expose('jinja:allura:templates/login_multifactor.html')
def multifactor(self, return_to='', mode='totp', **kwargs):
- if not asbool(config.get('auth.multifactor.totp', False)):
- raise wexc.HTTPNotFound
-
if not c.user.is_anonymous():
# already logged in, no need to do this form
redirect(self._verify_return_to(return_to))
- # currently only email_code mode is set via session
- if session.get('mode') == 'email_code':
- mode = 'email_code'
+
+ # this URL is also used for email code verification, but there's a different template to use
+ if session.get('mode') == 'email_code' and asbool(config.get('auth.email_auth_code.enabled', False)):
+ override_template(self.multifactor, 'jinja:allura:templates/login_email_sent.html')
+ return dict()
+
+ if not asbool(config.get('auth.multifactor.totp', False)):
+ raise wexc.HTTPNotFound
return dict(
return_to=return_to,
@@ -445,7 +450,7 @@ def do_multifactor(self, code, mode, **kwargs):
if 'multifactor-username' not in session:
tg.flash('Your multifactor login was disrupted, please start over.', 'error')
- plugin.AuthenticationProvider.get(request).logout() # clears all cookies that might be interfering
+ plugin.AuthenticationProvider.get(request).logout() # clears all cookies that might be interfering - also the flash message cookie :(
redirect('/auth/', {'return_to': kwargs.get('return_to', '')})
user = M.User.by_username(session['multifactor-username'])
@@ -458,10 +463,6 @@ def do_multifactor(self, code, mode, **kwargs):
recovery = RecoveryCodeService.get()
recovery.verify_and_remove_code(user, code)
h.auditlog_user('Logged in using a multifactor recovery code', user=user)
- elif mode == 'email_code':
- email_code_service = EmailCodeAuthenticationService()
- email_code_service.validate_code(user, code)
- h.auditlog_user('Logged in using email authentication code', user=user)
except (InvalidToken, InvalidRecoveryCode):
request.validation.errors['code'] = 'Invalid code, please try again.'
h.auditlog_user('Multifactor login - invalid code', user=user)
@@ -470,15 +471,49 @@ def do_multifactor(self, code, mode, **kwargs):
request.validation.errors['code'] = 'Multifactor rate limit exceeded, slow down and try again later.'
h.auditlog_user('Multifactor login - rate limit', user=user)
return self.multifactor(mode=mode, **kwargs)
- except InvalidEmailAuthCode:
- request.validation.errors['code'] = 'Invalid code, please try again.'
- h.auditlog_user('Email code login - invalid code', user=user)
- return self.multifactor(mode=mode, **kwargs)
else:
plugin.AuthenticationProvider.get(request).login(user=user, multifactor_success=True)
return_to = self._verify_return_to(kwargs.get('return_to'))
redirect(return_to)
+ @expose('jinja:allura:templates/login_email_sent.html')
+ def login_email_sent(self, return_to='/', **kwargs):
+ if not asbool(config.get('auth.email_auth_code.enabled', False)):
+ raise wexc.HTTPNotFound
+ if not c.user.is_anonymous():
+ redirect(self._verify_return_to(return_to))
+ if 'multifactor-username' not in session:
+ redirect('/auth/')
+ return dict()
+
+ @expose()
+ def login_email_verify(self, token, return_to='/', **kwargs):
+ if not asbool(config.get('auth.email_auth_code.enabled', False)):
+ raise wexc.HTTPNotFound
+ if not c.user.is_anonymous():
+ redirect(self._verify_return_to(return_to))
+
+ if not session.get('multifactor-username'):
+ tg.flash('Your login session was disrupted. Be sure to use the same browser when logging in and opening the email verification link.', 'error')
+ redirect('/auth/', {'return_to': kwargs['return_to']} if kwargs.get('return_to') else {})
+
+ user = M.User.by_username(session['multifactor-username'])
+ if not user:
+ redirect('/auth/')
+
+ try:
+ EmailCodeAuthenticationService().validate_token(user, token)
+ except InvalidEmailAuthCode:
+ tg.flash('Invalid or expired login link.', 'error')
+ session.pop('multifactor-username', None)
+ session.pop('mode', None)
+ session.save()
+ redirect('/auth/')
+
+ h.auditlog_user('Logged in using email authentication link', user=user)
+ plugin.AuthenticationProvider.get(request).login(user=user, multifactor_success=True)
+ redirect(self._verify_return_to(return_to))
+
@expose(content_type='text/plain')
def refresh_repo(self, *repo_path):
# post-commit hooks use this
diff --git a/Allura/allura/lib/multifactor.py b/Allura/allura/lib/multifactor.py
index 053c670e6..1a46fe05f 100644
--- a/Allura/allura/lib/multifactor.py
+++ b/Allura/allura/lib/multifactor.py
@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
+import hmac
import os
import logging
import random
@@ -458,23 +458,21 @@ def verify_and_remove_code(self, user, code):
raise InvalidRecoveryCode
-class EmailCodeAuthenticationService(MongodbMultifactorCommon):
- def generate_code(self, user):
- code_length = 6
- code_expiration_time = 300 # seconds
-
- code = ''.join(secrets.choice(string.digits) for i in range(code_length))
- code_expiration_timestamp = int(time() + code_expiration_time)
- user.set_tool_data('AuthEmailCode', code=code, code_expiry=code_expiration_timestamp)
- return code
-
- def validate_code(self, user, code):
- user_code = user.get_tool_data('AuthEmailCode', 'code')
- user_code_expiry = user.get_tool_data('AuthEmailCode', 'code_expiry')
- if user_code:
- self.enforce_rate_limit(user)
- if user_code == code and int(time()) < user_code_expiry:
+class EmailCodeAuthenticationService:
+ def generate_token(self, user):
+ token_expiration_secs = 10 * 60
+ token = secrets.token_urlsafe(nbytes=8)
+ token_expiration_timestamp = int(time() + token_expiration_secs)
+ user.set_tool_data('AuthEmailCode', token=token, token_expiry=token_expiration_timestamp)
+ return token
+
+ def validate_token(self, user, token):
+ user_token = user.get_tool_data('AuthEmailCode', 'token')
+ user_token_expiry = user.get_tool_data('AuthEmailCode', 'token_expiry')
+ if user_token:
+ # compare_digest more secure than == (against timing analysis attacks)
+ if hmac.compare_digest(user_token, token) and int(time()) < user_token_expiry:
# remove the AuthEmailCode tool data from the user
- user.set_tool_data('AuthEmailCode', code="", code_expiry=0)
+ user.set_tool_data('AuthEmailCode', token='', token_expiry=0)
return True
raise InvalidEmailAuthCode
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 216d516e2..476c1bf69 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -96,6 +96,8 @@ class AuthenticationProvider:
multifactor_allowed_urls = [
'/auth/multifactor',
'/auth/do_multifactor',
+ '/auth/login_email_sent',
+ '/auth/login_email_verify',
'/auth/logout',
]
cfg_prefix_pwds = 'auth.password.'
@@ -188,7 +190,7 @@ def register_user(self, user_doc):
'''
raise NotImplementedError('register_user')
- def _login(self):
+ def _login(self) -> M.User:
'''
Authorize a user, usually using ``self.request.params['username']`` and ``['password']``
@@ -212,6 +214,9 @@ def login(self, user: M.User = None, multifactor_success: bool = False) -> M.Use
h.auditlog_user('Failed login', user=M.User.by_username(self.request.params['username']))
raise
+ if user.is_anonymous():
+ raise ValueError('Cannot log in an anonymous user')
+
if user.get_pref('multifactor') and not multifactor_success:
self.session['multifactor-username'] = user.username
h.auditlog_user('Multifactor login - password ok, code not entered yet', user=user)
@@ -232,7 +237,7 @@ def login(self, user: M.User = None, multifactor_success: bool = False) -> M.Use
self.session['multifactor-username'] = user.username
self.session['mode'] = 'email_code'
self.session.save()
- user.send_email_auth_code()
+ user.send_email_auth_code(return_to=self.request.params.get('return_to', '/'))
return None
else:
# Validate if we used an auth code to skip the `after_login` which sends a foreign login email
@@ -651,7 +656,7 @@ def register_user(self, user_doc):
u.reg_date = datetime.utcnow()
return u
- def _login(self):
+ def _login(self) -> M.User:
user = self.by_username(self.request.params['username'])
if not self.validate_password(user, self.request.params['password']):
raise exc.HTTPUnauthorized()
@@ -907,7 +912,7 @@ def set_password(self, user, old_password, new_password, set_timestamp=True):
except ldap.INVALID_CREDENTIALS:
raise exc.HTTPUnauthorized()
- def _login(self):
+ def _login(self) -> M.User:
if ldap is None:
raise Exception('The python-ldap package needs to be installed. '
'Run `pip install python-ldap` in your allura environment.')
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 0b331ed3a..70990552c 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -20,8 +20,9 @@
import logging
import typing
+import tg
from markupsafe import Markup
-from urllib.parse import urlparse
+from urllib.parse import urlparse, urlencode
from email import header
from hashlib import sha256
from collections import deque
@@ -470,14 +471,23 @@ def make_password_reset_url(self):
reset_url = h.absurl(f'/auth/forgotten_password/{hash}')
return reset_url
- def send_email_auth_code(self, subject_tmpl='{site_name} Authentication Code'):
+ def send_email_auth_code(self, return_to='/', subject_tmpl='{site_name} Authentication Code'):
+ from allura.controllers.auth import AuthController
+
email_address = self.get_pref('email_address')
- email_auth_code = EmailCodeAuthenticationService().generate_code(self)
+ token = EmailCodeAuthenticationService().generate_token(self)
+ return_to = AuthController._verify_return_to(return_to)
+ link_params = {
+ 'token': token,
+ }
+ if return_to and return_to != '/':
+ link_params['return_to'] = return_to
+ verify_url = h.absurl(tg.url('/auth/login_email_verify', link_params))
subject = subject_tmpl.format(site_name=config['site_name'])
text = g.jinja2_env.get_template('allura:templates/mail/authentication_code.txt').render(dict(
user=self,
config=config,
- email_auth_code=email_auth_code,
+ verify_url=verify_url,
))
allura.tasks.mail_tasks.send_system_mail_to_user(email_address, subject, text)
diff --git a/Allura/allura/templates/mail/authentication_code.txt b/Allura/allura/templates/login_email_sent.html
similarity index 61%
copy from Allura/allura/templates/mail/authentication_code.txt
copy to Allura/allura/templates/login_email_sent.html
index 9f4637116..9152677cc 100644
--- a/Allura/allura/templates/mail/authentication_code.txt
+++ b/Allura/allura/templates/login_email_sent.html
@@ -1,4 +1,4 @@
-{#
+{#-
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
@@ -16,11 +16,20 @@
specific language governing permissions and limitations
under the License.
-#}
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
-An attempt to sign-in to your {{ config['site_name'] }} account was detected.
+{% block title %}{{ config['site_name'] }} Verify Your Sign-In{% endblock %}
-If this was you, use the following verification code:
+{% block header %}Verify Your Sign-In{% endblock %}
-{{ email_auth_code }}
-
-If you didn't request it, click <a href="/auth/forgotten_password">here</a> to reset your password immediately
+{% block content %}
+<h2>Check Your Email</h2>
+<p>A sign-in verification link has been sent to your email address.</p>
+<p>Click the link in the email to complete your sign-in. The link expires in 10 minutes.</p>
+<p>
+ <a href="/auth/logout?return_to=/auth/">
+ <input type="button" value="Log Out and Start Over">
+ </a>
+</p>
+{% endblock %}
diff --git a/Allura/allura/templates/login_multifactor.html b/Allura/allura/templates/login_multifactor.html
index cf05f0064..579a482df 100644
--- a/Allura/allura/templates/login_multifactor.html
+++ b/Allura/allura/templates/login_multifactor.html
@@ -25,50 +25,30 @@
{% block content %}
<form method="post" action="/auth/do_multifactor">
- {% if mode == "email_code" %}
- <h2>E-mail Code Authentication</h2>
- <p>
- <span>Please enter the 6-digit code sent to your email:</span>
- <br>
- {% if request.validation.errors['code'] %}
- <span class="fielderror">{{ request.validation.errors['code'] }}</span><br>
- {% endif %}
- <input id="auth_code" type="text" name="code" autofocus autocomplete="off"/>
- <input type="hidden" name="return_to" value="{{ return_to }}"/>
- <br>
- <input type="submit" value="Log In">
- <a href="/auth/logout?return_to=/auth/{{ '?return_to=' + return_to | urlencode }}">
- <input type="button" value="Cancel">
- </a>
- <input type="hidden" name="mode" value="{{ mode }}"/>
- {{ lib.csrf_token() }}
- </p>
- {% else %}
- <h2>Enter your Multifactor Authentication Code</h2>
- <p>
- <span class="totp">Please enter the {{ config['auth.multifactor.totp.length'] }}-digit code from your authenticator app:</span>
- <span class="recovery">Please enter a recovery code:</span>
- <br>
- {% if request.validation.errors['code'] %}
- <span class="fielderror">{{ request.validation.errors['code'] }}</span><br>
- {% endif %}
- <input type="text" name="code" autofocus autocomplete="off"/>
- <input type="hidden" name="return_to" value="{{ return_to }}"/>
- <br>
- <input type="submit" value="Log In">
- <span class="alternate-links">
- <span class="totp">
- or <a href="#" class="show-recovery">use a recovery code</a>
- </span>
- <span class="recovery">
- or <a href="#" class="show-totp">use an authenticator app code</a>
- </span>
- </span>
- or <a href="/auth/logout?return_to=/auth/{{ '?return_to=' + return_to | urlencode }}">Cancel</a>
- <input type="hidden" name="mode" value="{{ mode }}"/>
- {{ lib.csrf_token() }}
- </p>
+ <h2>Enter your Multifactor Authentication Code</h2>
+ <p>
+ <span class="totp">Please enter the {{ config['auth.multifactor.totp.length'] }}-digit code from your authenticator app:</span>
+ <span class="recovery">Please enter a recovery code:</span>
+ <br>
+ {% if request.validation.errors['code'] %}
+ <span class="fielderror">{{ request.validation.errors['code'] }}</span><br>
{% endif %}
+ <input type="text" name="code" autofocus autocomplete="off"/>
+ <input type="hidden" name="return_to" value="{{ return_to }}"/>
+ <br>
+ <input type="submit" value="Log In">
+ <span class="alternate-links">
+ <span class="totp">
+ or <a href="#" class="show-recovery">use a recovery code</a>
+ </span>
+ <span class="recovery">
+ or <a href="#" class="show-totp">use an authenticator app code</a>
+ </span>
+ </span>
+ or <a href="/auth/logout?return_to=/auth/{{ '?return_to=' + return_to | urlencode }}">Cancel</a>
+ <input type="hidden" name="mode" value="{{ mode }}"/>
+ {{ lib.csrf_token() }}
+ </p>
</form>
diff --git a/Allura/allura/templates/mail/authentication_code.txt b/Allura/allura/templates/mail/authentication_code.txt
index 9f4637116..e359b0d77 100644
--- a/Allura/allura/templates/mail/authentication_code.txt
+++ b/Allura/allura/templates/mail/authentication_code.txt
@@ -19,8 +19,10 @@
An attempt to sign-in to your {{ config['site_name'] }} account was detected.
-If this was you, use the following verification code:
+If this was you, click the link below to complete your sign-in:
-{{ email_auth_code }}
+{{ verify_url }}
+
+This link expires in 10 minutes.
If you didn't request it, click <a href="/auth/forgotten_password">here</a> to reset your password immediately
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index e28d4dabd..524d95820 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -2958,12 +2958,13 @@ def test_settings_off(self):
'/auth/preferences/multifactor_disable',
'/auth/preferences/multifactor_recovery',
'/auth/preferences/multifactor_recovery_regen',
- '/auth/multifactor',
'/auth/do_multifactor',
]:
self.app.post(url,
{'password': 'foo', '_csrf_token': self.app.cookies['_csrf_token']},
status=404)
+ self.app.get('/auth/multifactor/', status=404,
+ extra_environ={'username': '*anonymous'},)
def test_user_disabled(self):
r = self.app.get('/auth/preferences/')
@@ -3357,14 +3358,13 @@ class TestEmailAuthCode(TestController):
@mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
@patch('allura.tasks.mail_tasks.send_system_mail_to_user')
- def test_email_auth_code_with_mfa(self, send_system_mail_to_user):
- self.app.get('/auth/preferences/') # establish session_id cookie
+ def test_mfa_no_email_auth(self, send_system_mail_to_user):
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
user = M.User.by_username('test-user')
user.set_pref('multifactor', True)
with patch('allura.lib.plugin.AuthenticationProvider.trusted_login_source', return_value=True):
- extra = {'username': '*anonymous'}
- r = self.app.get('/auth/', extra_environ=extra)
+ r = self.app.get('/auth/')
f = r.forms[0]
encoded = self.app.antispam_field_names(f)
@@ -3376,14 +3376,13 @@ def test_email_auth_code_with_mfa(self, send_system_mail_to_user):
@mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
@patch('allura.tasks.mail_tasks.send_system_mail_to_user')
- def test_email_auth_code_no_mfa(self, send_system_mail_to_user):
- self.app.get('/auth/preferences/') # establish session_id cookie
- self.app.extra_environ = {'username': '*anonymous'}
+ def test_email_auth_code(self, send_system_mail_to_user):
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
user = M.User.by_username('test-user')
user.set_pref('multifactor', False)
with patch('allura.lib.plugin.AuthenticationProvider.trusted_login_source', return_value=False):
- r = self.app.get('/auth/')
+ r = self.app.get('/auth/?return_to=/p/test/')
f = r.forms[0]
encoded = self.app.antispam_field_names(f)
f[encoded['username']] = 'test-user'
@@ -3391,49 +3390,47 @@ def test_email_auth_code_no_mfa(self, send_system_mail_to_user):
r = f.submit(status=302)
r = r.follow()
- # Validate the email with the auth code was sent
+ # Validate that the user is directed to the "check your email" page
+ assert 'Check Your Email' in r.text
+ assert r.request.path == '/auth/login_email_sent'
+
+ user = M.User.by_username('test-user')
+ user_token = user.get_tool_data('AuthEmailCode', 'token')
+ user_expiry_ts = user.get_tool_data('AuthEmailCode', 'token_expiry')
+
+ # Make sure a token was generated
+ assert user_token
+ assert len(user_token) > 10
+
+ # Validate the token is still valid
+ assert int(time_time()) < int(user_expiry_ts)
+
+ # Validate the email with the verification link was sent
args, kwargs = send_system_mail_to_user.call_args
assert r.session.get('mode') == 'email_code'
assert args[1] == f"{config['site_name']} Authentication Code"
assert send_system_mail_to_user.call_count == 1
+ email_text = send_system_mail_to_user.call_args[0][2]
- # Validate that the user is directed to the authentication code page
- assert 'Please enter the 6-digit code sent to your email' in r.text
+ link = [line for line in email_text.splitlines() if '/auth/login_email_verify' in line][0]
+ assert f'token={user_token}' in link
- @mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
- def test_input_correct_auth_code(self):
- self.app.get('/auth/preferences/')
- self.app.extra_environ = {'username': '*anonymous'}
- user = M.User.by_username('test-user')
- user.set_pref('multifactor', False)
+ # Click the verification link and validate the user is logged in and at final return_to URL
+ r = self.app.get(link, status=302)
+ assert r.location == 'http://localhost/p/test/'
+ assert r.session.get('username') == 'test-user'
- with patch('allura.lib.plugin.AuthenticationProvider.trusted_login_source', return_value=False):
- r = self.app.get('/auth/')
- f = r.forms[0]
- encoded = self.app.antispam_field_names(f)
- f[encoded['username']] = 'test-user'
- f[encoded['password']] = 'foo'
- r = f.submit(status=302)
- r = r.follow()
-
- user = M.User.by_username('test-user')
- user_code = user.get_tool_data('AuthEmailCode', 'code')
- user_expiry_ts = user.get_tool_data('AuthEmailCode', 'code_expiry')
- # Make sure a 6-digit code was generated
- assert len(str(user_code)) == 6
-
- # Validate the auth code is still valid
- assert int(time_time()) < int(user_expiry_ts)
-
- # Submit the code and validate the user is logged in
- r.form['code'] = user_code
- r = r.form.submit(status=302)
- assert r.location == 'http://localhost/'
+ @mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
+ def test_input_auth_token_no_session(self):
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
+ r = self.app.get('/auth/login_email_verify?token=badtoken', status=302)
+ r = r.follow()
+ assert 'Your login session was disrupted' in r.text
+ assert not r.session.get('username')
@mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
- def test_input_incorrect_auth_code(self):
- self.app.get('/auth/preferences/')
- self.app.extra_environ = {'username': '*anonymous'}
+ def test_input_incorrect_auth_token(self):
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
user = M.User.by_username('test-user')
user.set_pref('multifactor', False)
@@ -3445,16 +3442,17 @@ def test_input_incorrect_auth_code(self):
f[encoded['password']] = 'foo'
r = f.submit(status=302)
r = r.follow()
+ r.mustcontain('Check Your Email')
- # Submit an incorrect code
- r.form['code'] = '1234567'
- r = r.form.submit()
- assert 'Invalid code' in r.text
+ # Use an incorrect token
+ r = self.app.get('/auth/login_email_verify?token=badtoken', status=302)
+ r = r.follow()
+ assert 'Invalid or expired login link' in r.text
+ assert not r.session.get('username')
@mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
- def test_input_expired_auth_code(self):
- self.app.get('/auth/preferences/')
- self.app.extra_environ = {'username': '*anonymous'}
+ def test_input_expired_auth_token(self):
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
user = M.User.by_username('test-user')
user.set_pref('multifactor', False)
@@ -3466,15 +3464,17 @@ def test_input_expired_auth_code(self):
f[encoded['password']] = 'foo'
r = f.submit(status=302)
r = r.follow()
+ r.mustcontain('Check Your Email')
- # Set the code expiration to 5 minutes ago
+ # Set the token expiration to 15 minutes ago
user = M.User.by_username('test-user')
- user.set_tool_data('AuthEmailCode', code_expiry=int(time_time()) - 300)
- user_code = user.get_tool_data('AuthEmailCode', 'code')
- r.form['code'] = user_code
- r = r.form.submit()
+ user_token = user.get_tool_data('AuthEmailCode', 'token')
+ user.set_tool_data('AuthEmailCode', token_expiry=int(time_time()) - 900)
- assert 'Invalid code' in r.text
+ r = self.app.get(f'/auth/login_email_verify?token={user_token}', status=302)
+ r = r.follow()
+ assert 'Invalid or expired login link' in r.text
+ assert not r.session.get('username')
class TestTrackUserSessions(TestController):