Authentication: Support for fallback secret keys in get_session_auth_hash (#4481)

* Authentication: Support for fallback secret keys in get_session_auth_hash

* Update src/pretix/presale/utils.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2024-10-07 16:58:37 +02:00
committed by GitHub
parent cdc5401dc2
commit 6cc9529d9a
4 changed files with 52 additions and 5 deletions

View File

@@ -571,13 +571,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
def get_session_auth_hash(self): def get_session_auth_hash(self):
""" """
Return an HMAC that needs to Return an HMAC that needs to be the same throughout the session, used e.g. for forced
logout after every password change.
"""
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
def get_session_auth_fallback_hash(self):
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
yield self._get_session_auth_hash(secret=fallback_secret)
def _get_session_auth_hash(self, secret):
"""
""" """
key_salt = "pretix.base.models.User.get_session_auth_hash" key_salt = "pretix.base.models.User.get_session_auth_hash"
payload = self.password payload = self.password
payload += self.email payload += self.email
payload += self.session_token payload += self.session_token
return salted_hmac(key_salt, payload).hexdigest() return salted_hmac(key_salt, payload, secret=secret).hexdigest()
def update_session_token(self): def update_session_token(self):
self.session_token = generate_session_token() self.session_token = generate_session_token()

View File

@@ -219,13 +219,24 @@ class Customer(LoggedModel):
return is_password_usable(self.password) return is_password_usable(self.password)
def get_session_auth_hash(self): def get_session_auth_hash(self):
"""
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
logout after every password change.
"""
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
def get_session_auth_fallback_hash(self):
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
yield self._get_session_auth_hash(secret=fallback_secret)
def _get_session_auth_hash(self, secret):
""" """
Return an HMAC of the password field. Return an HMAC of the password field.
""" """
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash" key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
payload = self.password payload = self.password
payload += self.email payload += self.email
return salted_hmac(key_salt, payload).hexdigest() return salted_hmac(key_salt, payload, secret=secret).hexdigest()
def get_email_context(self): def get_email_context(self):
from pretix.base.settings import get_name_parts_localized from pretix.base.settings import get_name_parts_localized

View File

@@ -100,10 +100,23 @@ def get_customer(request):
request._cached_customer = None request._cached_customer = None
else: else:
session_hash = session.get(hash_session_key) session_hash = session.get(hash_session_key)
session_auth_hash = customer.get_session_auth_hash()
session_hash_verified = session_hash and constant_time_compare( session_hash_verified = session_hash and constant_time_compare(
session_hash, session_hash,
customer.get_session_auth_hash() session_auth_hash,
) )
if not session_hash_verified:
# If the current secret does not verify the session, try
# with the fallback secrets and stop when a matching one is
# found.
if session_hash and any(
constant_time_compare(session_hash, fallback_auth_hash)
for fallback_auth_hash in customer.get_session_auth_fallback_hash()
):
request.session.cycle_key()
request.session[hash_session_key] = session_auth_hash
session_hash_verified = True
if session_hash_verified: if session_hash_verified:
request._cached_customer = customer request._cached_customer = customer
else: else:

View File

@@ -628,7 +628,7 @@ def test_change_email(env, client):
@pytest.mark.django_db @pytest.mark.django_db
def test_change_pw(env, client): def test_change_pw(env, client, client2):
with scopes_disabled(): with scopes_disabled():
customer = env[0].customers.create(email='john@example.org', is_verified=True) customer = env[0].customers.create(email='john@example.org', is_verified=True)
customer.set_password('foo') customer.set_password('foo')
@@ -640,6 +640,12 @@ def test_change_pw(env, client):
}) })
assert r.status_code == 302 assert r.status_code == 302
r = client2.post('/bigevents/account/login', {
'email': 'john@example.org',
'password': 'foo',
})
assert r.status_code == 302
r = client.post('/bigevents/account/password', { r = client.post('/bigevents/account/password', {
'password_current': 'invalid', 'password_current': 'invalid',
'password': 'aYLBRNg4', 'password': 'aYLBRNg4',
@@ -658,6 +664,13 @@ def test_change_pw(env, client):
customer.refresh_from_db() customer.refresh_from_db()
assert customer.check_password('aYLBRNg4') assert customer.check_password('aYLBRNg4')
r = client.get('/bigevents/account/password')
assert r.status_code == 200
# Client 2 got logged out
r = client2.post('/bigevents/account/password')
assert r.status_code == 302
@pytest.mark.django_db @pytest.mark.django_db
def test_login_per_org(env, client): def test_login_per_org(env, client):