authentik.enterprise.providers.ssf.models
1from datetime import datetime 2from functools import cached_property 3from uuid import uuid4 4 5from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey 6from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey 7from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes 8from django.contrib.postgres.fields import ArrayField 9from django.db import models 10from django.templatetags.static import static 11from django.utils.timezone import now 12from django.utils.translation import gettext_lazy as _ 13from jwt import encode 14 15from authentik.core.models import BackchannelProvider, ExpiringModel, Token 16from authentik.crypto.models import CertificateKeyPair 17from authentik.lib.models import CreatedUpdatedModel, InternallyManagedMixin 18from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator 19from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider 20from authentik.tasks.models import TasksModel 21 22 23class EventTypes(models.TextChoices): 24 """SSF Event types supported by authentik""" 25 26 CAEP_SESSION_REVOKED = "https://schemas.openid.net/secevent/caep/event-type/session-revoked" 27 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.1""" 28 CAEP_TOKEN_CLAIMS_CHANGE = ( 29 "https://schemas.openid.net/secevent/caep/event-type/token-claims-change" 30 ) 31 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.2""" 32 CAEP_CREDENTIAL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/credential-change" 33 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.3""" 34 CAEP_ASSURANCE_LEVEL_CHANGE = ( 35 "https://schemas.openid.net/secevent/caep/event-type/assurance-level-change" 36 ) 37 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.4""" 38 CAEP_DEVICE_COMPLIANCE_CHANGE = ( 39 "https://schemas.openid.net/secevent/caep/event-type/device-compliance-change" 40 ) 41 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.5""" 42 CAEP_SESSION_ESTABLISHED = ( 43 "https://schemas.openid.net/secevent/caep/event-type/session-established" 44 ) 45 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.6""" 46 CAEP_SESSION_PRESENTED = "https://schemas.openid.net/secevent/caep/event-type/session-presented" 47 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.7""" 48 CAEP_RISK_LEVEL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/risk-level-change" 49 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.8""" 50 SET_VERIFICATION = "https://schemas.openid.net/secevent/ssf/event-type/verification" 51 """https://openid.net/specs/openid-sharedsignals-framework-1_0.html#section-8.1.4.1""" 52 53 54class DeliveryMethods(models.TextChoices): 55 """SSF Delivery methods""" 56 57 RISC_PUSH = "https://schemas.openid.net/secevent/risc/delivery-method/push" 58 RISC_POLL = "https://schemas.openid.net/secevent/risc/delivery-method/poll" 59 RFC_PUSH = "urn:ietf:rfc:8935", _("SSF RFC Push") 60 RFC_PULL = "urn:ietf:rfc:8936", _("SSF RFC Pull") 61 62 63class SSFEventStatus(models.TextChoices): 64 """SSF Event status""" 65 66 PENDING_NEW = "pending_new" 67 PENDING_FAILED = "pending_failed" 68 SENT = "sent" 69 70 71class StreamStatus(models.TextChoices): 72 """SSF Stream status""" 73 74 ENABLED = "enabled" 75 PAUSED = "paused" 76 DISABLED = "disabled" 77 DISABLED_DELETED = "disabled_deleted" 78 79 80class SSFProvider(TasksModel, BackchannelProvider): 81 """Shared Signals Framework provider to allow applications to 82 receive user events from authentik.""" 83 84 signing_key = models.ForeignKey( 85 CertificateKeyPair, 86 verbose_name=_("Signing Key"), 87 on_delete=models.CASCADE, 88 help_text=_("Key used to sign the SSF Events."), 89 ) 90 91 push_verify_certificates = models.BooleanField(default=True) 92 93 oidc_auth_providers = models.ManyToManyField(OAuth2Provider, blank=True, default=None) 94 95 token = models.ForeignKey(Token, on_delete=models.CASCADE, null=True, default=None) 96 97 event_retention = models.TextField( 98 default="days=30", 99 validators=[timedelta_string_validator], 100 ) 101 102 @cached_property 103 def jwt_key(self) -> tuple[PrivateKeyTypes, str]: 104 """Get either the configured certificate or the client secret""" 105 key: CertificateKeyPair = self.signing_key 106 private_key = key.private_key 107 if isinstance(private_key, RSAPrivateKey): 108 return private_key, JWTAlgorithms.RS256 109 if isinstance(private_key, EllipticCurvePrivateKey): 110 return private_key, JWTAlgorithms.ES256 111 raise ValueError(f"Invalid private key type: {type(private_key)}") 112 113 @property 114 def service_account_identifier(self) -> str: 115 return f"ak-providers-ssf-{self.pk}" 116 117 @property 118 def serializer(self): 119 from authentik.enterprise.providers.ssf.api.providers import SSFProviderSerializer 120 121 return SSFProviderSerializer 122 123 @property 124 def icon_url(self) -> str | None: 125 return static("authentik/sources/ssf.svg") 126 127 @property 128 def component(self) -> str: 129 return "ak-provider-ssf-form" 130 131 class Meta: 132 verbose_name = _("Shared Signals Framework Provider") 133 verbose_name_plural = _("Shared Signals Framework Providers") 134 permissions = [ 135 # This overrides the default "add_stream" permission of the Stream object, 136 # as the user requesting to add a stream must have the permission on the provider 137 ("add_stream", _("Add stream to SSF provider")), 138 ] 139 140 141class Stream(models.Model): 142 """SSF Stream""" 143 144 uuid = models.UUIDField(default=uuid4, primary_key=True, editable=False) 145 146 status = models.TextField(choices=StreamStatus.choices, default=StreamStatus.ENABLED) 147 148 provider = models.ForeignKey(SSFProvider, on_delete=models.CASCADE) 149 150 delivery_method = models.TextField(choices=DeliveryMethods.choices) 151 endpoint_url = models.TextField(null=True) 152 authorization_header = models.TextField(null=True, default=None) 153 154 events_requested = ArrayField(models.TextField(choices=EventTypes.choices), default=list) 155 format = models.TextField() 156 aud = ArrayField(models.TextField(), default=list) 157 158 iss = models.TextField() 159 160 class Meta: 161 verbose_name = _("SSF Stream") 162 verbose_name_plural = _("SSF Streams") 163 default_permissions = ["change", "delete", "view"] 164 165 def __str__(self) -> str: 166 return "SSF Stream" 167 168 def prepare_event_payload(self, type: EventTypes, event_data: dict, **kwargs) -> dict: 169 jti = uuid4() 170 _now = now() 171 return { 172 "uuid": jti, 173 "stream_id": str(self.pk), 174 "type": type, 175 "expiring": True, 176 "status": SSFEventStatus.PENDING_NEW, 177 "expires": _now + timedelta_from_string(self.provider.event_retention), 178 "payload": { 179 "jti": jti.hex, 180 "aud": self.aud, 181 "iat": int(datetime.now().timestamp()), 182 "iss": self.iss, 183 "events": {type: event_data}, 184 **kwargs, 185 }, 186 } 187 188 def encode(self, data: dict) -> str: 189 headers = {"typ": "secevent+jwt"} 190 if self.provider.signing_key: 191 headers["kid"] = self.provider.signing_key.kid 192 key, alg = self.provider.jwt_key 193 return encode(data, key, algorithm=alg, headers=headers) 194 195 196class StreamEvent(InternallyManagedMixin, CreatedUpdatedModel, ExpiringModel): 197 """Single stream event to be sent""" 198 199 uuid = models.UUIDField(default=uuid4, primary_key=True, editable=False) 200 201 stream = models.ForeignKey(Stream, on_delete=models.CASCADE) 202 status = models.TextField(choices=SSFEventStatus.choices) 203 204 type = models.TextField(choices=EventTypes.choices) 205 payload = models.JSONField(default=dict) 206 207 def expire_action(self, *args, **kwargs): 208 """Only allow automatic cleanup of successfully sent event""" 209 if self.status != SSFEventStatus.SENT: 210 return 211 return super().expire_action(*args, **kwargs) 212 213 def __str__(self): 214 return f"Stream event {self.type}" 215 216 class Meta: 217 verbose_name = _("SSF Stream Event") 218 verbose_name_plural = _("SSF Stream Events") 219 ordering = ("-created",)
24class EventTypes(models.TextChoices): 25 """SSF Event types supported by authentik""" 26 27 CAEP_SESSION_REVOKED = "https://schemas.openid.net/secevent/caep/event-type/session-revoked" 28 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.1""" 29 CAEP_TOKEN_CLAIMS_CHANGE = ( 30 "https://schemas.openid.net/secevent/caep/event-type/token-claims-change" 31 ) 32 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.2""" 33 CAEP_CREDENTIAL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/credential-change" 34 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.3""" 35 CAEP_ASSURANCE_LEVEL_CHANGE = ( 36 "https://schemas.openid.net/secevent/caep/event-type/assurance-level-change" 37 ) 38 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.4""" 39 CAEP_DEVICE_COMPLIANCE_CHANGE = ( 40 "https://schemas.openid.net/secevent/caep/event-type/device-compliance-change" 41 ) 42 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.5""" 43 CAEP_SESSION_ESTABLISHED = ( 44 "https://schemas.openid.net/secevent/caep/event-type/session-established" 45 ) 46 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.6""" 47 CAEP_SESSION_PRESENTED = "https://schemas.openid.net/secevent/caep/event-type/session-presented" 48 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.7""" 49 CAEP_RISK_LEVEL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/risk-level-change" 50 """https://openid.net/specs/openid-caep-1_0-final.html#section-3.8""" 51 SET_VERIFICATION = "https://schemas.openid.net/secevent/ssf/event-type/verification" 52 """https://openid.net/specs/openid-sharedsignals-framework-1_0.html#section-8.1.4.1"""
SSF Event types supported by authentik
55class DeliveryMethods(models.TextChoices): 56 """SSF Delivery methods""" 57 58 RISC_PUSH = "https://schemas.openid.net/secevent/risc/delivery-method/push" 59 RISC_POLL = "https://schemas.openid.net/secevent/risc/delivery-method/poll" 60 RFC_PUSH = "urn:ietf:rfc:8935", _("SSF RFC Push") 61 RFC_PULL = "urn:ietf:rfc:8936", _("SSF RFC Pull")
SSF Delivery methods
64class SSFEventStatus(models.TextChoices): 65 """SSF Event status""" 66 67 PENDING_NEW = "pending_new" 68 PENDING_FAILED = "pending_failed" 69 SENT = "sent"
SSF Event status
72class StreamStatus(models.TextChoices): 73 """SSF Stream status""" 74 75 ENABLED = "enabled" 76 PAUSED = "paused" 77 DISABLED = "disabled" 78 DISABLED_DELETED = "disabled_deleted"
SSF Stream status
81class SSFProvider(TasksModel, BackchannelProvider): 82 """Shared Signals Framework provider to allow applications to 83 receive user events from authentik.""" 84 85 signing_key = models.ForeignKey( 86 CertificateKeyPair, 87 verbose_name=_("Signing Key"), 88 on_delete=models.CASCADE, 89 help_text=_("Key used to sign the SSF Events."), 90 ) 91 92 push_verify_certificates = models.BooleanField(default=True) 93 94 oidc_auth_providers = models.ManyToManyField(OAuth2Provider, blank=True, default=None) 95 96 token = models.ForeignKey(Token, on_delete=models.CASCADE, null=True, default=None) 97 98 event_retention = models.TextField( 99 default="days=30", 100 validators=[timedelta_string_validator], 101 ) 102 103 @cached_property 104 def jwt_key(self) -> tuple[PrivateKeyTypes, str]: 105 """Get either the configured certificate or the client secret""" 106 key: CertificateKeyPair = self.signing_key 107 private_key = key.private_key 108 if isinstance(private_key, RSAPrivateKey): 109 return private_key, JWTAlgorithms.RS256 110 if isinstance(private_key, EllipticCurvePrivateKey): 111 return private_key, JWTAlgorithms.ES256 112 raise ValueError(f"Invalid private key type: {type(private_key)}") 113 114 @property 115 def service_account_identifier(self) -> str: 116 return f"ak-providers-ssf-{self.pk}" 117 118 @property 119 def serializer(self): 120 from authentik.enterprise.providers.ssf.api.providers import SSFProviderSerializer 121 122 return SSFProviderSerializer 123 124 @property 125 def icon_url(self) -> str | None: 126 return static("authentik/sources/ssf.svg") 127 128 @property 129 def component(self) -> str: 130 return "ak-provider-ssf-form" 131 132 class Meta: 133 verbose_name = _("Shared Signals Framework Provider") 134 verbose_name_plural = _("Shared Signals Framework Providers") 135 permissions = [ 136 # This overrides the default "add_stream" permission of the Stream object, 137 # as the user requesting to add a stream must have the permission on the provider 138 ("add_stream", _("Add stream to SSF provider")), 139 ]
Shared Signals Framework provider to allow applications to receive user events from authentik.
Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Child.parent is a ForwardManyToOneDescriptor instance.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example::
class Pizza(Model):
toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings and Topping.pizzas are ManyToManyDescriptor
instances.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Child.parent is a ForwardManyToOneDescriptor instance.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
103 @cached_property 104 def jwt_key(self) -> tuple[PrivateKeyTypes, str]: 105 """Get either the configured certificate or the client secret""" 106 key: CertificateKeyPair = self.signing_key 107 private_key = key.private_key 108 if isinstance(private_key, RSAPrivateKey): 109 return private_key, JWTAlgorithms.RS256 110 if isinstance(private_key, EllipticCurvePrivateKey): 111 return private_key, JWTAlgorithms.ES256 112 raise ValueError(f"Invalid private key type: {type(private_key)}")
Get either the configured certificate or the client secret
118 @property 119 def serializer(self): 120 from authentik.enterprise.providers.ssf.api.providers import SSFProviderSerializer 121 122 return SSFProviderSerializer
Get serializer for this model
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
Accessor to the related object on the forward side of a one-to-one relation.
In the example::
class Restaurant(Model):
place = OneToOneField(Place, related_name='restaurant')
Restaurant.place is a ForwardOneToOneDescriptor instance.
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Parent.children is a ReverseManyToOneDescriptor instance.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
Inherited Members
- authentik.core.models.Provider
- name
- authentication_flow
- invalidation_flow
- property_mappings
- backchannel_application
- is_backchannel
- objects
- launch_url
- authentication_flow_id
- invalidation_flow_id
- backchannel_application_id
- id
- application
- outpost_set
- oauth2provider
- ldapprovider
- racprovider
- radiusprovider
- samlprovider
- scimprovider
- googleworkspaceprovider
- microsoftentraprovider
- ssfprovider
The requested object does not exist
The query returned multiple objects when only one was expected.
142class Stream(models.Model): 143 """SSF Stream""" 144 145 uuid = models.UUIDField(default=uuid4, primary_key=True, editable=False) 146 147 status = models.TextField(choices=StreamStatus.choices, default=StreamStatus.ENABLED) 148 149 provider = models.ForeignKey(SSFProvider, on_delete=models.CASCADE) 150 151 delivery_method = models.TextField(choices=DeliveryMethods.choices) 152 endpoint_url = models.TextField(null=True) 153 authorization_header = models.TextField(null=True, default=None) 154 155 events_requested = ArrayField(models.TextField(choices=EventTypes.choices), default=list) 156 format = models.TextField() 157 aud = ArrayField(models.TextField(), default=list) 158 159 iss = models.TextField() 160 161 class Meta: 162 verbose_name = _("SSF Stream") 163 verbose_name_plural = _("SSF Streams") 164 default_permissions = ["change", "delete", "view"] 165 166 def __str__(self) -> str: 167 return "SSF Stream" 168 169 def prepare_event_payload(self, type: EventTypes, event_data: dict, **kwargs) -> dict: 170 jti = uuid4() 171 _now = now() 172 return { 173 "uuid": jti, 174 "stream_id": str(self.pk), 175 "type": type, 176 "expiring": True, 177 "status": SSFEventStatus.PENDING_NEW, 178 "expires": _now + timedelta_from_string(self.provider.event_retention), 179 "payload": { 180 "jti": jti.hex, 181 "aud": self.aud, 182 "iat": int(datetime.now().timestamp()), 183 "iss": self.iss, 184 "events": {type: event_data}, 185 **kwargs, 186 }, 187 } 188 189 def encode(self, data: dict) -> str: 190 headers = {"typ": "secevent+jwt"} 191 if self.provider.signing_key: 192 headers["kid"] = self.provider.signing_key.kid 193 key, alg = self.provider.jwt_key 194 return encode(data, key, algorithm=alg, headers=headers)
SSF Stream
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Child.parent is a ForwardManyToOneDescriptor instance.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
169 def prepare_event_payload(self, type: EventTypes, event_data: dict, **kwargs) -> dict: 170 jti = uuid4() 171 _now = now() 172 return { 173 "uuid": jti, 174 "stream_id": str(self.pk), 175 "type": type, 176 "expiring": True, 177 "status": SSFEventStatus.PENDING_NEW, 178 "expires": _now + timedelta_from_string(self.provider.event_retention), 179 "payload": { 180 "jti": jti.hex, 181 "aud": self.aud, 182 "iat": int(datetime.now().timestamp()), 183 "iss": self.iss, 184 "events": {type: event_data}, 185 **kwargs, 186 }, 187 }
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Parent.children is a ReverseManyToOneDescriptor instance.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
The requested object does not exist
The query returned multiple objects when only one was expected.
197class StreamEvent(InternallyManagedMixin, CreatedUpdatedModel, ExpiringModel): 198 """Single stream event to be sent""" 199 200 uuid = models.UUIDField(default=uuid4, primary_key=True, editable=False) 201 202 stream = models.ForeignKey(Stream, on_delete=models.CASCADE) 203 status = models.TextField(choices=SSFEventStatus.choices) 204 205 type = models.TextField(choices=EventTypes.choices) 206 payload = models.JSONField(default=dict) 207 208 def expire_action(self, *args, **kwargs): 209 """Only allow automatic cleanup of successfully sent event""" 210 if self.status != SSFEventStatus.SENT: 211 return 212 return super().expire_action(*args, **kwargs) 213 214 def __str__(self): 215 return f"Stream event {self.type}" 216 217 class Meta: 218 verbose_name = _("SSF Stream Event") 219 verbose_name_plural = _("SSF Stream Events") 220 ordering = ("-created",)
Single stream event to be sent
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Child.parent is a ForwardManyToOneDescriptor instance.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
208 def expire_action(self, *args, **kwargs): 209 """Only allow automatic cleanup of successfully sent event""" 210 if self.status != SSFEventStatus.SENT: 211 return 212 return super().expire_action(*args, **kwargs)
Only allow automatic cleanup of successfully sent event
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
The requested object does not exist
The query returned multiple objects when only one was expected.