Corp/backend/api/oidc/models.py
Игорь Надеин 86fed54811
All checks were successful
Deploy production / deploy (push) Successful in 10m4s
Add OIDC back-channel logout for deactivated accounts
2026-10-27 20:01:26 +03:00

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"
)
]