All checks were successful
Deploy production / deploy (push) Successful in 10m4s
88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
from urllib.parse import urlsplit
|
|
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
|
|
class BackchannelClient(models.Model):
|
|
"""An administrator-approved OIDC client that can receive logout tokens."""
|
|
|
|
application = models.OneToOneField(
|
|
settings.OAUTH2_PROVIDER_APPLICATION_MODEL, on_delete=models.CASCADE
|
|
)
|
|
logout_uri = models.URLField(max_length=2048)
|
|
enabled = models.BooleanField(default=True)
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
url = urlsplit(self.logout_uri)
|
|
if (
|
|
url.scheme != "https"
|
|
or not url.hostname
|
|
or url.username
|
|
or url.password
|
|
or url.fragment
|
|
):
|
|
raise ValidationError(
|
|
{
|
|
"logout_uri": "Use an absolute HTTPS URL without credentials or fragment."
|
|
}
|
|
)
|
|
if self.application_id and self.application.algorithm != "RS256":
|
|
raise ValidationError(
|
|
{
|
|
"application": "Back-channel logout currently supports RS256 OIDC clients."
|
|
}
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.application.name
|
|
|
|
|
|
class OIDCClientIdentity(models.Model):
|
|
"""Remember a subject only after it has authenticated to this OIDC client."""
|
|
|
|
application = models.ForeignKey(
|
|
settings.OAUTH2_PROVIDER_APPLICATION_MODEL, on_delete=models.CASCADE
|
|
)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
|
|
)
|
|
subject = models.CharField(max_length=255)
|
|
last_login = models.DateTimeField(default=timezone.now)
|
|
logout_queued = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["application", "subject"], name="oidc_unique_client_subject"
|
|
)
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.application.name} / {self.subject}"
|
|
|
|
|
|
class BackchannelDelivery(models.Model):
|
|
"""Durable per-client logout queue; it never stores tokens or key material."""
|
|
|
|
identity = models.ForeignKey(OIDCClientIdentity, on_delete=models.CASCADE)
|
|
client = models.ForeignKey(BackchannelClient, on_delete=models.CASCADE)
|
|
state = models.CharField(max_length=12, default="pending")
|
|
created_at = models.DateTimeField(default=timezone.now)
|
|
next_attempt_at = models.DateTimeField(default=timezone.now, db_index=True)
|
|
locked_until = models.DateTimeField(null=True, blank=True)
|
|
lock_token = models.CharField(max_length=36, blank=True, default="")
|
|
attempts = models.PositiveIntegerField(default=0)
|
|
last_error = models.CharField(max_length=80, blank=True, default="")
|
|
completed_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(
|
|
fields=["state", "next_attempt_at"], name="oidc_logout_due_idx"
|
|
)
|
|
]
|