diff --git a/data/model/user.py b/data/model/user.py index 6e8265f69..134e48f8c 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -104,8 +104,7 @@ def change_password(user, new_password): pw_hash = hash_password(new_password) user.invalid_login_attempts = 0 user.password_hash = pw_hash - user.uuid = str(uuid4()) - user.save() + invalidate_all_sessions(user) # Remove any password required notifications for the user. notification.delete_notifications_by_kind(user, 'password_required') @@ -593,6 +592,13 @@ def get_user_or_org_by_customer_id(customer_id): except User.DoesNotExist: return None +def invalidate_all_sessions(user): + """ Invalidates all existing user sessions by rotating the user's UUID. """ + if not user: + return + + user.uuid = str(uuid4()) + user.save() def get_matching_user_namespaces(namespace_prefix, username, limit=10): namespace_search = prefix_search(Namespace.username, namespace_prefix) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 581f56b40..1443f4449 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -725,6 +725,7 @@ class Signout(ApiResource): @nickname('logout') def post(self): """ Request that the current user be signed out. """ + model.user.invalidate_all_sessions(get_authenticated_user()) logout_user() identity_changed.send(app, identity=AnonymousIdentity()) return {'success': True} diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 6fea04ddd..1e3476d04 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -37,7 +37,7 @@ {{ user.username }} - Sign out + Sign out all sessions
  • Sign in @@ -129,7 +129,7 @@
  • Super User Admin Panel
  • -
  • Sign out
  • +
  • Sign out all sessions
  • diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 5f9a22f15..089d36f30 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -895,6 +895,7 @@ class TestSignout(ApiTestCase): def test_signout(self): self.login(READ_ACCESS_USER) + read_user = model.user.get_user(READ_ACCESS_USER) json = self.getJsonResponse(User) assert json['username'] == READ_ACCESS_USER @@ -903,6 +904,10 @@ class TestSignout(ApiTestCase): # Make sure we're now signed out. self.getJsonResponse(User, expected_code=401) + # Make sure the user's UUID has rotated, to ensure sessions are no longer valid. + read_user_again = model.user.get_user(READ_ACCESS_USER) + self.assertNotEquals(read_user.uuid, read_user_again.uuid) + class TestConductSearch(ApiTestCase): def test_noaccess(self):