diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index d5f7a51cd..3dbe4874f 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -571,13 +571,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): 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" payload = self.password payload += self.email 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): self.session_token = generate_session_token() diff --git a/src/pretix/base/models/customers.py b/src/pretix/base/models/customers.py index 6904fe569..b93acdebb 100644 --- a/src/pretix/base/models/customers.py +++ b/src/pretix/base/models/customers.py @@ -219,13 +219,24 @@ class Customer(LoggedModel): return is_password_usable(self.password) 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. """ key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash" payload = self.password payload += self.email - return salted_hmac(key_salt, payload).hexdigest() + return salted_hmac(key_salt, payload, secret=secret).hexdigest() def get_email_context(self): from pretix.base.settings import get_name_parts_localized diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py index b933cc39c..bc21ee2ed 100644 --- a/src/pretix/presale/utils.py +++ b/src/pretix/presale/utils.py @@ -100,10 +100,23 @@ def get_customer(request): request._cached_customer = None else: 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, - 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: request._cached_customer = customer else: diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py index 085488d11..83084d0e2 100644 --- a/src/tests/presale/test_customer.py +++ b/src/tests/presale/test_customer.py @@ -628,7 +628,7 @@ def test_change_email(env, client): @pytest.mark.django_db -def test_change_pw(env, client): +def test_change_pw(env, client, client2): with scopes_disabled(): customer = env[0].customers.create(email='john@example.org', is_verified=True) customer.set_password('foo') @@ -640,6 +640,12 @@ def test_change_pw(env, client): }) 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', { 'password_current': 'invalid', 'password': 'aYLBRNg4', @@ -658,6 +664,13 @@ def test_change_pw(env, client): customer.refresh_from_db() 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 def test_login_per_org(env, client):