This is an automated email from the ASF dual-hosted git repository.
kentontaylor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git
The following commit(s) were added to refs/heads/master by this push:
new 5936654b6 [#8572] option to force pwd changes for all, similar to
logic after a HIBP hit
5936654b6 is described below
commit 5936654b69d7a7705c1b7be34d61d9f0b6e8b993
Author: Dave Brondsema <[email protected]>
AuthorDate: Mon Dec 23 13:37:39 2024 -0500
[#8572] option to force pwd changes for all, similar to logic after a HIBP
hit
---
Allura/allura/lib/plugin.py | 64 ++++++++++++++++---------
Allura/allura/templates/forgotten_password.html | 4 +-
Allura/allura/tests/functional/test_auth.py | 2 +-
Allura/development.ini | 6 ++-
4 files changed, 49 insertions(+), 27 deletions(-)
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 6275c20bc..6f48524cb 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -181,13 +181,13 @@ class AuthenticationProvider:
'''
raise NotImplementedError('_login')
- def after_login(self, user, request):
+ def after_login(self, user: M.User, request):
'''
This is a hook so that custom AuthenticationProviders can do things
after a successful login.
'''
pass
- def login(self, user=None, multifactor_success=False):
+ def login(self, user: M.User = None, multifactor_success: bool = False) ->
M.User | None:
from allura import model as M
if user is None:
try:
@@ -210,9 +210,8 @@ class AuthenticationProvider:
if self.is_password_expired(user):
h.auditlog_user('Successful login; Password expired', user=user)
expire_reason = 'via expiration process'
- if not expire_reason and 'password' in self.request.params:
- # password not present with multifactor token; or if login
directly after registering is enabled
- expire_reason = self.login_check_password_change_needed(user,
self.request.params['password'],
+ if not expire_reason:
+ expire_reason = self.login_check_password_change_needed(user,
self.request.params.get('password'),
login_details)
if expire_reason:
self.session['pwd-expired'] = True
@@ -234,30 +233,49 @@ class AuthenticationProvider:
user.track_login(self.request)
return user
- def login_check_password_change_needed(self, user, password,
login_details):
- if not self.hibp_password_check_enabled() \
- or not
asbool(tg.config.get('auth.hibp_failure_force_pwd_change', False)):
- return
-
- try:
- security.HIBPClient.check_breached_password(password)
- except security.HIBPClientError as ex:
- log.error("Error invoking HIBP API", exc_info=ex)
- except security.HIBPCompromisedCredentials:
- trusted = False
+ def login_check_password_change_needed(self, user: M.User, password: str |
None, login_details: M.UserLoginDetails) -> str | None:
+ reason = reason_code = None
+
+ # check setting to force pwd changes after date
+ before = asint(config.get('auth.force_pwd_change_after', 0))
+ if before and self.get_last_password_updated(user) <
datetime.utcfromtimestamp(before):
+ reason = 'requiring a password change'
+ reason_code = 'force_pwd_change'
+
+ # check HIBP
+ if (
+ self.hibp_password_check_enabled()
+ and asbool(tg.config.get('auth.hibp_failure_force_pwd_change',
False))
+ and password # not present with multifactor token; or if login
directly after registering is enabled
+ ):
+ try:
+ security.HIBPClient.check_breached_password(password)
+ except security.HIBPClientError as ex:
+ log.error("Error invoking HIBP API", exc_info=ex)
+ except security.HIBPCompromisedCredentials:
+ reason = 'password in HIBP breach database'
+ reason_code = 'hibp'
+
+ # do it
+ if reason:
+
+ # check if we can trust current login
+ trusted: str | False = False
+ if user.get_pref('multifactor'):
+ trusted = 'multifactor success'
try:
- trusted = self.trusted_login_source(user, login_details)
+ trusted = trusted or self.trusted_login_source(user,
login_details)
except Exception:
log.exception('Error checking if login is trusted: %s %s',
user.username, login_details)
if trusted:
# current user must change password
- h.auditlog_user('Successful login with password in HIBP breach
database, '
- 'from trusted source (reason:
{})'.format(trusted), user=user)
- return 'hibp' # reason
+ h.auditlog_user(f'Successful login but {reason}, '
+ f'from trusted source (reason: {trusted})',
user=user)
+ return reason_code
else:
# current user may not continue, must reset password via email
- h.auditlog_user('Attempted login from untrusted location with
password in HIBP breach database',
+ h.auditlog_user(f'Attempted login from untrusted location with
{reason}',
user=user)
user.send_password_reset_email(subject_tmpl='Update your
{site_name} password')
raise exc.HTTPBadRequest('To ensure account security, you must
reset your password via email.'
@@ -428,7 +446,7 @@ class AuthenticationProvider:
'''
return {}
- def is_password_expired(self, user):
+ def is_password_expired(self, user: M.User) -> bool:
days = asint(config.get('auth.pwdexpire.days', 0))
before = asint(config.get('auth.pwdexpire.before', 0))
now = datetime.utcnow()
@@ -499,7 +517,7 @@ class AuthenticationProvider:
ua=request.headers.get('User-Agent'),
)
- def trusted_login_source(self, user, login_details):
+ def trusted_login_source(self, user, login_details) -> str | False:
# TODO: could also factor in User-Agent but hard to know what parts of
the UA are meaningful to check here
from allura import model as M
for prev_login in M.UserLoginDetails.query.find({'user_id': user._id}):
diff --git a/Allura/allura/templates/forgotten_password.html
b/Allura/allura/templates/forgotten_password.html
index a30eb6d21..6b8703513 100644
--- a/Allura/allura/templates/forgotten_password.html
+++ b/Allura/allura/templates/forgotten_password.html
@@ -19,9 +19,9 @@
{% set hide_left_bar = True %}
{% extends g.theme.master %}
-{% block title %}Forgotten password recovery{% endblock %}
+{% block title %}Password Reset{% endblock %}
-{% block header %}Forgotten password recovery{% endblock %}
+{% block header %}Password Reset{% endblock %}
{% block content %}
<div class="grid-20">
diff --git a/Allura/allura/tests/functional/test_auth.py
b/Allura/allura/tests/functional/test_auth.py
index 4d0180811..7309495c9 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -178,7 +178,7 @@ class TestAuth(TestController):
f[encoded['username']] = 'test-user'
f[encoded['password']] = 'foo'
- with audits(r'Successful login with password in HIBP breach
database, from trusted source '
+ with audits(r'Successful login but password in HIBP breach
database, from trusted source '
r'\(reason: exact ip\)', user=True):
r = f.submit(status=302)
diff --git a/Allura/development.ini b/Allura/development.ini
index 9d64bd910..3336afe83 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -178,8 +178,12 @@ auth.password.algorithm.old = allura_sha256
; password expiration options (disabled if neither is set)
;auth.pwdexpire.days = 1
-; unix timestamp:
+; show a password change form after login, if last pwd change was before this
unix timestamp
;auth.pwdexpire.before = 1401949912
+; require password change, if last pwd change was before this unix timestamp
+; password can only be changed by the logging-in user if they are "trusted"
(e.g. known IP)
+; otherwise they must change via email
+;auth.force_pwd_change_after = 1734978358
; if using LDAP, also run `pip install python-ldap` in your Allura environment