Compare commits
7 Commits
enterprise
...
website/do
Author | SHA1 | Date | |
---|---|---|---|
778bd05289 | |||
bb4e3bcf42 | |||
43cedfb463 | |||
315680458f | |||
c85471575a | |||
5d00dc7e9e | |||
6982e7d1c9 |
@ -43,7 +43,6 @@ from authentik.core.models import (
|
||||
)
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import LicenseUsage
|
||||
from authentik.enterprise.providers.apple_psso.models import AppleNonce
|
||||
from authentik.enterprise.providers.google_workspace.models import (
|
||||
GoogleWorkspaceProviderGroup,
|
||||
GoogleWorkspaceProviderUser,
|
||||
@ -136,7 +135,6 @@ def excluded_models() -> list[type[Model]]:
|
||||
EndpointDeviceConnection,
|
||||
DeviceToken,
|
||||
StreamEvent,
|
||||
AppleNonce,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,32 +0,0 @@
|
||||
"""Apple Platform SSO Provider API Views"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.providers.apple_psso.models import ApplePlatformSSOProvider
|
||||
|
||||
|
||||
class ApplePlatformSSOProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
||||
"""ApplePlatformSSOProvider Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = ApplePlatformSSOProvider
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
|
||||
|
||||
class ApplePlatformSSOProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"""ApplePlatformSSOProvider Viewset"""
|
||||
|
||||
queryset = ApplePlatformSSOProvider.objects.all()
|
||||
serializer_class = ApplePlatformSSOProviderSerializer
|
||||
filterset_fields = [
|
||||
"name",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
@ -1,13 +0,0 @@
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEnterpriseProviderApplePSSOConfig(EnterpriseConfig):
|
||||
|
||||
name = "authentik.enterprise.providers.apple_psso"
|
||||
label = "authentik_providers_apple_psso"
|
||||
verbose_name = "authentik Enterprise.Providers.Apple Platform SSO"
|
||||
default = True
|
||||
mountpoints = {
|
||||
"authentik.enterprise.providers.apple_psso.urls": "endpoint/apple/sso/",
|
||||
"authentik.enterprise.providers.apple_psso.urls_root": "",
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
from base64 import urlsafe_b64encode
|
||||
from json import dumps
|
||||
from secrets import token_bytes
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
|
||||
from django.http import HttpResponse
|
||||
from jwcrypto.common import base64url_decode, base64url_encode
|
||||
|
||||
from authentik.enterprise.providers.apple_psso.models import AppleDevice
|
||||
|
||||
|
||||
def length_prefixed(data: bytes) -> bytes:
|
||||
length = len(data)
|
||||
return length.to_bytes(4, "big") + data
|
||||
|
||||
|
||||
def build_apu(public_key: ec.EllipticCurvePublicKey):
|
||||
# X9.63 representation: 0x04 || X || Y
|
||||
public_numbers = public_key.public_numbers()
|
||||
|
||||
x_bytes = public_numbers.x.to_bytes(32, "big")
|
||||
y_bytes = public_numbers.y.to_bytes(32, "big")
|
||||
|
||||
x963 = bytes([0x04]) + x_bytes + y_bytes
|
||||
|
||||
result = length_prefixed(b"APPLE") + length_prefixed(x963)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def encrypt_token_with_a256_gcm(body: dict, device_encryption_key: str, apv: bytes) -> str:
|
||||
ephemeral_key = ec.generate_private_key(curve=ec.SECP256R1())
|
||||
device_public_key = serialization.load_pem_public_key(
|
||||
device_encryption_key.encode(), backend=default_backend()
|
||||
)
|
||||
|
||||
shared_secret_z = ephemeral_key.exchange(ec.ECDH(), device_public_key)
|
||||
|
||||
apu = build_apu(ephemeral_key.public_key())
|
||||
|
||||
jwe_header = {
|
||||
"enc": "A256GCM",
|
||||
"kid": "ephemeralKey",
|
||||
"epk": {
|
||||
"x": base64url_encode(
|
||||
ephemeral_key.public_key().public_numbers().x.to_bytes(32, "big")
|
||||
),
|
||||
"y": base64url_encode(
|
||||
ephemeral_key.public_key().public_numbers().y.to_bytes(32, "big")
|
||||
),
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
},
|
||||
"typ": "platformsso-login-response+jwt",
|
||||
"alg": "ECDH-ES",
|
||||
"apu": base64url_encode(apu),
|
||||
"apv": base64url_encode(apv),
|
||||
}
|
||||
|
||||
party_u_info = length_prefixed(apu)
|
||||
party_v_info = length_prefixed(apv)
|
||||
supp_pub_info = (256).to_bytes(4, "big")
|
||||
|
||||
other_info = length_prefixed(b"A256GCM") + party_u_info + party_v_info + supp_pub_info
|
||||
|
||||
ckdf = ConcatKDFHash(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
otherinfo=other_info,
|
||||
)
|
||||
|
||||
derived_key = ckdf.derive(shared_secret_z)
|
||||
|
||||
nonce = token_bytes(12)
|
||||
|
||||
header_json = dumps(jwe_header, separators=(",", ":")).encode()
|
||||
aad = urlsafe_b64encode(header_json).rstrip(b"=")
|
||||
|
||||
aesgcm = AESGCM(derived_key)
|
||||
ciphertext = aesgcm.encrypt(nonce, dumps(body).encode(), aad)
|
||||
|
||||
ciphertext_body = ciphertext[:-16]
|
||||
tag = ciphertext[-16:]
|
||||
|
||||
# base64url encoding
|
||||
protected_b64 = urlsafe_b64encode(header_json).rstrip(b"=")
|
||||
iv_b64 = urlsafe_b64encode(nonce).rstrip(b"=")
|
||||
ciphertext_b64 = urlsafe_b64encode(ciphertext_body).rstrip(b"=")
|
||||
tag_b64 = urlsafe_b64encode(tag).rstrip(b"=")
|
||||
|
||||
jwe_compact = b".".join(
|
||||
[
|
||||
protected_b64,
|
||||
b"",
|
||||
iv_b64,
|
||||
ciphertext_b64,
|
||||
tag_b64,
|
||||
]
|
||||
)
|
||||
return jwe_compact.decode()
|
||||
|
||||
|
||||
class JWEResponse(HttpResponse):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: dict,
|
||||
device: AppleDevice,
|
||||
apv: str,
|
||||
):
|
||||
super().__init__(
|
||||
content=encrypt_token_with_a256_gcm(data, device.encryption_key, base64url_decode(apv)),
|
||||
content_type="application/platformsso-login-response+jwt",
|
||||
)
|
@ -1,36 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-06-28 00:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0028_migrate_session"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ApplePlatformSSOProvider",
|
||||
fields=[
|
||||
(
|
||||
"oauth2provider_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_providers_oauth2.oauth2provider",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("authentik_providers_oauth2.oauth2provider",),
|
||||
),
|
||||
]
|
@ -1,94 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-06-28 15:50
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_apple_psso", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AppleDevice",
|
||||
fields=[
|
||||
(
|
||||
"endpoint_uuid",
|
||||
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
("signing_key", models.TextField()),
|
||||
("encryption_key", models.TextField()),
|
||||
("key_exchange_key", models.TextField()),
|
||||
("sign_key_id", models.TextField()),
|
||||
("enc_key_id", models.TextField()),
|
||||
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"provider",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_providers_apple_psso.appleplatformssoprovider",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AppleDeviceUser",
|
||||
fields=[
|
||||
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
("signing_key", models.TextField()),
|
||||
("encryption_key", models.TextField()),
|
||||
("sign_key_id", models.TextField()),
|
||||
("enc_key_id", models.TextField()),
|
||||
(
|
||||
"device",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_providers_apple_psso.appledevice",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="appledevice",
|
||||
name="users",
|
||||
field=models.ManyToManyField(
|
||||
through="authentik_providers_apple_psso.AppleDeviceUser",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AppleNonce",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("expires", models.DateTimeField(default=None, null=True)),
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
("nonce", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
"indexes": [
|
||||
models.Index(fields=["expires"], name="authentik_p_expires_47d534_idx"),
|
||||
models.Index(fields=["expiring"], name="authentik_p_expirin_87253e_idx"),
|
||||
models.Index(
|
||||
fields=["expiring", "expires"], name="authentik_p_expirin_20a7c9_idx"
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
@ -1,34 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-06-28 22:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_providers_apple_psso",
|
||||
"0002_appledevice_appledeviceuser_appledevice_users_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="appledeviceuser",
|
||||
old_name="sign_key_id",
|
||||
new_name="enclave_key_id",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="appledeviceuser",
|
||||
old_name="signing_key",
|
||||
new_name="secure_enclave_key",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="appledeviceuser",
|
||||
name="enc_key_id",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="appledeviceuser",
|
||||
name="encryption_key",
|
||||
),
|
||||
]
|
@ -1,85 +0,0 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import ExpiringModel, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.providers.oauth2.models import (
|
||||
ClientTypes,
|
||||
IssuerMode,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
|
||||
|
||||
class ApplePlatformSSOProvider(OAuth2Provider):
|
||||
"""Integrate with Apple Platform SSO"""
|
||||
|
||||
def set_oauth_defaults(self):
|
||||
"""Ensure all OAuth2-related settings are correct"""
|
||||
self.issuer_mode = IssuerMode.PER_PROVIDER
|
||||
self.client_type = ClientTypes.PUBLIC
|
||||
self.signing_key = CertificateKeyPair.objects.get(name="authentik Self-signed Certificate")
|
||||
self.include_claims_in_id_token = True
|
||||
scopes = ScopeMapping.objects.filter(
|
||||
managed__in=[
|
||||
"goauthentik.io/providers/oauth2/scope-openid",
|
||||
"goauthentik.io/providers/oauth2/scope-profile",
|
||||
"goauthentik.io/providers/oauth2/scope-email",
|
||||
"goauthentik.io/providers/oauth2/scope-offline_access",
|
||||
"goauthentik.io/providers/oauth2/scope-authentik_api",
|
||||
]
|
||||
)
|
||||
self.property_mappings.add(*list(scopes))
|
||||
self.redirect_uris = [
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "io.goauthentik.endpoint:/oauth2redirect"),
|
||||
]
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-provider-apple-psso-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.enterprise.providers.apple_psso.api.providers import (
|
||||
ApplePlatformSSOProviderSerializer,
|
||||
)
|
||||
|
||||
return ApplePlatformSSOProviderSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Apple Platform SSO Provider")
|
||||
verbose_name_plural = _("Apple Platform SSO Providers")
|
||||
|
||||
|
||||
class AppleDevice(models.Model):
|
||||
|
||||
endpoint_uuid = models.UUIDField(default=uuid4, primary_key=True)
|
||||
|
||||
signing_key = models.TextField()
|
||||
encryption_key = models.TextField()
|
||||
key_exchange_key = models.TextField()
|
||||
sign_key_id = models.TextField()
|
||||
enc_key_id = models.TextField()
|
||||
creation_time = models.DateTimeField(auto_now_add=True)
|
||||
provider = models.ForeignKey(ApplePlatformSSOProvider, on_delete=models.CASCADE)
|
||||
users = models.ManyToManyField(User, through="AppleDeviceUser")
|
||||
|
||||
|
||||
class AppleDeviceUser(models.Model):
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, primary_key=True)
|
||||
|
||||
device = models.ForeignKey(AppleDevice, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
secure_enclave_key = models.TextField()
|
||||
enclave_key_id = models.TextField()
|
||||
|
||||
|
||||
class AppleNonce(ExpiringModel):
|
||||
nonce = models.TextField()
|
@ -1,15 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.providers.apple_psso.views.nonce import NonceView
|
||||
from authentik.enterprise.providers.apple_psso.views.register import (
|
||||
RegisterDeviceView,
|
||||
RegisterUserView,
|
||||
)
|
||||
from authentik.enterprise.providers.apple_psso.views.token import TokenView
|
||||
|
||||
urlpatterns = [
|
||||
path("token/", TokenView.as_view(), name="token"),
|
||||
path("nonce/", NonceView.as_view(), name="nonce"),
|
||||
path("register/device/", RegisterDeviceView.as_view(), name="register-device"),
|
||||
path("register/user/", RegisterUserView.as_view(), name="register-user"),
|
||||
]
|
@ -1,7 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.providers.apple_psso.views.site_association import AppleAppSiteAssociation
|
||||
|
||||
urlpatterns = [
|
||||
path(".well-known/apple-app-site-association", AppleAppSiteAssociation.as_view(), name="asa"),
|
||||
]
|
@ -1,25 +0,0 @@
|
||||
from base64 import b64encode
|
||||
from datetime import timedelta
|
||||
from secrets import token_bytes
|
||||
|
||||
from django.http import HttpRequest, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from authentik.enterprise.providers.apple_psso.models import AppleNonce
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NonceView(View):
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs):
|
||||
nonce = AppleNonce.objects.create(
|
||||
nonce=b64encode(token_bytes(32)).decode(), expires=now() + timedelta(minutes=5)
|
||||
)
|
||||
return JsonResponse(
|
||||
{
|
||||
"Nonce": nonce.nonce,
|
||||
}
|
||||
)
|
@ -1,92 +0,0 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.providers.apple_psso.models import (
|
||||
AppleDevice,
|
||||
AppleDeviceUser,
|
||||
ApplePlatformSSOProvider,
|
||||
)
|
||||
from authentik.lib.generators import generate_key
|
||||
|
||||
|
||||
class DeviceRegisterAuth(BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
# very temporary, lol
|
||||
return (User(), None)
|
||||
|
||||
|
||||
class RegisterDeviceView(APIView):
|
||||
|
||||
class DeviceRegistration(PassiveSerializer):
|
||||
|
||||
device_uuid = CharField()
|
||||
client_id = CharField()
|
||||
device_signing_key = CharField()
|
||||
device_encryption_key = CharField()
|
||||
sign_key_id = CharField()
|
||||
enc_key_id = CharField()
|
||||
|
||||
permission_classes = []
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
serializer_class = DeviceRegistration
|
||||
authentication_classes = [DeviceRegisterAuth, TokenAuthentication]
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
data = self.DeviceRegistration(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
provider = get_object_or_404(
|
||||
ApplePlatformSSOProvider, client_id=data.validated_data["client_id"]
|
||||
)
|
||||
AppleDevice.objects.update_or_create(
|
||||
endpoint_uuid=data.validated_data["device_uuid"],
|
||||
defaults={
|
||||
"signing_key": data.validated_data["device_signing_key"],
|
||||
"encryption_key": data.validated_data["device_encryption_key"],
|
||||
"sign_key_id": data.validated_data["sign_key_id"],
|
||||
"enc_key_id": data.validated_data["enc_key_id"],
|
||||
"key_exchange_key": generate_key(),
|
||||
"provider": provider,
|
||||
},
|
||||
)
|
||||
return Response()
|
||||
|
||||
|
||||
class RegisterUserView(APIView):
|
||||
|
||||
class UserRegistration(PassiveSerializer):
|
||||
|
||||
device_uuid = CharField()
|
||||
user_secure_enclave_key = CharField()
|
||||
enclave_key_id = CharField()
|
||||
|
||||
permission_classes = []
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
serializer_class = UserRegistration
|
||||
authentication_classes = [TokenAuthentication]
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
data = self.UserRegistration(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
device = get_object_or_404(AppleDevice, endpoint_uuid=data.validated_data["device_uuid"])
|
||||
AppleDeviceUser.objects.update_or_create(
|
||||
device=device,
|
||||
user=request.user,
|
||||
defaults={
|
||||
"secure_enclave_key": data.validated_data["user_secure_enclave_key"],
|
||||
"enclave_key_id": data.validated_data["enclave_key_id"],
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"username": request.user.username,
|
||||
}
|
||||
)
|
@ -1,16 +0,0 @@
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.views import View
|
||||
|
||||
|
||||
class AppleAppSiteAssociation(View):
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
return JsonResponse(
|
||||
{
|
||||
"authsrv": {
|
||||
"apps": [
|
||||
"232G855Y8N.io.goauthentik.endpoint",
|
||||
"232G855Y8N.io.goauthentik.endpoint.psso",
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
@ -1,140 +0,0 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from jwt import PyJWT, decode
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, Session, User
|
||||
from authentik.core.sessions import SessionStore
|
||||
from authentik.enterprise.providers.apple_psso.http import JWEResponse
|
||||
from authentik.enterprise.providers.apple_psso.models import (
|
||||
AppleDevice,
|
||||
AppleDeviceUser,
|
||||
AppleNonce,
|
||||
ApplePlatformSSOProvider,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.signals import SESSION_LOGIN_EVENT
|
||||
from authentik.providers.oauth2.constants import TOKEN_TYPE
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
from authentik.root.middleware import SessionMiddleware
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TokenView(View):
|
||||
|
||||
device: AppleDevice
|
||||
provider: ApplePlatformSSOProvider
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
version = request.POST.get("platform_sso_version")
|
||||
assertion = request.POST.get("assertion", request.POST.get("request"))
|
||||
if not assertion:
|
||||
return HttpResponse(status=400)
|
||||
|
||||
decode_unvalidated = PyJWT().decode_complete(assertion, options={"verify_signature": False})
|
||||
LOGGER.debug(decode_unvalidated["header"])
|
||||
expected_kid = decode_unvalidated["header"]["kid"]
|
||||
|
||||
self.device = AppleDevice.objects.filter(sign_key_id=expected_kid).first()
|
||||
if not self.device:
|
||||
raise Http404
|
||||
self.provider = self.device.provider
|
||||
|
||||
# Properly decode the JWT with the key from the device
|
||||
decoded = decode(
|
||||
assertion, self.device.signing_key, algorithms=["ES256"], options={"verify_aud": False}
|
||||
)
|
||||
LOGGER.debug(decoded)
|
||||
|
||||
LOGGER.debug("got device", device=self.device)
|
||||
|
||||
# Check that the nonce hasn't been used before
|
||||
nonce = AppleNonce.objects.filter(nonce=decoded["request_nonce"]).first()
|
||||
if not nonce:
|
||||
return HttpResponse(status=400)
|
||||
nonce.delete()
|
||||
|
||||
handler_func = (
|
||||
f"handle_v{version}_{decode_unvalidated["header"]["typ"]}".replace("-", "_")
|
||||
.replace("+", "_")
|
||||
.replace(".", "_")
|
||||
)
|
||||
handler = getattr(self, handler_func, None)
|
||||
if not handler:
|
||||
LOGGER.debug("Handler not found", handler=handler_func)
|
||||
return HttpResponse(status=400)
|
||||
LOGGER.debug("sending to handler", handler=handler_func)
|
||||
return handler(decoded)
|
||||
|
||||
def validate_device_user_response(self, assertion: str) -> tuple[AppleDeviceUser, dict] | None:
|
||||
"""Decode an embedded assertion and validate it by looking up the matching device user"""
|
||||
decode_unvalidated = PyJWT().decode_complete(assertion, options={"verify_signature": False})
|
||||
expected_kid = decode_unvalidated["header"]["kid"]
|
||||
|
||||
device_user = AppleDeviceUser.objects.filter(
|
||||
device=self.device, enclave_key_id=expected_kid
|
||||
).first()
|
||||
if not device_user:
|
||||
return None
|
||||
return device_user, decode(
|
||||
assertion,
|
||||
device_user.secure_enclave_key,
|
||||
audience="apple-platform-sso",
|
||||
algorithms=["ES256"],
|
||||
)
|
||||
|
||||
def create_auth_session(self, user: User):
|
||||
event = Event.new(EventAction.LOGIN).from_http(self.request, user=user)
|
||||
store = SessionStore()
|
||||
store[SESSION_LOGIN_EVENT] = event
|
||||
store.save()
|
||||
session = Session.objects.filter(session_key=store.session_key).first()
|
||||
AuthenticatedSession.objects.create(session=session, user=user)
|
||||
session = SessionMiddleware.encode_session(store.session_key, user)
|
||||
return session
|
||||
|
||||
def handle_v1_0_platformsso_login_request_jwt(self, decoded: dict):
|
||||
user = None
|
||||
if decoded["grant_type"] == "urn:ietf:params:oauth:grant-type:jwt-bearer":
|
||||
# Decode and validate inner assertion
|
||||
user, inner = self.validate_device_user_response(decoded["assertion"])
|
||||
if inner["nonce"] != decoded["nonce"]:
|
||||
LOGGER.warning("Mis-matched nonce to outer assertion")
|
||||
raise ValidationError("Invalid request")
|
||||
|
||||
refresh_token = RefreshToken(
|
||||
user=user.user,
|
||||
scope=decoded["scope"],
|
||||
expires=now() + timedelta(hours=8),
|
||||
provider=self.provider,
|
||||
auth_time=now(),
|
||||
session=None,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
refresh_token,
|
||||
self.request,
|
||||
)
|
||||
id_token.nonce = decoded["nonce"]
|
||||
refresh_token.id_token = id_token
|
||||
refresh_token.save()
|
||||
return JWEResponse(
|
||||
{
|
||||
"refresh_token": refresh_token.token,
|
||||
"refresh_token_expires_in": int((refresh_token.expires - now()).total_seconds()),
|
||||
"id_token": refresh_token.id_token.to_jwt(self.provider),
|
||||
"token_type": TOKEN_TYPE,
|
||||
"session_key": self.create_auth_session(user.user),
|
||||
},
|
||||
device=self.device,
|
||||
apv=decoded["jwe_crypto"]["apv"],
|
||||
)
|
@ -6,7 +6,7 @@ from djangoql.ast import Name
|
||||
from djangoql.exceptions import DjangoQLError
|
||||
from djangoql.queryset import apply_search
|
||||
from djangoql.schema import DjangoQLSchema
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.filters import BaseFilterBackend, SearchFilter
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -39,19 +39,21 @@ class BaseSchema(DjangoQLSchema):
|
||||
return super().resolve_name(name)
|
||||
|
||||
|
||||
class QLSearch(SearchFilter):
|
||||
class QLSearch(BaseFilterBackend):
|
||||
"""rest_framework search filter which uses DjangoQL"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._fallback = SearchFilter()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return apps.get_app_config("authentik_enterprise").enabled()
|
||||
|
||||
def get_search_terms(self, request) -> str:
|
||||
"""
|
||||
Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited.
|
||||
"""
|
||||
params = request.query_params.get(self.search_param, "")
|
||||
def get_search_terms(self, request: Request) -> str:
|
||||
"""Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited."""
|
||||
params = request.query_params.get("search", "")
|
||||
params = params.replace("\x00", "") # strip null characters
|
||||
return params
|
||||
|
||||
@ -70,9 +72,9 @@ class QLSearch(SearchFilter):
|
||||
search_query = self.get_search_terms(request)
|
||||
schema = self.get_schema(request, view)
|
||||
if len(search_query) == 0 or not self.enabled:
|
||||
return super().filter_queryset(request, queryset, view)
|
||||
return self._fallback.filter_queryset(request, queryset, view)
|
||||
try:
|
||||
return apply_search(queryset, search_query, schema=schema)
|
||||
except DjangoQLError as exc:
|
||||
LOGGER.debug("Failed to parse search expression", exc=exc)
|
||||
return super().filter_queryset(request, queryset, view)
|
||||
return self._fallback.filter_queryset(request, queryset, view)
|
||||
|
@ -57,7 +57,7 @@ class QLTest(APITestCase):
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
content = loads(res.content)
|
||||
self.assertGreaterEqual(content["pagination"]["count"], 1)
|
||||
self.assertEqual(content["pagination"]["count"], 1)
|
||||
self.assertEqual(content["results"][0]["username"], self.user.username)
|
||||
|
||||
def test_search_json(self):
|
||||
|
@ -15,7 +15,6 @@ CELERY_BEAT_SCHEDULE = {
|
||||
TENANT_APPS = [
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.policies.unique_password",
|
||||
"authentik.enterprise.providers.apple_psso",
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
|
@ -555,8 +555,6 @@ class TokenView(View):
|
||||
|
||||
provider: OAuth2Provider | None = None
|
||||
params: TokenParams | None = None
|
||||
params_class = TokenParams
|
||||
provider_class = OAuth2Provider
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
@ -576,14 +574,12 @@ class TokenView(View):
|
||||
op="authentik.providers.oauth2.post.parse",
|
||||
):
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
self.provider = self.provider_class.objects.filter(client_id=client_id).first()
|
||||
self.provider = OAuth2Provider.objects.filter(client_id=client_id).first()
|
||||
if not self.provider:
|
||||
LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
|
||||
raise TokenError("invalid_client")
|
||||
CTX_AUTH_VIA.set("oauth_client_secret")
|
||||
self.params = self.params_class.parse(
|
||||
request, self.provider, client_id, client_secret
|
||||
)
|
||||
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
|
||||
|
||||
with start_span(
|
||||
op="authentik.providers.oauth2.post.response",
|
||||
|
@ -61,22 +61,6 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
pass
|
||||
return session_key
|
||||
|
||||
@staticmethod
|
||||
def encode_session(session_key: str, user: User):
|
||||
payload = {
|
||||
"sid": session_key,
|
||||
"iss": "authentik",
|
||||
"sub": "anonymous",
|
||||
"authenticated": user.is_authenticated,
|
||||
"acr": ACR_AUTHENTIK_SESSION,
|
||||
}
|
||||
if user.is_authenticated:
|
||||
payload["sub"] = user.uid
|
||||
value = encode(payload=payload, key=SIGNING_HASH)
|
||||
if settings.TEST:
|
||||
value = session_key
|
||||
return value
|
||||
|
||||
def process_request(self, request: HttpRequest):
|
||||
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
session_key = SessionMiddleware.decode_session_key(raw_session)
|
||||
@ -133,9 +117,21 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
"request completed. The user may have logged "
|
||||
"out in a concurrent request, for example."
|
||||
) from None
|
||||
payload = {
|
||||
"sid": request.session.session_key,
|
||||
"iss": "authentik",
|
||||
"sub": "anonymous",
|
||||
"authenticated": request.user.is_authenticated,
|
||||
"acr": ACR_AUTHENTIK_SESSION,
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
payload["sub"] = request.user.uid
|
||||
value = encode(payload=payload, key=SIGNING_HASH)
|
||||
if settings.TEST:
|
||||
value = request.session.session_key
|
||||
response.set_cookie(
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
SessionMiddleware.encode_session(request.session.session_key, request.user),
|
||||
value,
|
||||
max_age=max_age,
|
||||
expires=expires,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
|
File diff suppressed because one or more lines are too long
@ -496,46 +496,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_providers_apple_psso.appleplatformssoprovider"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_providers_apple_psso.appleplatformssoprovider_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_providers_apple_psso.appleplatformssoprovider"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_providers_apple_psso.appleplatformssoprovider"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -5068,22 +5028,6 @@
|
||||
"authentik_policies_unique_password.delete_userpasswordhistory",
|
||||
"authentik_policies_unique_password.view_uniquepasswordpolicy",
|
||||
"authentik_policies_unique_password.view_userpasswordhistory",
|
||||
"authentik_providers_apple_psso.add_appledevice",
|
||||
"authentik_providers_apple_psso.add_appledeviceuser",
|
||||
"authentik_providers_apple_psso.add_applenonce",
|
||||
"authentik_providers_apple_psso.add_appleplatformssoprovider",
|
||||
"authentik_providers_apple_psso.change_appledevice",
|
||||
"authentik_providers_apple_psso.change_appledeviceuser",
|
||||
"authentik_providers_apple_psso.change_applenonce",
|
||||
"authentik_providers_apple_psso.change_appleplatformssoprovider",
|
||||
"authentik_providers_apple_psso.delete_appledevice",
|
||||
"authentik_providers_apple_psso.delete_appledeviceuser",
|
||||
"authentik_providers_apple_psso.delete_applenonce",
|
||||
"authentik_providers_apple_psso.delete_appleplatformssoprovider",
|
||||
"authentik_providers_apple_psso.view_appledevice",
|
||||
"authentik_providers_apple_psso.view_appledeviceuser",
|
||||
"authentik_providers_apple_psso.view_applenonce",
|
||||
"authentik_providers_apple_psso.view_appleplatformssoprovider",
|
||||
"authentik_providers_google_workspace.add_googleworkspaceprovider",
|
||||
"authentik_providers_google_workspace.add_googleworkspaceprovidergroup",
|
||||
"authentik_providers_google_workspace.add_googleworkspaceprovidermapping",
|
||||
@ -5655,43 +5599,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_providers_apple_psso.appleplatformssoprovider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_providers_apple_psso.appleplatformssoprovider_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_appleplatformssoprovider",
|
||||
"change_appleplatformssoprovider",
|
||||
"delete_appleplatformssoprovider",
|
||||
"view_appleplatformssoprovider"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_providers_google_workspace.googleworkspaceprovider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -7435,7 +7342,6 @@
|
||||
"authentik.enterprise",
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.policies.unique_password",
|
||||
"authentik.enterprise.providers.apple_psso",
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
@ -7546,7 +7452,6 @@
|
||||
"authentik_core.token",
|
||||
"authentik_enterprise.license",
|
||||
"authentik_policies_unique_password.uniquepasswordpolicy",
|
||||
"authentik_providers_apple_psso.appleplatformssoprovider",
|
||||
"authentik_providers_google_workspace.googleworkspaceprovider",
|
||||
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
|
||||
"authentik_providers_microsoft_entra.microsoftentraprovider",
|
||||
@ -9769,22 +9674,6 @@
|
||||
"authentik_policies_unique_password.delete_userpasswordhistory",
|
||||
"authentik_policies_unique_password.view_uniquepasswordpolicy",
|
||||
"authentik_policies_unique_password.view_userpasswordhistory",
|
||||
"authentik_providers_apple_psso.add_appledevice",
|
||||
"authentik_providers_apple_psso.add_appledeviceuser",
|
||||
"authentik_providers_apple_psso.add_applenonce",
|
||||
"authentik_providers_apple_psso.add_appleplatformssoprovider",
|
||||
"authentik_providers_apple_psso.change_appledevice",
|
||||
"authentik_providers_apple_psso.change_appledeviceuser",
|
||||
"authentik_providers_apple_psso.change_applenonce",
|
||||
"authentik_providers_apple_psso.change_appleplatformssoprovider",
|
||||
"authentik_providers_apple_psso.delete_appledevice",
|
||||
"authentik_providers_apple_psso.delete_appledeviceuser",
|
||||
"authentik_providers_apple_psso.delete_applenonce",
|
||||
"authentik_providers_apple_psso.delete_appleplatformssoprovider",
|
||||
"authentik_providers_apple_psso.view_appledevice",
|
||||
"authentik_providers_apple_psso.view_appledeviceuser",
|
||||
"authentik_providers_apple_psso.view_applenonce",
|
||||
"authentik_providers_apple_psso.view_appleplatformssoprovider",
|
||||
"authentik_providers_google_workspace.add_googleworkspaceprovider",
|
||||
"authentik_providers_google_workspace.add_googleworkspaceprovidergroup",
|
||||
"authentik_providers_google_workspace.add_googleworkspaceprovidermapping",
|
||||
|
16
schema.yml
16
schema.yml
@ -24864,7 +24864,6 @@ paths:
|
||||
- authentik_policies_password.passwordpolicy
|
||||
- authentik_policies_reputation.reputationpolicy
|
||||
- authentik_policies_unique_password.uniquepasswordpolicy
|
||||
- authentik_providers_apple_psso.appleplatformssoprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovidermapping
|
||||
- authentik_providers_ldap.ldapprovider
|
||||
@ -25114,7 +25113,6 @@ paths:
|
||||
- authentik_policies_password.passwordpolicy
|
||||
- authentik_policies_reputation.reputationpolicy
|
||||
- authentik_policies_unique_password.uniquepasswordpolicy
|
||||
- authentik_providers_apple_psso.appleplatformssoprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovidermapping
|
||||
- authentik_providers_ldap.ldapprovider
|
||||
@ -41214,7 +41212,6 @@ components:
|
||||
- authentik.enterprise
|
||||
- authentik.enterprise.audit
|
||||
- authentik.enterprise.policies.unique_password
|
||||
- authentik.enterprise.providers.apple_psso
|
||||
- authentik.enterprise.providers.google_workspace
|
||||
- authentik.enterprise.providers.microsoft_entra
|
||||
- authentik.enterprise.providers.ssf
|
||||
@ -41261,15 +41258,6 @@ components:
|
||||
- redirect_uri
|
||||
- scope
|
||||
- state
|
||||
ApplePlatformSSOProviderRequest:
|
||||
type: object
|
||||
description: ApplePlatformSSOProvider Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- name
|
||||
Application:
|
||||
type: object
|
||||
description: Application Serializer
|
||||
@ -48726,7 +48714,6 @@ components:
|
||||
- authentik_core.token
|
||||
- authentik_enterprise.license
|
||||
- authentik_policies_unique_password.uniquepasswordpolicy
|
||||
- authentik_providers_apple_psso.appleplatformssoprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovidermapping
|
||||
- authentik_providers_microsoft_entra.microsoftentraprovider
|
||||
@ -56672,7 +56659,6 @@ components:
|
||||
type: string
|
||||
ProviderModelEnum:
|
||||
enum:
|
||||
- authentik_providers_apple_psso.appleplatformssoprovider
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
- authentik_providers_ldap.ldapprovider
|
||||
- authentik_providers_microsoft_entra.microsoftentraprovider
|
||||
@ -61885,7 +61871,6 @@ components:
|
||||
- worker_id
|
||||
modelRequest:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/ApplePlatformSSOProviderRequest'
|
||||
- $ref: '#/components/schemas/GoogleWorkspaceProviderRequest'
|
||||
- $ref: '#/components/schemas/LDAPProviderRequest'
|
||||
- $ref: '#/components/schemas/MicrosoftEntraProviderRequest'
|
||||
@ -61899,7 +61884,6 @@ components:
|
||||
discriminator:
|
||||
propertyName: provider_model
|
||||
mapping:
|
||||
authentik_providers_apple_psso.appleplatformssoprovider: '#/components/schemas/ApplePlatformSSOProviderRequest'
|
||||
authentik_providers_google_workspace.googleworkspaceprovider: '#/components/schemas/GoogleWorkspaceProviderRequest'
|
||||
authentik_providers_ldap.ldapprovider: '#/components/schemas/LDAPProviderRequest'
|
||||
authentik_providers_microsoft_entra.microsoftentraprovider: '#/components/schemas/MicrosoftEntraProviderRequest'
|
||||
|
@ -478,8 +478,10 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
renderSearch(): TemplateResult {
|
||||
const runSearch = (value: string) => {
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
updateURLParams({
|
||||
search: value,
|
||||
tablePage: 1,
|
||||
});
|
||||
this.fetch();
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { updateURLParams } from "#elements/router/RouteMatch";
|
||||
import { Table } from "#elements/table/Table";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult } from "lit";
|
||||
import { CSSResult, nothing } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@ -45,7 +45,7 @@ export abstract class TablePage<T> extends Table<T> {
|
||||
: html`<ak-empty-state icon=${this.pageIcon()}
|
||||
><span>${msg("No objects found.")}</span>
|
||||
<div slot="body">
|
||||
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
|
||||
${this.searchEnabled() ? this.renderEmptyClearSearch() : nothing}
|
||||
</div>
|
||||
<div slot="primary">${this.renderObjectCreate()}</div>
|
||||
</ak-empty-state>`}
|
||||
@ -61,8 +61,10 @@ export abstract class TablePage<T> extends Table<T> {
|
||||
this.search = "";
|
||||
this.requestUpdate();
|
||||
this.fetch();
|
||||
this.page = 1;
|
||||
updateURLParams({
|
||||
search: "",
|
||||
tablePage: 1,
|
||||
});
|
||||
}}
|
||||
class="pf-c-button pf-m-link"
|
||||
|
@ -10,13 +10,19 @@ Upgrading to the latest version of authentik, whether a new major release or a p
|
||||
authentik does not support downgrading. Make sure to back up your database in case you need to revert an upgrade.
|
||||
:::
|
||||
|
||||
- **Preview the Release Notes**: Be sure to carefully read the [Release Notes](../releases/) for the specific version to which you plan to upgrade. The release might have special requirements or actions or contain breaking changes.
|
||||
**Preview the release notes**: Be sure to carefully read the [Release Notes](../releases/) for the specific version to which you plan to upgrade. The release might have special requirements or actions or contain breaking changes.
|
||||
|
||||
- **Database backup**: Make a backup of your PostgreSQL database before upgrading. You can dump your existing database to get a backup file. For more information about dumping and backing up your database, refer to [Upgrade PostgreSQL on Docker Compose](../troubleshooting/postgres/upgrade_docker.md) or [Upgrade PostgreSQL on Kubernetes](../troubleshooting/postgres/upgrade_kubernetes.md).
|
||||
**Database backup**: Before upgrading, make a backup of your PostgreSQL database. You can create a backup by dumping your existing database. For detailed instructions, refer to the relevant guide for your deployment method ([Docker Compose](../troubleshooting/postgres/upgrade_docker.md) or [Kubernetes](../troubleshooting/postgres/upgrade_kubernetes.md)).
|
||||
|
||||
- **Upgrade sequence**: Upgrades need to follow the sequence of major releases; do not skip directly from an older major version to the most recent version. For example, if you are currently running 2023.10.3, you should first upgrade to the latest 2024.2.x release, then to the latest 2024.4.x release, and finally to the latest 2024.6.x release, in sequence. Always use the latest available patch version (_x_ in this case being the latest patch release) for each major.minor release.
|
||||
**Upgrade sequence**: Upgrades must follow the sequence of major releases; **do not skip** directly from an older major version to the most recent version.
|
||||
|
||||
- **Outposts**: The versions of the authentik server and any authentik outposts must be the same. Always [upgrade any outposts](../add-secure-apps/outposts/upgrading.md) at the same time that you upgrade your authentik instance.
|
||||
Always upgrade to the latest minor version (`.x`) within each `major.minor` version before upgrading to the next major version. For example, if you're currently running `2025.2.1`, upgrade in the following order:
|
||||
|
||||
1. Upgrade to the latest `2025.2.x`.
|
||||
2. Then to the latest `2025.4.x`.
|
||||
3. Finally to the latest `2025.6.x`.
|
||||
|
||||
**Outposts**: The version of the authentik server and all authentik outposts must match. Ensure that all [outposts are upgraded](../add-secure-apps/outposts/upgrading.md) at the same time as the core authentik instance.
|
||||
|
||||
## Upgrade authentik
|
||||
|
||||
|
Reference in New Issue
Block a user