authentik.enterprise.providers.ssf.signals

  1from hashlib import sha256
  2
  3from django.db.models import Model
  4from django.db.models.signals import post_delete, post_save, pre_delete
  5from django.dispatch import receiver
  6
  7from authentik.core.models import (
  8    USER_PATH_SYSTEM_PREFIX,
  9    AuthenticatedSession,
 10    Token,
 11    TokenIntents,
 12    User,
 13    UserTypes,
 14)
 15from authentik.core.signals import password_changed, password_hash_changed
 16from authentik.enterprise.providers.ssf.models import (
 17    EventTypes,
 18    SSFProvider,
 19)
 20from authentik.enterprise.providers.ssf.tasks import send_ssf_events
 21from authentik.events.middleware import audit_ignore
 22from authentik.stages.authenticator.models import Device
 23from authentik.stages.authenticator_duo.models import DuoDevice
 24from authentik.stages.authenticator_static.models import StaticDevice
 25from authentik.stages.authenticator_totp.models import TOTPDevice
 26from authentik.stages.authenticator_webauthn.models import (
 27    UNKNOWN_DEVICE_TYPE_AAGUID,
 28    WebAuthnDevice,
 29)
 30
 31USER_PATH_PROVIDERS_SSF = USER_PATH_SYSTEM_PREFIX + "/providers/ssf"
 32
 33
 34@receiver(post_save, sender=SSFProvider)
 35def ssf_providers_post_save(sender: type[Model], instance: SSFProvider, created: bool, **_):
 36    """Create service account before provider is saved"""
 37    identifier = instance.service_account_identifier
 38    user, _ = User.objects.update_or_create(
 39        username=identifier,
 40        defaults={
 41            "name": f"SSF Provider {instance.name} Service-Account",
 42            "type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
 43            "path": USER_PATH_PROVIDERS_SSF,
 44        },
 45    )
 46    user.assign_perms_to_managed_role("authentik_providers_ssf.add_stream", instance)
 47    token, token_created = Token.objects.update_or_create(
 48        identifier=identifier,
 49        defaults={
 50            "user": user,
 51            "intent": TokenIntents.INTENT_API,
 52            "expiring": False,
 53            "managed": f"goauthentik.io/providers/ssf/{instance.pk}",
 54        },
 55    )
 56    if created or token_created:
 57        with audit_ignore():
 58            instance.token = token
 59            instance.save()
 60
 61
 62@receiver(pre_delete, sender=AuthenticatedSession)
 63def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_):
 64    """Session revoked trigger (users' session has been deleted)
 65
 66    As this signal is also triggered with a regular logout, we can't be sure
 67    if the session has been deleted by an admin or by the user themselves."""
 68    send_ssf_events(
 69        EventTypes.CAEP_SESSION_REVOKED,
 70        {
 71            "initiating_entity": "user",
 72        },
 73        sub_id={
 74            "format": "complex",
 75            "session": {
 76                "format": "opaque",
 77                "id": sha256(instance.session.session_key.encode("ascii")).hexdigest(),
 78            },
 79            "user": {
 80                "format": "email",
 81                "email": instance.user.email,
 82            },
 83        },
 84    )
 85
 86
 87def _send_password_credential_change(user: User, change_type: str):
 88    """Credential change trigger (password changed)"""
 89    send_ssf_events(
 90        EventTypes.CAEP_CREDENTIAL_CHANGE,
 91        {
 92            "credential_type": "password",
 93            "change_type": change_type,
 94        },
 95        sub_id={
 96            "format": "complex",
 97            "user": {
 98                "format": "email",
 99                "email": user.email,
100            },
101        },
102    )
103
104
105@receiver(password_hash_changed)
106@receiver(password_changed)
107def ssf_password_changed_cred_change(signal, sender, user: User, password: str | None = None, **_):
108    """Credential change trigger (password changed)"""
109    if signal is password_hash_changed:
110        _send_password_credential_change(user, "update")
111        return
112    _send_password_credential_change(user, "revoke" if password is None else "update")
113
114
115device_type_map = {
116    StaticDevice: "pin",
117    TOTPDevice: "pin",
118    WebAuthnDevice: "fido-u2f",
119    DuoDevice: "app",
120}
121
122
123@receiver(post_save)
124def ssf_device_post_save(sender: type[Model], instance: Device, created: bool, **_):
125    if not isinstance(instance, Device):
126        return
127    if not instance.confirmed:
128        return
129    device_type = device_type_map.get(instance.__class__)
130    data = {
131        "credential_type": device_type,
132        "change_type": "create" if created else "update",
133        "friendly_name": instance.name,
134    }
135    if isinstance(instance, WebAuthnDevice) and instance.aaguid != UNKNOWN_DEVICE_TYPE_AAGUID:
136        data["fido2_aaguid"] = instance.aaguid
137    send_ssf_events(
138        EventTypes.CAEP_CREDENTIAL_CHANGE,
139        data,
140        sub_id={
141            "format": "complex",
142            "user": {
143                "format": "email",
144                "email": instance.user.email,
145            },
146        },
147    )
148
149
150@receiver(post_delete)
151def ssf_device_post_delete(sender: type[Model], instance: Device, **_):
152    if not isinstance(instance, Device):
153        return
154    if not instance.confirmed:
155        return
156    device_type = device_type_map.get(instance.__class__)
157    data = {
158        "credential_type": device_type,
159        "change_type": "delete",
160        "friendly_name": instance.name,
161    }
162    if isinstance(instance, WebAuthnDevice) and instance.aaguid != UNKNOWN_DEVICE_TYPE_AAGUID:
163        data["fido2_aaguid"] = instance.aaguid
164    send_ssf_events(
165        EventTypes.CAEP_CREDENTIAL_CHANGE,
166        data,
167        sub_id={
168            "format": "complex",
169            "user": {
170                "format": "email",
171                "email": instance.user.email,
172            },
173        },
174    )
USER_PATH_PROVIDERS_SSF = 'goauthentik.io/providers/ssf'
@receiver(post_save, sender=SSFProvider)
def ssf_providers_post_save( sender: type[django.db.models.base.Model], instance: authentik.enterprise.providers.ssf.models.SSFProvider, created: bool, **_):
35@receiver(post_save, sender=SSFProvider)
36def ssf_providers_post_save(sender: type[Model], instance: SSFProvider, created: bool, **_):
37    """Create service account before provider is saved"""
38    identifier = instance.service_account_identifier
39    user, _ = User.objects.update_or_create(
40        username=identifier,
41        defaults={
42            "name": f"SSF Provider {instance.name} Service-Account",
43            "type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
44            "path": USER_PATH_PROVIDERS_SSF,
45        },
46    )
47    user.assign_perms_to_managed_role("authentik_providers_ssf.add_stream", instance)
48    token, token_created = Token.objects.update_or_create(
49        identifier=identifier,
50        defaults={
51            "user": user,
52            "intent": TokenIntents.INTENT_API,
53            "expiring": False,
54            "managed": f"goauthentik.io/providers/ssf/{instance.pk}",
55        },
56    )
57    if created or token_created:
58        with audit_ignore():
59            instance.token = token
60            instance.save()

Create service account before provider is saved

@receiver(pre_delete, sender=AuthenticatedSession)
def ssf_user_session_delete_session_revoked(sender, instance: authentik.core.models.AuthenticatedSession, **_):
63@receiver(pre_delete, sender=AuthenticatedSession)
64def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_):
65    """Session revoked trigger (users' session has been deleted)
66
67    As this signal is also triggered with a regular logout, we can't be sure
68    if the session has been deleted by an admin or by the user themselves."""
69    send_ssf_events(
70        EventTypes.CAEP_SESSION_REVOKED,
71        {
72            "initiating_entity": "user",
73        },
74        sub_id={
75            "format": "complex",
76            "session": {
77                "format": "opaque",
78                "id": sha256(instance.session.session_key.encode("ascii")).hexdigest(),
79            },
80            "user": {
81                "format": "email",
82                "email": instance.user.email,
83            },
84        },
85    )

Session revoked trigger (users' session has been deleted)

As this signal is also triggered with a regular logout, we can't be sure if the session has been deleted by an admin or by the user themselves.

@receiver(password_hash_changed)
@receiver(password_changed)
def ssf_password_changed_cred_change( signal, sender, user: authentik.core.models.User, password: str | None = None, **_):
106@receiver(password_hash_changed)
107@receiver(password_changed)
108def ssf_password_changed_cred_change(signal, sender, user: User, password: str | None = None, **_):
109    """Credential change trigger (password changed)"""
110    if signal is password_hash_changed:
111        _send_password_credential_change(user, "update")
112        return
113    _send_password_credential_change(user, "revoke" if password is None else "update")

Credential change trigger (password changed)

@receiver(post_save)
def ssf_device_post_save( sender: type[django.db.models.base.Model], instance: authentik.stages.authenticator.models.Device, created: bool, **_):
124@receiver(post_save)
125def ssf_device_post_save(sender: type[Model], instance: Device, created: bool, **_):
126    if not isinstance(instance, Device):
127        return
128    if not instance.confirmed:
129        return
130    device_type = device_type_map.get(instance.__class__)
131    data = {
132        "credential_type": device_type,
133        "change_type": "create" if created else "update",
134        "friendly_name": instance.name,
135    }
136    if isinstance(instance, WebAuthnDevice) and instance.aaguid != UNKNOWN_DEVICE_TYPE_AAGUID:
137        data["fido2_aaguid"] = instance.aaguid
138    send_ssf_events(
139        EventTypes.CAEP_CREDENTIAL_CHANGE,
140        data,
141        sub_id={
142            "format": "complex",
143            "user": {
144                "format": "email",
145                "email": instance.user.email,
146            },
147        },
148    )
@receiver(post_delete)
def ssf_device_post_delete( sender: type[django.db.models.base.Model], instance: authentik.stages.authenticator.models.Device, **_):
151@receiver(post_delete)
152def ssf_device_post_delete(sender: type[Model], instance: Device, **_):
153    if not isinstance(instance, Device):
154        return
155    if not instance.confirmed:
156        return
157    device_type = device_type_map.get(instance.__class__)
158    data = {
159        "credential_type": device_type,
160        "change_type": "delete",
161        "friendly_name": instance.name,
162    }
163    if isinstance(instance, WebAuthnDevice) and instance.aaguid != UNKNOWN_DEVICE_TYPE_AAGUID:
164        data["fido2_aaguid"] = instance.aaguid
165    send_ssf_events(
166        EventTypes.CAEP_CREDENTIAL_CHANGE,
167        data,
168        sub_id={
169            "format": "complex",
170            "user": {
171                "format": "email",
172                "email": instance.user.email,
173            },
174        },
175    )