diff --git a/authentik/providers/oauth2/signals.py b/authentik/providers/oauth2/signals.py index 0e69f0ae1c..8efbbff088 100644 --- a/authentik/providers/oauth2/signals.py +++ b/authentik/providers/oauth2/signals.py @@ -1,19 +1,29 @@ from django.contrib.auth.signals import user_logged_out -from django.db.models.signals import post_save +from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.http import HttpRequest -from authentik.core.models import User +from authentik.core.models import AuthenticatedSession, User from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken @receiver(user_logged_out) -def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, **_): - """Revoke access tokens upon user logout""" +def user_logged_out_oauth_tokens_removal(sender, request: HttpRequest, user: User, **_): + """Revoke tokens upon user logout""" if not request.session or not request.session.session_key: return AccessToken.objects.filter( - user=user, session__session__session_key=request.session.session_key + user=user, + session__session__session_key=request.session.session_key, + ).delete() + + +@receiver(pre_delete, sender=AuthenticatedSession) +def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_): + """Revoke tokens upon user logout""" + AccessToken.objects.filter( + user=instance.user, + session__session__session_key=instance.session.session_key, ).delete() @@ -22,6 +32,6 @@ def user_deactivated(sender, instance: User, **_): """Remove user tokens when deactivated""" if instance.is_active: return - AccessToken.objects.filter(session__user=instance).delete() - RefreshToken.objects.filter(session__user=instance).delete() - DeviceToken.objects.filter(session__user=instance).delete() + AccessToken.objects.filter(user=instance).delete() + RefreshToken.objects.filter(user=instance).delete() + DeviceToken.objects.filter(user=instance).delete() diff --git a/authentik/providers/oauth2/tests/test_revoke.py b/authentik/providers/oauth2/tests/test_revoke.py index 93d1d01dec..dd21c19f39 100644 --- a/authentik/providers/oauth2/tests/test_revoke.py +++ b/authentik/providers/oauth2/tests/test_revoke.py @@ -7,12 +7,13 @@ from dataclasses import asdict from django.urls import reverse from django.utils import timezone -from authentik.core.models import Application +from authentik.core.models import Application, AuthenticatedSession, Session from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.lib.generators import generate_id from authentik.providers.oauth2.models import ( AccessToken, ClientTypes, + DeviceToken, IDToken, OAuth2Provider, RedirectURI, @@ -20,6 +21,7 @@ from authentik.providers.oauth2.models import ( RefreshToken, ) from authentik.providers.oauth2.tests.utils import OAuthTestCase +from authentik.root.middleware import ClientIPMiddleware class TesOAuth2Revoke(OAuthTestCase): @@ -135,3 +137,86 @@ class TesOAuth2Revoke(OAuthTestCase): }, ) self.assertEqual(res.status_code, 200) + + def test_revoke_logout(self): + """Test revoke on logout""" + self.client.force_login(self.user) + AccessToken.objects.create( + provider=self.provider, + user=self.user, + session=self.client.session["authenticatedsession"], + token=generate_id(), + auth_time=timezone.now(), + _scope="openid user profile", + _id_token=json.dumps( + asdict( + IDToken("foo", "bar"), + ) + ), + ) + self.client.logout() + self.assertEqual(AccessToken.objects.all().count(), 0) + + def test_revoke_session_delete(self): + """Test revoke on logout""" + session = AuthenticatedSession.objects.create( + session=Session.objects.create( + session_key=generate_id(), + last_ip=ClientIPMiddleware.default_ip, + ), + user=self.user, + ) + AccessToken.objects.create( + provider=self.provider, + user=self.user, + session=session, + token=generate_id(), + auth_time=timezone.now(), + _scope="openid user profile", + _id_token=json.dumps( + asdict( + IDToken("foo", "bar"), + ) + ), + ) + session.delete() + self.assertEqual(AccessToken.objects.all().count(), 0) + + def test_revoke_user_deactivated(self): + """Test revoke on logout""" + AccessToken.objects.create( + provider=self.provider, + user=self.user, + token=generate_id(), + auth_time=timezone.now(), + _scope="openid user profile", + _id_token=json.dumps( + asdict( + IDToken("foo", "bar"), + ) + ), + ) + RefreshToken.objects.create( + provider=self.provider, + user=self.user, + token=generate_id(), + auth_time=timezone.now(), + _scope="openid user profile", + _id_token=json.dumps( + asdict( + IDToken("foo", "bar"), + ) + ), + ) + DeviceToken.objects.create( + provider=self.provider, + user=self.user, + _scope="openid user profile", + ) + + self.user.is_active = False + self.user.save() + + self.assertEqual(AccessToken.objects.all().count(), 0) + self.assertEqual(RefreshToken.objects.all().count(), 0) + self.assertEqual(DeviceToken.objects.all().count(), 0) diff --git a/authentik/providers/rac/signals.py b/authentik/providers/rac/signals.py index d4e4a3877b..799f2a08a5 100644 --- a/authentik/providers/rac/signals.py +++ b/authentik/providers/rac/signals.py @@ -8,7 +8,7 @@ from django.db.models.signals import post_delete, post_save, pre_delete from django.dispatch import receiver from django.http import HttpRequest -from authentik.core.models import User +from authentik.core.models import AuthenticatedSession, User from authentik.providers.rac.api.endpoints import user_endpoint_cache_key from authentik.providers.rac.consumer_client import ( RAC_CLIENT_GROUP_SESSION, @@ -32,6 +32,18 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_): ) +@receiver(pre_delete, sender=AuthenticatedSession) +def user_session_deleted(sender, instance: AuthenticatedSession, **_): + layer = get_channel_layer() + async_to_sync(layer.group_send)( + RAC_CLIENT_GROUP_SESSION + % { + "session": instance.session.session_key, + }, + {"type": "event.disconnect", "reason": "session_logout"}, + ) + + @receiver(pre_delete, sender=ConnectionToken) def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **_): """Disconnect session when connection token is deleted"""