Compare commits

..

3 Commits

Author SHA1 Message Date
Teffen Ellis
64e7fa6bc0 root/middleware: drop nonce from style-src so dynamic <style> is allowed
Mermaid (and a handful of other libs in the bundle) inject `<style>`
elements at runtime via createElement+appendChild, which never carries a
nonce. CSP3 §6.6.2.2 specifies that browsers ignore `'unsafe-inline'`
whenever a nonce is also present in the same source list, so we cannot
have both — keep the nonce and break dynamic styling, or drop it and
rely on `'unsafe-inline'`. Script-side CSP keeps its nonce + strict
allowlist; only style-src is relaxed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:02:50 +02:00
Teffen Ellis
4b4968c66b Flesh out CSP requirements. 2026-04-27 23:30:04 +02:00
Teffen Ellis
4e5b938ebe Flesh out CSP. 2026-04-27 22:01:20 +02:00
140 changed files with 1163 additions and 20496 deletions

View File

@@ -64,7 +64,7 @@ runs:
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@cf525cb33f51aca27cd6fa02034117ab963ff9f1 # v2
uses: taiki-e/install-action@787505cde8a44ea468a00478fe52baf23b15bccd # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (web)

View File

@@ -14,7 +14,6 @@ pyproject.toml @goauthentik/backend
uv.lock @goauthentik/backend
Cargo.toml @goauthentik/backend
Cargo.lock @goauthentik/backend
build.rs @goauthentik/backend
go.mod @goauthentik/backend
go.sum @goauthentik/backend
.cargo/ @goauthentik/backend

5
Cargo.lock generated
View File

@@ -198,7 +198,6 @@ dependencies = [
"metrics-exporter-prometheus",
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"sqlx",
"tokio",
"tracing",
@@ -3000,9 +2999,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.40"
version = "0.23.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
dependencies = [
"aws-lc-rs",
"log",

View File

@@ -49,7 +49,6 @@ nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
notify = "= 8.2.0"
pin-project-lite = "= 0.2.17"
pyo3 = "= 0.28.3"
pyo3-build-config = "= 0.28.3"
regex = "= 1.12.3"
reqwest = { version = "= 0.13.2", features = [
"form",
@@ -66,7 +65,7 @@ reqwest-middleware = { version = "= 0.5.1", features = [
"query",
"rustls",
] }
rustls = { version = "= 0.23.40", features = ["fips"] }
rustls = { version = "= 0.23.39", features = ["fips"] }
sentry = { version = "= 0.47.0", default-features = false, features = [
"backtrace",
"contexts",
@@ -261,9 +260,6 @@ default = ["core", "proxy"]
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
proxy = ["ak-common/proxy"]
[build-dependencies]
pyo3-build-config.workspace = true
[dependencies]
ak-axum.workspace = true
ak-common.workspace = true

View File

@@ -66,4 +66,5 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"footer_links": tenant.footer_links,
"html_meta": {**get_http_meta()},
"version": authentik_full_version(),
"csp_nonce": request.request_id,
}

View File

@@ -30,8 +30,6 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
SAML_STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
DEFAULT_ISSUER = "authentik"
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2

View File

@@ -4,7 +4,7 @@ from collections.abc import Iterator
from copy import copy
from django.core.cache import cache
from django.db.models import Case, QuerySet
from django.db.models import Case, Q, QuerySet
from django.db.models.expressions import When
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
@@ -120,7 +120,6 @@ class ApplicationSerializer(ModelSerializer):
"meta_publisher",
"policy_engine_mode",
"group",
"meta_hide",
]
extra_kwargs = {
"backchannel_providers": {"required": False},
@@ -284,12 +283,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
) == "true"
queryset = self._filter_queryset_for_list(self.get_queryset())
queryset = queryset.exclude(meta_hide=True)
if only_with_launch_url:
# Pre-filter at DB level to skip expensive per-app policy evaluation
# for apps that can never appear in the launcher (no meta_launch_url
# and no provider, so no possible launch URL).
queryset = queryset.exclude(meta_launch_url="", provider__isnull=True)
# for apps that can never appear in the launcher:
# - No meta_launch_url AND no provider: no possible launch URL
# - meta_launch_url="blank://blank": documented convention to hide from launcher
queryset = queryset.exclude(
Q(meta_launch_url="", provider__isnull=True) | Q(meta_launch_url="blank://blank")
)
paginator: Pagination = self.paginator
paginated_apps = paginator.paginate_queryset(queryset, request)

View File

@@ -19,7 +19,7 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import IsAuthenticated
from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ValidationError
@@ -37,77 +37,6 @@ from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class BulkManyRelatedField(ManyRelatedField):
"""ManyRelatedField that validates all PKs in a single query instead of one per PK."""
def to_internal_value(self, data):
if isinstance(data, str) or not hasattr(data, "__iter__"):
self.fail("not_a_list", input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail("empty")
child = self.child_relation
pk_field = child.pk_field
# Coerce PKs through pk_field if defined
pk_map = {}
for item in data:
if isinstance(item, bool):
self.fail("incorrect_type", data_type=type(item).__name__)
pk = pk_field.to_internal_value(item) if pk_field else item
pk_map[pk] = item # map coerced PK -> original value for error reporting
queryset = child.get_queryset()
# Use count to validate all PKs exist in a single query
found_count = queryset.filter(pk__in=pk_map.keys()).count()
if found_count < len(pk_map):
# Some PKs not found — fall back to per-PK checks for error reporting.
# This only runs when there's an actual validation error (rare path).
for pk, original in pk_map.items():
if not queryset.filter(pk=pk).exists():
child.fail("does_not_exist", pk_value=original)
# Return raw PKs — Django's M2M set() accepts both objects and PKs,
# using get_prep_value() for type coercion. This avoids loading all
# objects into memory and avoids triggering post_init signals.
return list(pk_map.keys())
def to_representation(self, iterable):
# For non-prefetched querysets, get PKs directly without loading model instances.
# When prefetched, _result_cache is a list (possibly empty); when not, it's None.
if hasattr(iterable, "values_list") and getattr(iterable, "_result_cache", None) is None:
return list(iterable.values_list("pk", flat=True))
return super().to_representation(iterable)
class BulkPrimaryKeyRelatedField(PrimaryKeyRelatedField):
"""PrimaryKeyRelatedField that uses bulk validation when many=True."""
@classmethod
def many_init(cls, *args, **kwargs):
allow_empty = kwargs.pop("allow_empty", None)
max_length = kwargs.pop("max_length", None)
min_length = kwargs.pop("min_length", None)
child_relation = cls(*args, **kwargs)
list_kwargs = {
"child_relation": child_relation,
}
if allow_empty is not None:
list_kwargs["allow_empty"] = allow_empty
if max_length is not None:
list_kwargs["max_length"] = max_length
if min_length is not None:
list_kwargs["min_length"] = min_length
list_kwargs.update(
{
key: value
for key, value in kwargs.items()
if key in ("required", "default", "source")
}
)
return BulkManyRelatedField(**list_kwargs)
PARTIAL_USER_SERIALIZER_MODEL_FIELDS = [
"pk",
"username",
@@ -150,7 +79,6 @@ class GroupSerializer(ModelSerializer):
"""Group Serializer"""
attributes = JSONDictField(required=False)
users = BulkPrimaryKeyRelatedField(queryset=User.objects.all(), many=True, default=list)
parents = PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
parents_obj = SerializerMethodField(allow_null=True)
children_obj = SerializerMethodField(allow_null=True)
@@ -265,6 +193,9 @@ class GroupSerializer(ModelSerializer):
"children_obj",
]
extra_kwargs = {
"users": {
"default": list,
},
"children": {
"required": False,
"default": list,
@@ -294,7 +225,6 @@ class GroupFilter(FilterSet):
members_by_pk = ModelMultipleChoiceFilter(
field_name="users",
queryset=User.objects.all(),
distinct=False,
)
def filter_attributes(self, queryset, name, value):
@@ -346,8 +276,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
]
def get_queryset(self):
# Always prefetch parents and children since their PKs are always serialized
base_qs = Group.objects.all().prefetch_related("roles", "parents", "children")
base_qs = Group.objects.all().prefetch_related("roles")
if self.serializer_class(context={"request": self.request})._should_include_users:
# Only fetch fields needed by PartialUserSerializer to reduce DB load and instantiation
@@ -358,9 +287,16 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
queryset=User.objects.all().only(*PARTIAL_USER_SERIALIZER_MODEL_FIELDS),
)
)
# When include_users=false, skip users prefetch entirely.
# BulkManyRelatedField.to_representation will use values_list to get PKs
# directly without loading User instances into memory.
else:
base_qs = base_qs.prefetch_related(
Prefetch("users", queryset=User.objects.all().only("id"))
)
if self.serializer_class(context={"request": self.request})._should_include_children:
base_qs = base_qs.prefetch_related("children")
if self.serializer_class(context={"request": self.request})._should_include_parents:
base_qs = base_qs.prefetch_related("parents")
return base_qs

View File

@@ -6,7 +6,6 @@ from typing import Any
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import AnonymousUser, Permission
from django.db.models import Exists, OuterRef, Prefetch, Q
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from django.urls import reverse_lazy
@@ -14,7 +13,6 @@ from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from django_filters.filters import (
BooleanFilter,
CharFilter,
@@ -107,10 +105,6 @@ from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
INVALID_PASSWORD_HASH_MESSAGE = gettext_lazy(
"Invalid password hash format. Must be a valid Django password hash."
)
class ParamUserSerializer(PassiveSerializer):
"""Partial serializer for query parameters to select a user"""
@@ -137,7 +131,7 @@ class PartialGroupSerializer(ModelSerializer):
class UserSerializer(ModelSerializer):
"""User Serializer"""
is_superuser = SerializerMethodField()
is_superuser = BooleanField(read_only=True)
avatar = SerializerMethodField()
attributes = JSONDictField(required=False)
groups = PrimaryKeyRelatedField(
@@ -174,14 +168,6 @@ class UserSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_roles", "true")).lower() == "true"
@extend_schema_field(BooleanField)
def get_is_superuser(self, instance: User) -> bool:
"""Use annotation if available to avoid N+1 query"""
ann = getattr(instance, "_annotated_is_superuser", None)
if ann is not None:
return ann
return instance.is_superuser
@extend_schema_field(PartialGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
if not self._should_include_groups:
@@ -195,79 +181,47 @@ class UserSerializer(ModelSerializer):
return RoleSerializer(instance.roles, many=True).data
def __init__(self, *args, **kwargs):
"""Setting password and permissions directly is allowed only in blueprints."""
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False, allow_null=True)
self.fields["password_hash"] = CharField(required=False, allow_null=True)
self.fields["permissions"] = ListField(
required=False,
child=ChoiceField(choices=get_permission_choices()),
)
def create(self, validated_data: dict) -> User:
"""Create a user, with blueprint-only password and permission writes."""
is_blueprint = SERIALIZER_CONTEXT_BLUEPRINT in self.context
if is_blueprint:
password = validated_data.pop("password", None)
password_hash = validated_data.pop("password_hash", None)
permissions = validated_data.pop("permissions", [])
self._validate_password_inputs(password, password_hash)
"""If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework."""
password = validated_data.pop("password", None)
perms_qs = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in list(perms_qs)]
instance: User = super().create(validated_data)
if is_blueprint:
self._set_password(instance, password, password_hash)
perms_qs = Permission.objects.filter(
codename__in=[permission.split(".")[1] for permission in permissions]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in perms_qs]
instance.assign_perms_to_managed_role(perms_list)
self._ensure_password_not_empty(instance)
self._set_password(instance, password)
instance.assign_perms_to_managed_role(perms_list)
return instance
def update(self, instance: User, validated_data: dict) -> User:
"""Update a user, with blueprint-only password and permission writes."""
is_blueprint = SERIALIZER_CONTEXT_BLUEPRINT in self.context
if is_blueprint:
password = validated_data.pop("password", None)
password_hash = validated_data.pop("password_hash", None)
permissions = validated_data.pop("permissions", [])
self._validate_password_inputs(password, password_hash)
"""Same as `create` above, set the password directly if we're in a blueprint
context"""
password = validated_data.pop("password", None)
perms_qs = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in list(perms_qs)]
instance = super().update(instance, validated_data)
if is_blueprint:
self._set_password(instance, password, password_hash)
perms_qs = Permission.objects.filter(
codename__in=[permission.split(".")[1] for permission in permissions]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in perms_qs]
instance.assign_perms_to_managed_role(perms_list)
self._ensure_password_not_empty(instance)
self._set_password(instance, password)
instance.assign_perms_to_managed_role(perms_list)
return instance
def _validate_password_inputs(self, password: str | None, password_hash: str | None):
"""Validate mutually-exclusive password inputs before any model mutation."""
if password is not None and password_hash is not None:
raise ValidationError(_("Cannot set both password and password_hash. Use only one."))
if password_hash is None:
return
try:
User.validate_password_hash(password_hash)
except ValueError as exc:
LOGGER.warning("Failed to identify password hash format", exc_info=exc)
raise ValidationError(INVALID_PASSWORD_HASH_MESSAGE) from exc
def _set_password(self, instance: User, password: str | None, password_hash: str | None = None):
"""Set password from plain text or hash."""
if password_hash is not None:
instance.set_password_from_hash(password_hash)
instance.save()
elif password:
def _set_password(self, instance: User, password: str | None):
"""Set password of user if we're in a blueprint context, and if it's an empty
string then use an unusable password"""
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and password:
instance.set_password(password)
instance.save()
def _ensure_password_not_empty(self, instance: User):
"""Store an explicit unusable password instead of an empty password field."""
if len(instance.password) == 0:
instance.set_unusable_password()
instance.save()
@@ -436,12 +390,6 @@ class UserPasswordSetSerializer(PassiveSerializer):
password = CharField(required=True)
class UserPasswordHashSetSerializer(PassiveSerializer):
"""Payload to set a users' password hash directly"""
password = CharField(required=True)
class UserServiceAccountSerializer(PassiveSerializer):
"""Payload to create a service account"""
@@ -593,30 +541,10 @@ class UserViewSet(
def get_queryset(self):
base_qs = User.objects.all().exclude_anonymous()
# Always prefetch groups since group PKs are always serialized.
# Use full prefetch when include_groups=true (for groups_obj), ID-only otherwise.
if self.serializer_class(context={"request": self.request})._should_include_groups:
base_qs = base_qs.prefetch_related("groups")
else:
base_qs = base_qs.prefetch_related(
Prefetch("groups", queryset=Group.objects.all().only("group_uuid"))
)
if self.serializer_class(context={"request": self.request})._should_include_roles:
base_qs = base_qs.prefetch_related("roles")
else:
base_qs = base_qs.prefetch_related(
Prefetch("roles", queryset=Role.objects.all().only("uuid"))
)
# Annotate is_superuser to avoid N+1 query per user
base_qs = base_qs.annotate(
_annotated_is_superuser=Exists(
Group.objects.filter(
is_superuser=True,
).filter(
Q(users=OuterRef("pk")) | Q(descendant_nodes__descendant__users=OuterRef("pk"))
)
)
)
return base_qs
@extend_schema(
@@ -785,11 +713,6 @@ class UserViewSet(
self.request.session.modified = True
return Response(serializer.initial_data)
def _update_session_hash_after_password_change(self, request: Request, user: User):
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
LOGGER.debug("Updating session hash after password change")
update_session_auth_hash(self.request, user)
@permission_required("authentik_core.reset_user_password")
@extend_schema(
request=UserPasswordSetSerializer,
@@ -813,45 +736,9 @@ class UserViewSet(
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc)
return Response(status=400)
self._update_session_hash_after_password_change(request, user)
return Response(status=204)
@permission_required("authentik_core.reset_user_password")
@extend_schema(
request=UserPasswordHashSetSerializer,
responses={
204: OpenApiResponse(description="Successfully changed password"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
methods=["POST"],
permission_classes=[IsAuthenticated],
)
@validate(UserPasswordHashSetSerializer)
def set_password_hash(
self, request: Request, pk: int, body: UserPasswordHashSetSerializer
) -> Response:
"""Set a user's password from a pre-hashed Django password value.
Submit the Django password hash in the shared ``password`` request field.
This updates authentik's local password verifier only. It does not attempt
to propagate the password change to LDAP or Kerberos because no raw password
is available from the request payload.
"""
user: User = self.get_object()
try:
user.set_password_from_hash(body.validated_data["password"], request=request)
user.save()
except ValueError as exc:
LOGGER.debug("Failed to set password hash", exc=exc)
return Response(data={"password": [INVALID_PASSWORD_HASH_MESSAGE]}, status=400)
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password hash", exc=exc)
return Response(status=400)
self._update_session_hash_after_password_change(request, user)
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
LOGGER.debug("Updating session hash after password change")
update_session_auth_hash(self.request, user)
return Response(status=204)
@permission_required("authentik_core.reset_user_password")

View File

@@ -1,28 +0,0 @@
"""Hash password using Django's password hashers"""
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
"""Hash a password using Django's password hashers"""
help = "Hash a password for use with AUTHENTIK_BOOTSTRAP_PASSWORD_HASH"
def add_arguments(self, parser):
parser.add_argument(
"password",
type=str,
help="Password to hash",
)
def handle(self, *args, **options):
password = options["password"]
if not password:
raise CommandError("Password cannot be empty")
try:
hashed = make_password(password)
self.stdout.write(hashed)
except ValueError as exc:
raise CommandError(f"Error hashing password: {exc}") from exc

View File

@@ -1,33 +0,0 @@
# Generated by Django 5.2.12 on 2026-04-09 18:04
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_blank_launch_url(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Application = apps.get_model("authentik_core", "Application")
Application.objects.using(db_alias).filter(meta_launch_url="blank://blank").update(
meta_hide=True, meta_launch_url=""
)
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0058_setup"),
]
operations = [
migrations.AddField(
model_name="application",
name="meta_hide",
field=models.BooleanField(
default=False,
help_text="Hide this application from the user's My applications page.",
),
),
migrations.RunPython(migrate_blank_launch_url, migrations.RunPython.noop),
]

View File

@@ -10,7 +10,7 @@ from uuid import uuid4
import pgtrigger
from deepmerge import always_merger
from django.contrib.auth.hashers import check_password, identify_hasher
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser, Permission
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
@@ -560,33 +560,6 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
self.password_change_date = now()
return super().set_password(raw_password)
@staticmethod
def validate_password_hash(password_hash: str):
"""Validate that the value is a recognized Django password hash."""
identify_hasher(password_hash) # Raises ValueError if invalid
def set_password_from_hash(self, password_hash: str, signal=True, sender=None, request=None):
"""Set password directly from a pre-hashed value.
Unlike set_password(), this does not hash the input again. The provided value
must already be a valid Django password hash, and it is stored directly on the
user after validation.
Because no raw password is available, downstream password sync integrations
such as LDAP and Kerberos cannot be updated from this code path.
Raises ValueError if the hash format is not recognized.
"""
self.validate_password_hash(password_hash)
if self.pk and signal:
from authentik.core.signals import password_hash_changed
if not sender:
sender = self
password_hash_changed.send(sender=sender, user=self, request=request)
self.password = password_hash
self.password_change_date = now()
def check_password(self, raw_password: str) -> bool:
"""
Return a boolean of whether the raw_password was correct. Handles
@@ -762,9 +735,6 @@ class Application(SerializerModel, PolicyBindingModel):
meta_icon = FileField(default="", blank=True)
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
meta_hide = models.BooleanField(
default=False, help_text=_("Hide this application from the user's My applications page.")
)
objects = ApplicationQuerySet.as_manager()

View File

@@ -16,11 +16,7 @@ LOGGER = get_logger()
@receiver(post_startup)
def post_startup_setup_bootstrap(sender, **_):
if (
not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD")
and not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD_HASH")
and not getenv("AUTHENTIK_BOOTSTRAP_TOKEN")
):
if not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD") and not getenv("AUTHENTIK_BOOTSTRAP_TOKEN"):
return
LOGGER.info("Configuring authentik through bootstrap environment variables")
content = BlueprintInstance(path=BOOTSTRAP_BLUEPRINT).retrieve()

View File

@@ -24,8 +24,6 @@ from authentik.root.ws.consumer import build_device_group
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: user: User, request: HttpRequest | None
password_hash_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest,
# stage: Stage, context: dict[str, any]
login_failed = Signal()

View File

@@ -1,7 +1,7 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<script data-id="authentik-config">
<script data-id="authentik-config" nonce="{{ csp_nonce }}">
"use strict";
window.authentik = {

View File

@@ -14,6 +14,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
<meta name="darkreader-lock">
<script nonce="{{ csp_nonce }}">window.litNonce = "{{ csp_nonce }}";</script>
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
@@ -27,7 +28,7 @@
{% include "base/theme.html" %}
<style data-id="brand-css">{{ brand_css }}</style>
<style data-id="brand-css" nonce="{{ csp_nonce }}">{{ brand_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
{% block head %}
{% endblock %}

View File

@@ -9,7 +9,7 @@
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#ffffff">
{% else %}
<script data-id="theme-script">
<script data-id="theme-script" nonce="{{ csp_nonce }}">
"use strict";
(function () {
@@ -22,7 +22,7 @@
if (!(["auto", "light", "dark"].includes(locallyStoredTheme))) {
locallyStoredTheme = null;
}
const initialThemeChoice =
new URLSearchParams(window.location.search).get("theme") || locallyStoredTheme;

View File

@@ -10,7 +10,7 @@
{% endblock %}
{% block head %}
<style data-id="static-styles">
<style data-id="static-styles" nonce="{{ csp_nonce }}">
:root {
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
}

View File

@@ -129,7 +129,6 @@ class TestApplicationsAPI(APITestCase):
"meta_icon_url": None,
"meta_icon_themed_urls": None,
"meta_description": "",
"meta_hide": False,
"meta_publisher": "",
"policy_engine_mode": "any",
},
@@ -188,14 +187,12 @@ class TestApplicationsAPI(APITestCase):
"meta_icon_url": None,
"meta_icon_themed_urls": None,
"meta_description": "",
"meta_hide": False,
"meta_publisher": "",
"policy_engine_mode": "any",
},
{
"launch_url": None,
"meta_description": "",
"meta_hide": False,
"meta_icon": "",
"meta_icon_url": None,
"meta_icon_themed_urls": None,

View File

@@ -1,28 +0,0 @@
"""Tests for hash_password management command."""
from io import StringIO
from django.contrib.auth.hashers import check_password
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase
class TestHashPasswordCommand(TestCase):
"""Test hash_password management command."""
def test_hash_password(self):
"""Test hashing a password."""
out = StringIO()
call_command("hash_password", "test123", stdout=out)
hashed = out.getvalue().strip()
self.assertTrue(hashed.startswith("pbkdf2_sha256$"))
self.assertTrue(check_password("test123", hashed))
def test_hash_password_empty_fails(self):
"""Test that empty password raises error."""
with self.assertRaises(CommandError) as ctx:
call_command("hash_password", "")
self.assertIn("Password cannot be empty", str(ctx.exception))

View File

@@ -1,7 +1,6 @@
from http import HTTPStatus
from os import environ
from django.contrib.auth.hashers import make_password
from django.urls import reverse
from authentik.blueprints.tests import apply_blueprint
@@ -17,7 +16,6 @@ from authentik.tenants.flags import patch_flag
class TestSetup(FlowTestCase):
def tearDown(self):
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD", None)
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD_HASH", None)
environ.pop("AUTHENTIK_BOOTSTRAP_TOKEN", None)
@patch_flag(Setup, True)
@@ -156,19 +154,3 @@ class TestSetup(FlowTestCase):
token = Token.objects.filter(identifier="authentik-bootstrap-token").first()
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.key, environ["AUTHENTIK_BOOTSTRAP_TOKEN"])
def test_setup_bootstrap_env_password_hash(self):
"""Test setup with password hash env var"""
User.objects.filter(username="akadmin").delete()
Setup.set(False)
password = generate_id()
password_hash = make_password(password)
environ["AUTHENTIK_BOOTSTRAP_PASSWORD_HASH"] = password_hash
pre_startup.send(sender=self)
post_startup.send(sender=self)
self.assertTrue(Setup.get())
user = User.objects.get(username="akadmin")
self.assertEqual(user.password, password_hash)
self.assertTrue(user.check_password(password))

View File

@@ -1,15 +1,8 @@
"""user tests"""
from unittest.mock import patch
from django.contrib.auth.hashers import make_password
from django.test.testcases import TestCase
from rest_framework.exceptions import ValidationError
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.users import UserSerializer
from authentik.core.models import User
from authentik.core.signals import password_changed, password_hash_changed
from authentik.events.models import Event
from authentik.lib.generators import generate_id
@@ -40,99 +33,3 @@ class TestUsers(TestCase):
self.assertEqual(Event.objects.count(), 1)
user.ak_groups.all()
self.assertEqual(Event.objects.count(), 1)
def test_set_password_from_hash_signal_skips_source_sync_receivers(self):
"""Test hash password updates do not expose a raw password to sync receivers."""
user = User.objects.create(
username=generate_id(),
attributes={"distinguishedName": "cn=test,ou=users,dc=example,dc=com"},
)
password_changed_captured = []
password_hash_changed_captured = []
dispatch_uid = generate_id()
hash_dispatch_uid = generate_id()
def password_changed_receiver(sender, **kwargs):
password_changed_captured.append(kwargs)
def password_hash_changed_receiver(sender, **kwargs):
password_hash_changed_captured.append(kwargs)
password_changed.connect(password_changed_receiver, dispatch_uid=dispatch_uid)
password_hash_changed.connect(
password_hash_changed_receiver, dispatch_uid=hash_dispatch_uid
)
try:
with (
patch(
"authentik.sources.ldap.signals.LDAPSource.objects.filter"
) as ldap_sources_filter,
patch(
"authentik.sources.kerberos.signals."
"UserKerberosSourceConnection.objects.select_related"
) as kerberos_connections_select,
):
user.set_password_from_hash(make_password("new-password")) # nosec
user.save()
finally:
password_changed.disconnect(dispatch_uid=dispatch_uid)
password_hash_changed.disconnect(dispatch_uid=hash_dispatch_uid)
self.assertEqual(password_changed_captured, [])
self.assertEqual(len(password_hash_changed_captured), 1)
ldap_sources_filter.assert_not_called()
kerberos_connections_select.assert_not_called()
class TestUserSerializerPasswordHash(TestCase):
"""Test UserSerializer password_hash support in blueprint context."""
def test_password_hash_sets_password_directly(self):
"""Test a valid password hash is stored without re-hashing."""
password = "test-password-123" # nosec
password_hash = make_password(password)
serializer = UserSerializer(
data={
"username": generate_id(),
"name": "Test User",
"password_hash": password_hash,
},
context={SERIALIZER_CONTEXT_BLUEPRINT: True},
)
self.assertTrue(serializer.is_valid(), serializer.errors)
user = serializer.save()
self.assertEqual(user.password, password_hash)
self.assertTrue(user.check_password(password))
self.assertIsNotNone(user.password_change_date)
def test_password_hash_rejects_invalid_format(self):
"""Test invalid password hash values are rejected."""
serializer = UserSerializer(
data={
"username": generate_id(),
"name": "Test User",
"password_hash": "not-a-valid-hash",
},
context={SERIALIZER_CONTEXT_BLUEPRINT: True},
)
self.assertTrue(serializer.is_valid(), serializer.errors)
with self.assertRaises(ValidationError) as ctx:
serializer.save()
self.assertIn("Invalid password hash format", str(ctx.exception))
def test_password_hash_ignored_outside_blueprint_context(self):
"""Test password_hash is not accepted by the regular serializer."""
serializer = UserSerializer(
data={
"username": generate_id(),
"name": "Test User",
"password_hash": make_password("test"), # nosec
}
)
self.assertTrue(serializer.is_valid(), serializer.errors)
self.assertNotIn("password_hash", serializer.validated_data)

View File

@@ -3,7 +3,6 @@
from datetime import datetime, timedelta
from json import loads
from django.contrib.auth.hashers import make_password
from django.urls.base import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
@@ -27,9 +26,6 @@ from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignatio
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
INVALID_PASSWORD_HASH = "not-a-valid-hash"
INVALID_PASSWORD_HASH_ERROR = "Invalid password hash format. Must be a valid Django password hash."
class TestUsersAPI(APITestCase):
"""Test Users API"""
@@ -38,20 +34,6 @@ class TestUsersAPI(APITestCase):
self.admin = create_test_admin_user()
self.user = create_test_user()
def _set_password_hash(self, user: User, password_hash: str, client=None):
return (client or self.client).post(
reverse("authentik_api:user-set-password-hash", kwargs={"pk": user.pk}),
data={"password": password_hash},
)
def _assert_password_hash_set(
self, user: User, password: str, password_hash: str, response
) -> None:
self.assertEqual(response.status_code, 204, response.data)
user.refresh_from_db()
self.assertEqual(user.password, password_hash)
self.assertTrue(user.check_password(password))
def test_filter_type(self):
"""Test API filtering by type"""
self.client.force_login(self.admin)
@@ -131,26 +113,6 @@ class TestUsersAPI(APITestCase):
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
def test_set_password_hash(self):
"""Test setting a user's password from a hash."""
self.client.force_login(self.admin)
password = generate_key()
password_hash = make_password(password)
response = self._set_password_hash(self.user, password_hash)
self._assert_password_hash_set(self.user, password, password_hash, response)
def test_set_password_hash_invalid(self):
"""Test invalid password hashes are rejected."""
self.client.force_login(self.admin)
response = self._set_password_hash(self.user, INVALID_PASSWORD_HASH)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"password": [INVALID_PASSWORD_HASH_ERROR]},
)
def test_recovery(self):
"""Test user recovery link"""
flow = create_test_flow(
@@ -299,29 +261,6 @@ class TestUsersAPI(APITestCase):
self.assertTrue(token_filter.exists())
self.assertTrue(token_filter.first().expiring)
def test_service_account_set_password_hash(self):
"""Service account password hash can be set through the API."""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": False,
},
)
self.assertEqual(response.status_code, 200, response.data)
body = loads(response.content)
user = User.objects.get(pk=body["user_pk"])
self.assertEqual(user.type, UserTypes.SERVICE_ACCOUNT)
self.assertFalse(user.has_usable_password())
password = generate_key()
password_hash = make_password(password)
response = self._set_password_hash(user, password_hash)
self._assert_password_hash_set(user, password, password_hash, response)
def test_service_account_no_expire(self):
"""Service account creation without token expiration"""
self.client.force_login(self.admin)

View File

@@ -12,7 +12,7 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.core.signals import password_changed, password_hash_changed
from authentik.core.signals import password_changed
from authentik.enterprise.providers.ssf.models import (
EventTypes,
SSFProvider,
@@ -84,13 +84,14 @@ def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSessi
)
def _send_password_credential_change(user: User, change_type: str):
@receiver(password_changed)
def ssf_password_changed_cred_change(sender, user: User, password: str | None, **_):
"""Credential change trigger (password changed)"""
send_ssf_events(
EventTypes.CAEP_CREDENTIAL_CHANGE,
{
"credential_type": "password",
"change_type": change_type,
"change_type": "revoke" if password is None else "update",
},
sub_id={
"format": "complex",
@@ -102,16 +103,6 @@ def _send_password_credential_change(user: User, change_type: str):
)
@receiver(password_hash_changed)
@receiver(password_changed)
def ssf_password_changed_cred_change(signal, sender, user: User, password: str | None = None, **_):
"""Credential change trigger (password changed)"""
if signal is password_hash_changed:
_send_password_credential_change(user, "update")
return
_send_password_credential_change(user, "revoke" if password is None else "update")
device_type_map = {
StaticDevice: "pin",
TOTPDevice: "pin",

View File

@@ -1,6 +1,5 @@
from uuid import uuid4
from django.contrib.auth.hashers import make_password
from django.urls import reverse
from rest_framework.test import APITestCase
@@ -53,21 +52,6 @@ class TestSignals(APITestCase):
)
self.assertEqual(res.status_code, 201, res.content)
def _assert_password_credential_change(self, user, change_type: str):
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
event_payload = event.payload["events"][
"https://schemas.openid.net/secevent/caep/event-type/credential-change"
]
self.assertEqual(event_payload["change_type"], change_type)
self.assertEqual(event_payload["credential_type"], "password")
self.assertEqual(event.payload["sub_id"]["format"], "complex")
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
def test_signal_logout(self):
"""Test user logout"""
user = create_test_user()
@@ -95,25 +79,19 @@ class TestSignals(APITestCase):
user.set_password(generate_id())
user.save()
self._assert_password_credential_change(user, "update")
def test_signal_password_change_from_hash(self):
"""Test user password change from a pre-hashed password."""
user = create_test_user()
self.client.force_login(user)
user.set_password_from_hash(make_password(generate_id()))
user.save()
self._assert_password_credential_change(user, "update")
def test_signal_password_revoke(self):
"""Test explicit password revoke."""
user = create_test_user()
self.client.force_login(user)
user.set_password(None)
user.save()
self._assert_password_credential_change(user, "revoke")
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
event_payload = event.payload["events"][
"https://schemas.openid.net/secevent/caep/event-type/credential-change"
]
self.assertEqual(event_payload["change_type"], "update")
self.assertEqual(event_payload["credential_type"], "password")
self.assertEqual(event.payload["sub_id"]["format"], "complex")
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
def test_signal_authenticator_added(self):
"""Test authenticator creation signal"""

View File

@@ -11,7 +11,7 @@ from django.http import HttpRequest
from rest_framework.request import Request
from authentik.core.models import AuthenticatedSession, User
from authentik.core.signals import login_failed, password_changed, password_hash_changed
from authentik.core.signals import login_failed, password_changed
from authentik.events.models import Event, EventAction
from authentik.flows.models import Stage
from authentik.flows.planner import (
@@ -112,15 +112,8 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
)
@receiver(password_hash_changed)
@receiver(password_changed)
def on_password_changed(
sender,
user: User,
password: str | None = None,
request: HttpRequest | None = None,
**_,
):
def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_):
"""Log password change"""
Event.new(EventAction.PASSWORD_SET).from_http(request, user=user)

View File

@@ -2,7 +2,6 @@
from urllib.parse import urlencode
from django.contrib.auth.hashers import make_password
from django.contrib.contenttypes.models import ContentType
from django.test import RequestFactory, TestCase
from django.views.debug import SafeExceptionReporterFilter
@@ -11,7 +10,7 @@ from guardian.shortcuts import get_anonymous_user
from authentik.brands.models import Brand
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_user
from authentik.events.models import Event, EventAction
from authentik.events.models import Event
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views.executor import QS_QUERY, SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
@@ -214,14 +213,3 @@ class TestEvents(TestCase):
event = Event.new("unittest", foo="foo bar \u0000 baz")
event.save()
self.assertEqual(event.context["foo"], "foo bar baz")
def test_password_set_signal_on_set_password_from_hash(self):
"""Changing password from hash should still emit an audit event."""
user = create_test_user()
old_count = Event.objects.filter(action=EventAction.PASSWORD_SET, user__pk=user.pk).count()
user.set_password_from_hash(make_password(generate_id()))
user.save()
new_count = Event.objects.filter(action=EventAction.PASSWORD_SET, user__pk=user.pk).count()
self.assertEqual(new_count, old_count + 1)

View File

@@ -1,5 +1,5 @@
<html>
<script>
<script nonce="{{ csp_nonce }}">
window.parent.postMessage({
message: "submit",
source: "goauthentik.io",

View File

@@ -17,7 +17,7 @@
<meta name="sentry-trace" content="{{ sentry_trace }}" />
<link rel="prefetch" href="{{ flow_background_url }}" />
{% include "base/header_js.html" %}
<style data-id="flow-sfe">
<style data-id="flow-sfe" nonce="{{ csp_nonce }}">
html,
body {
height: 100%;
@@ -43,13 +43,13 @@
max-width: 100%;
}
</style>
<script src="{% static 'dist/sfe/index.js' %}"></script>
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary">
<div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
</main>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
</div>
<script src="{% static 'dist/sfe/index.js' %}"></script>
<div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
</main>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
</div>
</body>
</html>

View File

@@ -12,7 +12,7 @@
{% comment %}
@see {@link web/types/webcomponents.d.ts} for type definitions.
{% endcomment %}
<script data-id="shady-dom">
<script data-id="shady-dom" nonce="{{ csp_nonce }}">
"use strict";
window.ShadyDOM = window.ShadyDOM || {}
@@ -20,7 +20,7 @@
</script>
{% endif %}
{% include "base/header_js.html" %}
<script data-id="flow-config">
<script data-id="flow-config" nonce="{{ csp_nonce }}">
"use strict";
window.authentik.flow = {
@@ -37,7 +37,7 @@
{% block head %}
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style data-id="flow-css">
<style data-id="flow-css" nonce="{{ csp_nonce }}">
:root {
--ak-global--background-image: url("{{ flow_background_url }}");
}
@@ -55,10 +55,10 @@
loading
>
{% include "base/placeholder.html" %}
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
</ak-flow-executor>
<ak-flow-inspector
slot="panel"
id="flow-inspector"

View File

@@ -165,6 +165,8 @@ web:
timeout_http_read: 30s
timeout_http_write: 60s
timeout_http_idle: 120s
csp:
report_only: false
worker:
processes: 1

View File

@@ -24,11 +24,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.validation import validate
from authentik.common.saml.constants import (
DEFAULT_ISSUER,
SAML_BINDING_POST,
SAML_BINDING_REDIRECT,
)
from authentik.common.saml.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
@@ -59,7 +55,6 @@ class SAMLProviderSerializer(ProviderSerializer):
"""SAMLProvider Serializer"""
url_download_metadata = SerializerMethodField()
url_issuer = SerializerMethodField()
url_sso_post = SerializerMethodField()
url_sso_redirect = SerializerMethodField()
@@ -90,23 +85,6 @@ class SAMLProviderSerializer(ProviderSerializer):
+ "?download"
)
def get_url_issuer(self, instance: SAMLProvider) -> str:
"""Get Issuer/EntityID URL"""
if instance.issuer_override:
return instance.issuer_override
if "request" not in self._context:
return DEFAULT_ISSUER
request: HttpRequest = self._context["request"]._request
try:
return request.build_absolute_uri(
reverse(
"authentik_providers_saml:base",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return DEFAULT_ISSUER
def get_url_sso_post(self, instance: SAMLProvider) -> str:
"""Get SSO Post URL"""
if "request" not in self._context:
@@ -220,7 +198,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"acs_url",
"sls_url",
"audience",
"issuer_override",
"issuer",
"assertion_valid_not_before",
"assertion_valid_not_on_or_after",
"session_valid_not_on_or_after",
@@ -242,7 +220,6 @@ class SAMLProviderSerializer(ProviderSerializer):
"default_relay_state",
"default_name_id_policy",
"url_download_metadata",
"url_issuer",
"url_sso_post",
"url_sso_redirect",
"url_sso_init",

View File

@@ -1,34 +0,0 @@
# Generated by Django 5.2.11 on 2026-02-24 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0021_samlprovider_sign_logout_response"),
]
operations = [
migrations.RenameField(
model_name="samlprovider",
old_name="issuer",
new_name="issuer_override",
),
migrations.AlterField(
model_name="samlprovider",
name="issuer_override",
field=models.TextField(
blank=True,
default="",
help_text="Also known as EntityID. Providing a value overrides the default issuer generated by authentik.",
),
),
migrations.AddField(
model_name="samlsession",
name="issuer",
field=models.TextField(
default=None, help_text="SAML Issuer used for this session", null=True
),
),
]

View File

@@ -77,14 +77,7 @@ class SAMLProvider(Provider):
"no audience restriction will be added."
),
)
issuer_override = models.TextField(
blank=True,
default="",
help_text=_(
"Also known as EntityID. Providing a value overrides the default issuer "
"generated by authentik."
),
)
issuer = models.TextField(help_text=_("Also known as EntityID"), default="authentik")
sls_url = models.TextField(
blank=True,
validators=[DomainlessURLValidator(schemes=("http", "https"))],
@@ -325,9 +318,6 @@ class SAMLSession(InternallyManagedMixin, SerializerModel, ExpiringModel):
session_index = models.TextField(help_text=_("SAML SessionIndex for this session"))
name_id = models.TextField(help_text=_("SAML NameID value for this session"))
name_id_format = models.TextField(default="", blank=True, help_text=_("SAML NameID format"))
issuer = models.TextField(
default=None, null=True, help_text=_("SAML Issuer used for this session")
)
created = models.DateTimeField(auto_now_add=True)
@property

View File

@@ -6,7 +6,6 @@ from types import GeneratorType
import xmlsec
from django.http import HttpRequest
from django.urls import reverse
from django.utils.timezone import now
from lxml import etree # nosec
from lxml.etree import Element, SubElement, _Element # nosec
@@ -64,7 +63,6 @@ class AssertionProcessor:
session_index: str
name_id: str
name_id_format: str
issuer: str
session_not_on_or_after_datetime: datetime
def __init__(self, provider: SAMLProvider, request: HttpRequest, auth_n_request: AuthNRequest):
@@ -139,24 +137,10 @@ class AssertionProcessor:
continue
return attribute_statement
def _get_issuer_value(self) -> str:
"""Get issuer value, with fallback to generated URL if empty"""
# If user has set an override issuer, use it
if self.provider.issuer_override:
return self.provider.issuer_override
return self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:base",
kwargs={"application_slug": self.provider.application.slug},
)
)
def get_issuer(self) -> Element:
"""Get Issuer Element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer", nsmap=NS_MAP)
self.issuer = self._get_issuer_value()
issuer.text = self.issuer
issuer.text = self.provider.issuer
return issuer
def get_assertion_auth_n_statement(self) -> Element:

View File

@@ -8,7 +8,6 @@ from lxml import etree # nosec
from lxml.etree import Element, _Element
from authentik.common.saml.constants import (
DEFAULT_ISSUER,
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_MAP,
NS_SAML_ASSERTION,
@@ -34,12 +33,11 @@ class LogoutRequestProcessor:
name_id_format: str
session_index: str | None
relay_state: str | None
issuer: str | None
_issue_instant: str
_request_id: str
def __init__( # noqa: PLR0913
def __init__(
self,
provider: SAMLProvider,
user: User | None,
@@ -48,7 +46,6 @@ class LogoutRequestProcessor:
name_id_format: str = SAML_NAME_ID_FORMAT_EMAIL,
session_index: str | None = None,
relay_state: str | None = None,
issuer: str | None = None,
):
self.provider = provider
self.user = user
@@ -57,23 +54,14 @@ class LogoutRequestProcessor:
self.name_id_format = name_id_format
self.session_index = session_index
self.relay_state = relay_state
self.issuer = issuer
self._issue_instant = get_time_string()
self._request_id = get_random_id()
def _get_issuer_value(self) -> str:
"""Get issuer value from session, with fallback to provider"""
if self.issuer:
return self.issuer
if self.provider.issuer_override:
return self.provider.issuer_override
return DEFAULT_ISSUER
def get_issuer(self) -> Element:
"""Get Issuer element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self._get_issuer_value()
issuer.text = self.provider.issuer
return issuer
def get_name_id(self) -> Element:

View File

@@ -8,7 +8,6 @@ from lxml import etree
from lxml.etree import Element, SubElement
from authentik.common.saml.constants import (
DEFAULT_ISSUER,
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_MAP,
NS_SAML_ASSERTION,
@@ -29,38 +28,27 @@ class LogoutResponseProcessor:
logout_request: LogoutRequest
destination: str | None
relay_state: str | None
issuer: str | None
_issue_instant: str
_response_id: str
def __init__( # noqa: PLR0913
def __init__(
self,
provider: SAMLProvider,
logout_request: LogoutRequest,
destination: str | None = None,
relay_state: str | None = None,
issuer: str | None = None,
):
self.provider = provider
self.logout_request = logout_request
self.destination = destination
self.relay_state = relay_state or (logout_request.relay_state if logout_request else None)
self.issuer = issuer
self._issue_instant = get_time_string()
self._response_id = get_random_id()
def _get_issuer_value(self) -> str:
"""Get issuer value from session, with fallback to provider"""
if self.issuer:
return self.issuer
if self.provider.issuer_override:
return self.provider.issuer_override
return DEFAULT_ISSUER
def get_issuer(self) -> Element:
"""Get Issuer element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self._get_issuer_value()
issuer.text = self.provider.issuer
return issuer
def build(self, status: str = "Success") -> Element:

View File

@@ -40,19 +40,6 @@ class MetadataProcessor:
self.force_binding = None
self.xml_id = "_" + sha256(f"{provider.name}-{provider.pk}".encode("ascii")).hexdigest()
def _get_issuer_value(self) -> str:
"""Get issuer value, with fallback to generated URL if empty"""
# If user has set an override issuer, use it
if self.provider.issuer_override:
return self.provider.issuer_override
return self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:base",
kwargs={"application_slug": self.provider.application.slug},
)
)
# Using type unions doesn't work with cython types (which is what lxml is)
def get_signing_key_descriptor(self) -> Element | None:
"""Get Signing KeyDescriptor, if enabled for the provider"""
@@ -202,7 +189,7 @@ class MetadataProcessor:
"""Build full EntityDescriptor"""
entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP)
entity_descriptor.attrib["ID"] = self.xml_id
entity_descriptor.attrib["entityID"] = self._get_issuer_value()
entity_descriptor.attrib["entityID"] = self.provider.issuer
if self.provider.signing_kp:
self._prepare_signature(entity_descriptor)

View File

@@ -51,6 +51,7 @@ class ServiceProviderMetadata:
provider = SAMLProvider.objects.create(
name=name, authorization_flow=authorization_flow, invalidation_flow=invalidation_flow
)
provider.issuer = self.entity_id
provider.sp_binding = self.acs_binding
provider.acs_url = self.acs_location
provider.default_name_id_policy = self.name_id_policy

View File

@@ -75,7 +75,6 @@ def handle_saml_iframe_pre_user_logout(
name_id_format=session.name_id_format,
session_index=session.session_index,
relay_state=relay_state,
issuer=session.issuer,
)
if session.provider.sls_binding == SAMLBindings.POST:
@@ -164,7 +163,6 @@ def handle_flow_pre_user_logout(
name_id_format=session.name_id_format,
session_index=session.session_index,
relay_state=relay_state,
issuer=session.issuer,
)
if session.provider.sls_binding == SAMLBindings.POST:
@@ -226,7 +224,6 @@ def user_session_deleted_saml_logout(sender, instance: AuthenticatedSession, **_
name_id=saml_session.name_id,
name_id_format=saml_session.name_id_format,
session_index=saml_session.session_index,
issuer=saml_session.issuer,
)
@@ -260,5 +257,4 @@ def user_deactivated_saml_logout(sender, instance: User, **kwargs):
name_id=saml_session.name_id,
name_id_format=saml_session.name_id_format,
session_index=saml_session.session_index,
issuer=saml_session.issuer,
)

View File

@@ -22,7 +22,6 @@ def send_saml_logout_request(
name_id: str,
name_id_format: str,
session_index: str,
issuer: str,
):
"""Send SAML LogoutRequest to a Service Provider using session data"""
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
@@ -48,7 +47,6 @@ def send_saml_logout_request(
name_id=name_id,
name_id_format=name_id_format,
session_index=session_index,
issuer=issuer,
)
return send_post_logout_request(provider, processor)
@@ -91,7 +89,6 @@ def send_saml_logout_response(
sls_url: str,
logout_request_id: str | None = None,
relay_state: str | None = None,
issuer: str | None = None,
):
"""Send SAML LogoutResponse to a Service Provider using backchannel (server-to-server)"""
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
@@ -122,7 +119,6 @@ def send_saml_logout_response(
logout_request=logout_request,
destination=sls_url,
relay_state=relay_state,
issuer=issuer,
)
encoded_response = processor.encode_post()

View File

@@ -15,7 +15,6 @@ from authentik.common.saml.constants import (
SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
)
from authentik.core.models import Application
from authentik.core.tests.utils import (
RequestFactory,
create_test_admin_user,
@@ -98,11 +97,6 @@ class TestAuthNRequest(TestCase):
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
Application.objects.create(
name="test-app",
slug="test-app",
provider=self.provider,
)
self.source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",
@@ -532,7 +526,7 @@ class TestAuthNRequest(TestCase):
authorization_flow=create_test_flow(),
acs_url="https://10.120.20.200/saml-sp/SAML2/POST",
audience="https://10.120.20.200/saml-sp/SAML2/POST",
issuer_override="https://10.120.20.200/saml-sp/SAML2/POST",
issuer="https://10.120.20.200/saml-sp/SAML2/POST",
signing_kp=static_keypair,
verification_kp=static_keypair,
)
@@ -553,7 +547,7 @@ class TestAuthNRequest(TestCase):
"saml/acs/2d737f96-55fb-4035-953e-5e24134eb778"
),
audience="https://10.120.20.200/saml-sp/SAML2/POST",
issuer_override="https://10.120.20.200/saml-sp/SAML2/POST",
issuer="https://10.120.20.200/saml-sp/SAML2/POST",
signing_kp=create_test_cert(),
)
parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST)

View File

@@ -47,7 +47,7 @@ class TestNativeLogoutStageView(TestCase):
authorization_flow=self.flow,
acs_url="https://sp1.example.com/acs",
sls_url="https://sp1.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
logout_method=SAMLLogoutMethods.FRONTCHANNEL_NATIVE,
@@ -58,7 +58,7 @@ class TestNativeLogoutStageView(TestCase):
authorization_flow=self.flow,
acs_url="https://sp2.example.com/acs",
sls_url="https://sp2.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="post",
sls_binding="post",
logout_method=SAMLLogoutMethods.FRONTCHANNEL_NATIVE,
@@ -218,7 +218,7 @@ class TestIframeLogoutStageView(TestCase):
authorization_flow=self.flow,
acs_url="https://sp1.example.com/acs",
sls_url="https://sp1.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
logout_method="frontchannel_iframe",
@@ -229,7 +229,7 @@ class TestIframeLogoutStageView(TestCase):
authorization_flow=self.flow,
acs_url="https://sp2.example.com/acs",
sls_url="https://sp2.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="post",
sls_binding="post",
logout_method="frontchannel_iframe",
@@ -372,7 +372,7 @@ class TestIdPLogoutIntegration(FlowTestCase):
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
signing_kp=self.keypair,

View File

@@ -28,7 +28,7 @@ class TestLogoutIntegration(TestCase):
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
signature_algorithm=RSA_SHA256,
@@ -57,7 +57,7 @@ class TestLogoutIntegration(TestCase):
parsed = self.parser.parse(encoded)
# Verify all fields match
self.assertEqual(parsed.issuer, self.provider.issuer_override)
self.assertEqual(parsed.issuer, self.provider.issuer)
self.assertEqual(parsed.name_id, "test@example.com")
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
self.assertEqual(parsed.session_index, "test-session-123")
@@ -72,7 +72,7 @@ class TestLogoutIntegration(TestCase):
parsed = self.parser.parse_detached(encoded)
# Verify all fields match
self.assertEqual(parsed.issuer, self.provider.issuer_override)
self.assertEqual(parsed.issuer, self.provider.issuer)
self.assertEqual(parsed.name_id, "test@example.com")
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
self.assertEqual(parsed.session_index, "test-session-123")
@@ -106,7 +106,7 @@ class TestLogoutIntegration(TestCase):
parsed = parser.parse(encoded)
# Verify all fields match
self.assertEqual(parsed.issuer, self.provider.issuer_override)
self.assertEqual(parsed.issuer, self.provider.issuer)
self.assertEqual(parsed.name_id, "signed@example.com")
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
self.assertEqual(parsed.session_index, "signed-session-456")
@@ -125,7 +125,7 @@ class TestLogoutIntegration(TestCase):
parsed = self.parser.parse_detached(saml_request)
# Verify parsing succeeded
self.assertEqual(parsed.issuer, self.provider.issuer_override)
self.assertEqual(parsed.issuer, self.provider.issuer)
self.assertEqual(parsed.name_id, "test@example.com")
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
@@ -164,7 +164,7 @@ class TestLogoutIntegration(TestCase):
# Parse the SAMLRequest (unsigned XML)
parsed = self.parser.parse_detached(params["SAMLRequest"][0])
self.assertEqual(parsed.issuer, self.provider.issuer_override)
self.assertEqual(parsed.issuer, self.provider.issuer)
def test_form_data_can_be_parsed(self):
"""Test that form data generates parseable POST request"""
@@ -175,7 +175,7 @@ class TestLogoutIntegration(TestCase):
parsed = self.parser.parse(form_data["SAMLRequest"])
# Verify parsing succeeded
self.assertEqual(parsed.issuer, self.provider.issuer_override)
self.assertEqual(parsed.issuer, self.provider.issuer)
self.assertEqual(parsed.name_id, "test@example.com")
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
self.assertEqual(parsed.session_index, "test-session-123")
@@ -244,4 +244,4 @@ class TestLogoutIntegration(TestCase):
# But same issuer
self.assertEqual(parsed1.issuer, parsed2.issuer)
self.assertEqual(parsed1.issuer, self.provider.issuer_override)
self.assertEqual(parsed1.issuer, self.provider.issuer)

View File

@@ -35,7 +35,7 @@ class TestLogoutRequestProcessor(TestCase):
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
signature_algorithm=RSA_SHA256,

View File

@@ -1,7 +1,7 @@
"""logout response tests"""
from defusedxml import ElementTree
from django.test import RequestFactory, TestCase
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.common.saml.constants import (
@@ -9,13 +9,10 @@ from authentik.common.saml.constants import (
NS_SAML_PROTOCOL,
NS_SIGNATURE,
)
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
from authentik.providers.saml.processors.metadata import MetadataProcessor
class TestLogoutResponse(TestCase):
@@ -24,7 +21,6 @@ class TestLogoutResponse(TestCase):
@apply_blueprint("system/providers-saml.yaml")
def setUp(self):
cert = create_test_cert()
self.factory = RequestFactory()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(),
acs_url="http://testserver/source/saml/provider/acs/",
@@ -34,31 +30,17 @@ class TestLogoutResponse(TestCase):
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
self.application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
provider=self.provider,
)
def test_build_response(self):
"""Test building a LogoutResponse uses the generated issuer from the assertion"""
# Generate the issuer the same way the assertion/metadata processors would
request = self.factory.get("/")
metadata_processor = MetadataProcessor(self.provider, request)
generated_issuer = metadata_processor._get_issuer_value()
"""Test building a LogoutResponse"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
# Pass the generated issuer as if it came from SAMLSession.issuer
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
issuer=generated_issuer,
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
@@ -69,9 +51,9 @@ class TestLogoutResponse(TestCase):
self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
# Check Issuer matches the generated issuer from the assertion processor
# Check Issuer
issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
self.assertEqual(issuer.text, generated_issuer)
self.assertEqual(issuer.text, self.provider.issuer)
# Check Status
status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")

View File

@@ -85,6 +85,7 @@ class TestServiceProviderMetadataParser(TestCase):
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
provider = metadata.to_provider("test", self.flow, self.flow)
self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
self.assertEqual(
@@ -98,6 +99,7 @@ class TestServiceProviderMetadataParser(TestCase):
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
provider = metadata.to_provider("test", self.flow, self.flow)
self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata")
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
self.assertEqual(
provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")

View File

@@ -32,7 +32,7 @@ class TestSAMLSessionModel(TestCase):
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
)
# Create another provider for testing
@@ -40,7 +40,7 @@ class TestSAMLSessionModel(TestCase):
name="test-provider-2",
authorization_flow=self.flow,
acs_url="https://sp2.example.com/acs",
issuer_override="https://idp2.example.com",
issuer="https://idp2.example.com",
)
# Create a session first (using authentik's custom Session model)
@@ -72,7 +72,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify the session was created
@@ -101,7 +100,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Try to create another session with same session_index and provider
@@ -115,7 +113,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
def test_cascade_deletion_user(self):
@@ -130,7 +127,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify session exists
@@ -154,7 +150,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify session exists
@@ -178,7 +173,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify session exists
@@ -202,7 +196,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Create second session with different provider
@@ -215,7 +208,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify both sessions exist
@@ -237,7 +229,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=future_time,
expiring=True,
issuer="authentik",
)
# Verify expiry time
@@ -257,7 +248,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=past_time,
expiring=True,
issuer="authentik",
)
# Check if marked as expired
@@ -275,7 +265,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format="", # Blank format
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify it was created successfully
@@ -294,7 +283,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
session2 = SAMLSession.objects.create(
@@ -306,7 +294,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Query by provider
@@ -329,7 +316,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Check serializer property
@@ -348,7 +334,6 @@ class TestSAMLSessionModel(TestCase):
name_id_format=self.name_id_format,
expires=self.expires,
expiring=True,
issuer="authentik",
)
# Verify sessions exist

View File

@@ -7,7 +7,6 @@ from guardian.shortcuts import get_anonymous_user
from lxml import etree # nosec
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import RequestFactory, create_test_cert, create_test_flow
from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
@@ -31,11 +30,6 @@ class TestSchema(TestCase):
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
Application.objects.create(
name="test-app",
slug="test-app",
provider=self.provider,
)
self.source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",

View File

@@ -28,7 +28,7 @@ class TestSendSamlLogoutResponse(TestCase):
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@@ -137,7 +137,7 @@ class TestSendSamlLogoutRequest(TestCase):
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@@ -155,7 +155,6 @@ class TestSendSamlLogoutRequest(TestCase):
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
issuer="https://idp.example.com",
)
self.assertTrue(result)
@@ -180,7 +179,6 @@ class TestSendSamlLogoutRequest(TestCase):
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
issuer="https://idp.example.com",
)
self.assertFalse(result)
@@ -200,7 +198,6 @@ class TestSendSamlLogoutRequest(TestCase):
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
issuer="https://idp.example.com",
)
@@ -217,7 +214,7 @@ class TestSendPostLogoutRequest(TestCase):
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
signing_kp=self.cert,
)

View File

@@ -40,7 +40,7 @@ class TestSPInitiatedSLOViews(TestCase):
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
)
@@ -90,7 +90,7 @@ class TestSPInitiatedSLOViews(TestCase):
# Verify logout request was stored in plan context
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
logout_request = view.plan_context["authentik/providers/saml/logout_request"]
self.assertEqual(logout_request.issuer, self.provider.issuer_override)
self.assertEqual(logout_request.issuer, self.provider.issuer)
self.assertEqual(logout_request.session_index, "test-session-123")
def test_redirect_view_handles_logout_response_with_plan_context(self):
@@ -228,7 +228,7 @@ class TestSPInitiatedSLOViews(TestCase):
# Verify logout request was stored in plan context
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
logout_request = view.plan_context["authentik/providers/saml/logout_request"]
self.assertEqual(logout_request.issuer, self.provider.issuer_override)
self.assertEqual(logout_request.issuer, self.provider.issuer)
self.assertEqual(logout_request.session_index, "test-session-123")
def test_post_view_handles_logout_response_with_plan_context(self):
@@ -396,7 +396,7 @@ class TestSPInitiatedSLOViews(TestCase):
authorization_flow=self.flow,
acs_url="https://sp2.example.com/acs",
sls_url="https://sp2.example.com/sls",
issuer_override="https://idp2.example.com",
issuer="https://idp2.example.com",
invalidation_flow=None, # No invalidation flow
)
@@ -524,7 +524,7 @@ class TestSPInitiatedSLOLogoutMethods(TestCase):
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
signing_kp=self.cert,
@@ -714,7 +714,7 @@ class TestSPInitiatedSLOLogoutMethods(TestCase):
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="", # No SLS URL
issuer_override="https://idp.example.com",
issuer="https://idp.example.com",
)
app_no_sls = Application.objects.create(

View File

@@ -11,12 +11,6 @@ from authentik.providers.saml.views.sp_slo import (
)
urlpatterns = [
# Base path for Issuer/Entity ID
path(
"<slug:application_slug>/",
sso.SAMLSSOBindingRedirectView.as_view(),
name="base",
),
# SSO Bindings
path(
"<slug:application_slug>/sso/binding/redirect/",

View File

@@ -81,7 +81,6 @@ class SAMLFlowFinalView(ChallengeStageView):
"session": auth_session,
"name_id": processor.name_id,
"name_id_format": processor.name_id_format,
"issuer": processor.issuer,
"expires": processor.session_not_on_or_after_datetime,
"expiring": True,
},

View File

@@ -107,25 +107,12 @@ class SPInitiatedSLOView(PolicyAccessView):
# Store relay state for the logout response
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
# Look up the session issuer to use in the logout response
auth_session = AuthenticatedSession.from_request(request, request.user)
session_issuer = None
if auth_session:
saml_session = SAMLSession.objects.filter(
session=auth_session,
user=request.user,
provider=self.provider,
).first()
if saml_session:
session_issuer = saml_session.issuer
if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
# Native mode - user will be redirected/posted away from authentik
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
issuer=session_issuer,
)
if self.provider.sls_binding == SAMLBindings.POST:
@@ -165,7 +152,6 @@ class SPInitiatedSLOView(PolicyAccessView):
sls_url=self.provider.sls_url,
logout_request_id=logout_request.id if logout_request else None,
relay_state=relay_state,
issuer=session_issuer,
)
LOGGER.debug(
@@ -182,7 +168,6 @@ class SPInitiatedSLOView(PolicyAccessView):
self.provider,
logout_request,
destination=self.provider.sls_url,
issuer=session_issuer,
)
logout_response = processor.build_response()

View File

@@ -5,6 +5,7 @@ from hashlib import sha512
from ipaddress import ip_address
from time import perf_counter, time
from typing import Any
from urllib.parse import urlsplit
from channels.exceptions import DenyConnection
from django.conf import settings
@@ -314,6 +315,126 @@ class ChannelsLoggingMiddleware:
)
CSP_HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"
CSP_HEADER_ENFORCE = "Content-Security-Policy"
class ContentSecurityPolicyMiddleware:
"""Emit a Content-Security-Policy(-Report-Only) header carrying the per-request nonce.
The policy is intentionally strict: inline `<script>`/`<style>` are rejected unless
they carry the request's nonce (set via `request.request_id` and exposed to templates
as `csp_nonce`). External resources from third-party login providers (Apple, Telegram)
and configurable captcha hosts are allow-listed below; the report-only mode lets the
browser surface anything else as a console violation without breaking the page.
"""
get_response: Callable[[HttpRequest], HttpResponse]
# Hosts that the bundled login flows pull resources from. The captcha stage allows
# an admin-configured `js_url`, so the well-known third-party captcha origins are
# included here so that report-only output is not drowned in expected violations.
SCRIPT_SRC_THIRD_PARTY = (
"https://appleid.cdn-apple.com",
"https://telegram.org",
"https://www.google.com",
"https://www.gstatic.com",
"https://www.recaptcha.net",
"https://js.hcaptcha.com",
"https://challenges.cloudflare.com",
)
FRAME_SRC_THIRD_PARTY = (
"https://appleid.apple.com",
"https://oauth.telegram.org",
"https://www.google.com",
"https://newassets.hcaptcha.com",
"https://challenges.cloudflare.com",
)
# Dev-only origins. The esbuild live-reload plugin opens an EventSource against
# a dynamically chosen localhost port, so localhost on any scheme/port is allowed
# when DEBUG is on. Never folded into prod policy.
DEBUG_CONNECT_SRC = (
"http://localhost:*",
"https://localhost:*",
"ws://localhost:*",
"wss://localhost:*",
)
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
self.report_only = CONFIG.get_bool("web.csp.report_only", True)
self.debug = settings.DEBUG
self.sentry_origin = self._sentry_origin(CONFIG.get("error_reporting.sentry_dsn", ""))
@staticmethod
def _sentry_origin(dsn: str) -> str | None:
"""Pull `scheme://host[:port]` out of a Sentry DSN so the browser SDK
can ship envelopes to it. DSNs are `https://<key>@<host>/<project>`."""
if not dsn:
return None
parts = urlsplit(dsn)
if not parts.scheme or not parts.hostname:
return None
host = parts.hostname
if parts.port:
host = f"{host}:{parts.port}"
return f"{parts.scheme}://{host}"
def _build_policy(self, nonce: str) -> str:
nonce_token = f"'nonce-{nonce}'"
script_src = ("'self'", nonce_token, *self.SCRIPT_SRC_THIRD_PARTY)
# Per CSP3 §6.6.2.2, browsers ignore `'unsafe-inline'` whenever a
# nonce is also present in the same source list. Several runtime
# libraries we ship (mermaid, PatternFly's own style injections,
# DOMPurify's sanitization sandbox) emit `<style>` elements
# dynamically without a nonce, so we drop the nonce for styles
# and rely on `'unsafe-inline'`. Script-side CSP is unaffected
# — the eval/script protections remain strict.
style_src = ("'self'", "'unsafe-inline'")
frame_src = ("'self'", *self.FRAME_SRC_THIRD_PARTY)
connect_src: tuple[str, ...] = ("'self'", "ws:", "wss:")
if self.sentry_origin:
connect_src = (*connect_src, self.sentry_origin)
if self.debug:
connect_src = (*connect_src, *self.DEBUG_CONNECT_SRC)
directives = {
"default-src": ("'self'",),
"script-src": script_src,
"style-src": style_src,
# Inline `style="..."` attributes can't carry a nonce; many libraries
# (PatternFly, Lit style bindings) set them dynamically.
"style-src-attr": ("'unsafe-inline'",),
"img-src": ("'self'", "data:", "blob:", "https:"),
"font-src": ("'self'", "data:"),
"connect-src": connect_src,
"frame-src": frame_src,
"media-src": ("'self'",),
"worker-src": ("'self'", "blob:"),
"object-src": ("'none'",),
"base-uri": ("'self'",),
"form-action": ("'self'",),
"frame-ancestors": ("'none'",),
}
return "; ".join(f"{name} {' '.join(values)}" for name, values in directives.items())
def __call__(self, request: HttpRequest) -> HttpResponse:
response = self.get_response(request)
# Only attach to HTML responses — CSP on JSON/binary responses is just header bloat.
content_type = response.get("Content-Type", "")
if not content_type.startswith("text/html"):
return response
nonce = getattr(request, "request_id", None)
if not nonce:
return response
header = CSP_HEADER_REPORT_ONLY if self.report_only else CSP_HEADER_ENFORCE
# Don't clobber a policy a downstream view explicitly set.
if header in response:
return response
response[header] = self._build_policy(nonce)
return response
class LoggingMiddleware:
"""Logger middleware"""

View File

@@ -276,6 +276,7 @@ MIDDLEWARE = [
"authentik.core.middleware.RequestIDMiddleware",
"authentik.brands.middleware.BrandMiddleware",
"authentik.events.middleware.AuditMiddleware",
"authentik.root.middleware.ContentSecurityPolicyMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"authentik.root.middleware.CsrfViewMiddleware",

View File

@@ -94,7 +94,7 @@ class OAuthCallback(OAuthClientMixin, View):
def get_user_id(self, info: dict[str, Any]) -> str | None:
"""Return unique identifier from the profile info."""
if "id" in info:
return str(info["id"])
return info["id"]
return None
def handle_login_failure(self, reason: str) -> HttpResponse:

View File

@@ -353,7 +353,7 @@ class IdentificationStageView(ChallengeStageView):
PLAN_CONTEXT_APPLICATION, Application()
)
challenge.initial_data["application_pre"] = app.name
if not app.meta_hide and (launch_url := app.get_launch_url()):
if launch_url := app.get_launch_url():
challenge.initial_data["application_pre_launch"] = launch_url
if (
PLAN_CONTEXT_DEVICE in self.executor.plan.context

View File

@@ -5215,11 +5215,6 @@
"type": "string",
"title": "Group"
},
"meta_hide": {
"type": "boolean",
"title": "Meta hide",
"description": "Hide this application from the user's My applications page."
},
"icon": {
"type": "string",
"minLength": 1,
@@ -5537,14 +5532,6 @@
"minLength": 1,
"title": "Password"
},
"password_hash": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Password hash"
},
"permissions": {
"type": "array",
"items": {
@@ -10825,10 +10812,11 @@
"title": "Audience",
"description": "Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added."
},
"issuer_override": {
"issuer": {
"type": "string",
"title": "Issuer override",
"description": "Also known as EntityID. Providing a value overrides the default issuer generated by authentik."
"minLength": 1,
"title": "Issuer",
"description": "Also known as EntityID"
},
"assertion_valid_not_before": {
"type": "string",

View File

@@ -11,7 +11,6 @@ context:
group_name: authentik Admins
email: !Env [AUTHENTIK_BOOTSTRAP_EMAIL, "root@example.com"]
password: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD, null]
password_hash: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD_HASH, null]
token: !Env [AUTHENTIK_BOOTSTRAP_TOKEN, null]
entries:
- model: authentik_core.group
@@ -32,7 +31,6 @@ entries:
groups:
- !KeyOf admin-group
password: !Context password
password_hash: !Context password_hash
- model: authentik_core.token
state: created
conditions:

View File

@@ -1,6 +0,0 @@
fn main() {
#[cfg(feature = "core")]
{
pyo3_build_config::add_libpython_rpath_link_args();
}
}

2
go.mod
View File

@@ -7,7 +7,7 @@ require (
beryju.io/radius-eap v0.1.0
github.com/avast/retry-go/v4 v4.7.0
github.com/coreos/go-oidc/v3 v3.18.0
github.com/getsentry/sentry-go v0.46.0
github.com/getsentry/sentry-go v0.45.1
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-openapi/runtime v0.29.4

4
go.sum
View File

@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.46.0 h1:mbdDaarbUdOt9X+dx6kDdntkShLEX3/+KyOsVDTPDj0=
github.com/getsentry/sentry-go v0.46.0/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
github.com/getsentry/sentry-go v0.45.1 h1:9rfzJtGiJG+MGIaWZXidDGHcH5GU1Z5y0WVJGf9nysw=
github.com/getsentry/sentry-go v0.45.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=

View File

@@ -1,15 +1,17 @@
"""Wrapper for lifecycle/ak, to be installed by uv"""
import subprocess
from os import system, waitstatus_to_exitcode
from pathlib import Path
from sys import argv, exit
def main():
"""Wrapper around ak bash script"""
script = Path(__file__).parent / "ak"
res = subprocess.run([str(script), *argv[1:]], check=False)
exit(res.returncode)
current_path = Path(__file__)
args = " ".join(argv[1:])
res = system(f"{current_path.parent}/ak {args}") # nosec
exit_code = waitstatus_to_exitcode(res)
exit(exit_code)
if __name__ == "__main__":

View File

@@ -147,7 +147,6 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
--mount=type=bind,target=.cargo/,src=.cargo/ \
--mount=type=bind,target=build.rs,src=build.rs \
--mount=type=bind,target=src/,src=src/ \
--mount=type=bind,target=packages/ak-axum,src=packages/ak-axum \
--mount=type=bind,target=packages/ak-common,src=packages/ak-common \

Binary file not shown.

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-29 00:28+0000\n"
"POT-Creation-Date: 2026-04-23 00:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -392,10 +392,6 @@ msgstr ""
msgid "Open launch URL in a new browser tab or window."
msgstr ""
#: authentik/core/models.py
msgid "Hide this application from the user's My applications page."
msgstr ""
#: authentik/core/models.py
msgid "Application"
msgstr ""
@@ -2495,9 +2491,7 @@ msgid ""
msgstr ""
#: authentik/providers/saml/models.py
msgid ""
"Also known as EntityID. Providing a value overrides the default issuer "
"generated by authentik."
msgid "Also known as EntityID"
msgstr ""
#: authentik/providers/saml/models.py
@@ -2691,10 +2685,6 @@ msgstr ""
msgid "SAML NameID format"
msgstr ""
#: authentik/providers/saml/models.py
msgid "SAML Issuer used for this session"
msgstr ""
#: authentik/providers/saml/models.py
msgid "SAML Session"
msgstr ""
@@ -2727,10 +2717,6 @@ msgstr ""
msgid "Webex"
msgstr ""
#: authentik/providers/scim/models.py
msgid "vCenter"
msgstr ""
#: authentik/providers/scim/models.py
msgid "Group filters used to define sync-scope for groups."
msgstr ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,7 @@
//! Utilities for working with [`Router`].
use ak_common::config;
use axum::{
Router,
extract::Request,
http::{HeaderName, HeaderValue, StatusCode},
middleware::{Next, from_fn},
response::Response,
};
use axum::{Router, http::StatusCode, middleware::from_fn};
use tower::ServiceBuilder;
use tower_http::timeout::TimeoutLayer;
@@ -19,16 +13,6 @@ use crate::{
tracing::{span_middleware, tracing_middleware},
};
const X_POWERED_BY: HeaderName = HeaderName::from_static("x-powered-by");
async fn powered_by_authentik_middleware(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
response
.headers_mut()
.insert(X_POWERED_BY, HeaderValue::from_static("authentik"));
response
}
/// Wrap a [`Router`] with common middlewares.
///
/// Set `with_tracing` to [`true`] to log requests.
@@ -46,7 +30,6 @@ pub fn wrap_router(router: Router, with_tracing: bool) -> Router {
timeout,
))
.layer(from_fn(span_middleware))
.layer(from_fn(powered_by_authentik_middleware))
.layer(from_fn(trusted_proxy_middleware))
.layer(from_fn(client_ip_middleware))
.layer(from_fn(scheme_middleware))

View File

@@ -54,7 +54,6 @@ import type {
User,
UserAccountRequest,
UserConsent,
UserPasswordHashSetRequest,
UserPasswordSetRequest,
UserPath,
UserRecoveryEmailRequest,
@@ -105,7 +104,6 @@ import {
UserAccountRequestToJSON,
UserConsentFromJSON,
UserFromJSON,
UserPasswordHashSetRequestToJSON,
UserPasswordSetRequestToJSON,
UserPathFromJSON,
UserRecoveryEmailRequestToJSON,
@@ -510,11 +508,6 @@ export interface CoreUsersSetPasswordCreateRequest {
userPasswordSetRequest: UserPasswordSetRequest;
}
export interface CoreUsersSetPasswordHashCreateRequest {
id: number;
userPasswordHashSetRequest: UserPasswordHashSetRequest;
}
export interface CoreUsersUpdateRequest {
id: number;
userRequest: UserRequest;
@@ -5295,77 +5288,6 @@ export class CoreApi extends runtime.BaseAPI {
await this.coreUsersSetPasswordCreateRaw(requestParameters, initOverrides);
}
/**
* Creates request options for coreUsersSetPasswordHashCreate without sending the request
*/
async coreUsersSetPasswordHashCreateRequestOpts(
requestParameters: CoreUsersSetPasswordHashCreateRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["id"] == null) {
throw new runtime.RequiredError(
"id",
'Required parameter "id" was null or undefined when calling coreUsersSetPasswordHashCreate().',
);
}
if (requestParameters["userPasswordHashSetRequest"] == null) {
throw new runtime.RequiredError(
"userPasswordHashSetRequest",
'Required parameter "userPasswordHashSetRequest" was null or undefined when calling coreUsersSetPasswordHashCreate().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/users/{id}/set_password_hash/`;
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters["id"])));
return {
path: urlPath,
method: "POST",
headers: headerParameters,
query: queryParameters,
body: UserPasswordHashSetRequestToJSON(requestParameters["userPasswordHashSetRequest"]),
};
}
/**
* Set a user\'s password from a pre-hashed Django password value. Submit the Django password hash in the shared ``password`` request field. This updates authentik\'s local password verifier only. It does not attempt to propagate the password change to LDAP or Kerberos because no raw password is available from the request payload.
*/
async coreUsersSetPasswordHashCreateRaw(
requestParameters: CoreUsersSetPasswordHashCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<void>> {
const requestOptions =
await this.coreUsersSetPasswordHashCreateRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* Set a user\'s password from a pre-hashed Django password value. Submit the Django password hash in the shared ``password`` request field. This updates authentik\'s local password verifier only. It does not attempt to propagate the password change to LDAP or Kerberos because no raw password is available from the request payload.
*/
async coreUsersSetPasswordHashCreate(
requestParameters: CoreUsersSetPasswordHashCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<void> {
await this.coreUsersSetPasswordHashCreateRaw(requestParameters, initOverrides);
}
/**
* Creates request options for coreUsersUpdate without sending the request
*/

View File

@@ -634,7 +634,7 @@ export interface ProvidersSamlListRequest {
encryptionKp?: string;
invalidationFlow?: string;
isBackchannel?: boolean;
issuerOverride?: string;
issuer?: string;
logoutMethod?: SAMLLogoutMethods;
name?: string;
nameIdMapping?: string;
@@ -841,7 +841,7 @@ export interface ProvidersWsfedListRequest {
encryptionKp?: string;
invalidationFlow?: string;
isBackchannel?: boolean;
issuerOverride?: string;
issuer?: string;
logoutMethod?: SAMLLogoutMethods;
name?: string;
nameIdMapping?: string;
@@ -6842,8 +6842,8 @@ export class ProvidersApi extends runtime.BaseAPI {
queryParameters["is_backchannel"] = requestParameters["isBackchannel"];
}
if (requestParameters["issuerOverride"] != null) {
queryParameters["issuer_override"] = requestParameters["issuerOverride"];
if (requestParameters["issuer"] != null) {
queryParameters["issuer"] = requestParameters["issuer"];
}
if (requestParameters["logoutMethod"] != null) {
@@ -9326,8 +9326,8 @@ export class ProvidersApi extends runtime.BaseAPI {
queryParameters["is_backchannel"] = requestParameters["isBackchannel"];
}
if (requestParameters["issuerOverride"] != null) {
queryParameters["issuer_override"] = requestParameters["issuerOverride"];
if (requestParameters["issuer"] != null) {
queryParameters["issuer"] = requestParameters["issuer"];
}
if (requestParameters["logoutMethod"] != null) {

View File

@@ -127,12 +127,6 @@ export interface Application {
* @memberof Application
*/
group?: string;
/**
* Hide this application from the user's My applications page.
* @type {boolean}
* @memberof Application
*/
metaHide?: boolean;
}
/**
@@ -183,7 +177,6 @@ export function ApplicationFromJSONTyped(json: any, ignoreDiscriminator: boolean
? undefined
: PolicyEngineModeFromJSON(json["policy_engine_mode"]),
group: json["group"] == null ? undefined : json["group"],
metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"],
};
}
@@ -219,6 +212,5 @@ export function ApplicationToJSONTyped(
meta_publisher: value["metaPublisher"],
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
group: value["group"],
meta_hide: value["metaHide"],
};
}

View File

@@ -87,12 +87,6 @@ export interface ApplicationRequest {
* @memberof ApplicationRequest
*/
group?: string;
/**
* Hide this application from the user's My applications page.
* @type {boolean}
* @memberof ApplicationRequest
*/
metaHide?: boolean;
}
/**
@@ -131,7 +125,6 @@ export function ApplicationRequestFromJSONTyped(
? undefined
: PolicyEngineModeFromJSON(json["policy_engine_mode"]),
group: json["group"] == null ? undefined : json["group"],
metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"],
};
}
@@ -159,6 +152,5 @@ export function ApplicationRequestToJSONTyped(
meta_publisher: value["metaPublisher"],
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
group: value["group"],
meta_hide: value["metaHide"],
};
}

View File

@@ -87,12 +87,6 @@ export interface PatchedApplicationRequest {
* @memberof PatchedApplicationRequest
*/
group?: string;
/**
* Hide this application from the user's My applications page.
* @type {boolean}
* @memberof PatchedApplicationRequest
*/
metaHide?: boolean;
}
/**
@@ -131,7 +125,6 @@ export function PatchedApplicationRequestFromJSONTyped(
? undefined
: PolicyEngineModeFromJSON(json["policy_engine_mode"]),
group: json["group"] == null ? undefined : json["group"],
metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"],
};
}
@@ -159,6 +152,5 @@ export function PatchedApplicationRequestToJSONTyped(
meta_publisher: value["metaPublisher"],
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
group: value["group"],
meta_hide: value["metaHide"],
};
}

View File

@@ -81,11 +81,11 @@ export interface PatchedSAMLProviderRequest {
*/
audience?: string;
/**
* Also known as EntityID. Providing a value overrides the default issuer generated by authentik.
* Also known as EntityID
* @type {string}
* @memberof PatchedSAMLProviderRequest
*/
issuerOverride?: string;
issuer?: string;
/**
* Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3).
* @type {string}
@@ -233,7 +233,7 @@ export function PatchedSAMLProviderRequestFromJSONTyped(
acsUrl: json["acs_url"] == null ? undefined : json["acs_url"],
slsUrl: json["sls_url"] == null ? undefined : json["sls_url"],
audience: json["audience"] == null ? undefined : json["audience"],
issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"],
issuer: json["issuer"] == null ? undefined : json["issuer"],
assertionValidNotBefore:
json["assertion_valid_not_before"] == null
? undefined
@@ -306,7 +306,7 @@ export function PatchedSAMLProviderRequestToJSONTyped(
acs_url: value["acsUrl"],
sls_url: value["slsUrl"],
audience: value["audience"],
issuer_override: value["issuerOverride"],
issuer: value["issuer"],
assertion_valid_not_before: value["assertionValidNotBefore"],
assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"],
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],

View File

@@ -135,11 +135,11 @@ export interface SAMLProvider {
*/
audience?: string;
/**
* Also known as EntityID. Providing a value overrides the default issuer generated by authentik.
* Also known as EntityID
* @type {string}
* @memberof SAMLProvider
*/
issuerOverride?: string;
issuer?: string;
/**
* Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3).
* @type {string}
@@ -260,12 +260,6 @@ export interface SAMLProvider {
* @memberof SAMLProvider
*/
readonly urlDownloadMetadata: string;
/**
* Get Issuer/EntityID URL
* @type {string}
* @memberof SAMLProvider
*/
readonly urlIssuer: string;
/**
* Get SSO Post URL
* @type {string}
@@ -327,7 +321,6 @@ export function instanceOfSAMLProvider(value: object): value is SAMLProvider {
if (!("acsUrl" in value) || value["acsUrl"] === undefined) return false;
if (!("urlDownloadMetadata" in value) || value["urlDownloadMetadata"] === undefined)
return false;
if (!("urlIssuer" in value) || value["urlIssuer"] === undefined) return false;
if (!("urlSsoPost" in value) || value["urlSsoPost"] === undefined) return false;
if (!("urlSsoRedirect" in value) || value["urlSsoRedirect"] === undefined) return false;
if (!("urlSsoInit" in value) || value["urlSsoInit"] === undefined) return false;
@@ -363,7 +356,7 @@ export function SAMLProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea
acsUrl: json["acs_url"],
slsUrl: json["sls_url"] == null ? undefined : json["sls_url"],
audience: json["audience"] == null ? undefined : json["audience"],
issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"],
issuer: json["issuer"] == null ? undefined : json["issuer"],
assertionValidNotBefore:
json["assertion_valid_not_before"] == null
? undefined
@@ -413,7 +406,6 @@ export function SAMLProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea
? undefined
: SAMLNameIDPolicyEnumFromJSON(json["default_name_id_policy"]),
urlDownloadMetadata: json["url_download_metadata"],
urlIssuer: json["url_issuer"],
urlSsoPost: json["url_sso_post"],
urlSsoRedirect: json["url_sso_redirect"],
urlSsoInit: json["url_sso_init"],
@@ -439,7 +431,6 @@ export function SAMLProviderToJSONTyped(
| "verbose_name_plural"
| "meta_model_name"
| "url_download_metadata"
| "url_issuer"
| "url_sso_post"
| "url_sso_redirect"
| "url_sso_init"
@@ -461,7 +452,7 @@ export function SAMLProviderToJSONTyped(
acs_url: value["acsUrl"],
sls_url: value["slsUrl"],
audience: value["audience"],
issuer_override: value["issuerOverride"],
issuer: value["issuer"],
assertion_valid_not_before: value["assertionValidNotBefore"],
assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"],
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],

View File

@@ -81,11 +81,11 @@ export interface SAMLProviderRequest {
*/
audience?: string;
/**
* Also known as EntityID. Providing a value overrides the default issuer generated by authentik.
* Also known as EntityID
* @type {string}
* @memberof SAMLProviderRequest
*/
issuerOverride?: string;
issuer?: string;
/**
* Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3).
* @type {string}
@@ -234,7 +234,7 @@ export function SAMLProviderRequestFromJSONTyped(
acsUrl: json["acs_url"],
slsUrl: json["sls_url"] == null ? undefined : json["sls_url"],
audience: json["audience"] == null ? undefined : json["audience"],
issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"],
issuer: json["issuer"] == null ? undefined : json["issuer"],
assertionValidNotBefore:
json["assertion_valid_not_before"] == null
? undefined
@@ -307,7 +307,7 @@ export function SAMLProviderRequestToJSONTyped(
acs_url: value["acsUrl"],
sls_url: value["slsUrl"],
audience: value["audience"],
issuer_override: value["issuerOverride"],
issuer: value["issuer"],
assertion_valid_not_before: value["assertionValidNotBefore"],
assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"],
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],

View File

@@ -1,70 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Payload to set a users' password hash directly
* @export
* @interface UserPasswordHashSetRequest
*/
export interface UserPasswordHashSetRequest {
/**
*
* @type {string}
* @memberof UserPasswordHashSetRequest
*/
password: string;
}
/**
* Check if a given object implements the UserPasswordHashSetRequest interface.
*/
export function instanceOfUserPasswordHashSetRequest(
value: object,
): value is UserPasswordHashSetRequest {
if (!("password" in value) || value["password"] === undefined) return false;
return true;
}
export function UserPasswordHashSetRequestFromJSON(json: any): UserPasswordHashSetRequest {
return UserPasswordHashSetRequestFromJSONTyped(json, false);
}
export function UserPasswordHashSetRequestFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): UserPasswordHashSetRequest {
if (json == null) {
return json;
}
return {
password: json["password"],
};
}
export function UserPasswordHashSetRequestToJSON(json: any): UserPasswordHashSetRequest {
return UserPasswordHashSetRequestToJSONTyped(json, false);
}
export function UserPasswordHashSetRequestToJSONTyped(
value?: UserPasswordHashSetRequest | null,
ignoreDiscriminator: boolean = false,
): any {
if (value == null) {
return value;
}
return {
password: value["password"],
};
}

View File

@@ -842,7 +842,6 @@ export * from "./UserLogoutStageRequest";
export * from "./UserMatchingModeEnum";
export * from "./UserOAuthSourceConnection";
export * from "./UserOAuthSourceConnectionRequest";
export * from "./UserPasswordHashSetRequest";
export * from "./UserPasswordSetRequest";
export * from "./UserPath";
export * from "./UserPlexSourceConnection";

View File

@@ -66,7 +66,7 @@ dependencies = [
"ua-parser==1.0.2",
"unidecode==1.4.0",
"urllib3<3",
"uvicorn[standard]==0.45.0",
"uvicorn[standard]==0.44.0",
"watchdog==6.0.0",
"webauthn==2.7.1",
"wsproto==1.3.2",
@@ -97,13 +97,13 @@ dev = [
"pytest-django==4.12.0",
"pytest-flakefinder==1.1.0",
"pytest-github-actions-annotate-failures==0.4.0",
"pytest-randomly==4.1.0",
"pytest-randomly==4.0.1",
"pytest-timeout==2.4.0",
"pytest==9.0.3",
"requests-mock==1.12.1",
"ruff==0.15.12",
"ruff==0.15.11",
"selenium==4.43.0",
"types-channels==4.3.0.20260421",
"types-channels==4.3.0.20260408",
"types-docker==7.1.0.20260409",
"types-jwcrypto==1.5.7.20260409",
"types-ldap3==2.9.13.20260408",

View File

@@ -4522,41 +4522,6 @@ paths:
description: Bad request
'403':
$ref: '#/components/responses/GenericErrorResponse'
/core/users/{id}/set_password_hash/:
post:
operationId: core_users_set_password_hash_create
description: |-
Set a user's password from a pre-hashed Django password value.
Submit the Django password hash in the shared ``password`` request field.
This updates authentik's local password verifier only. It does not attempt
to propagate the password change to LDAP or Kerberos because no raw password
is available from the request payload.
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this User.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserPasswordHashSetRequest'
required: true
security:
- authentik: []
responses:
'204':
description: Successfully changed password
'400':
description: Bad request
'403':
$ref: '#/components/responses/GenericErrorResponse'
/core/users/{id}/used_by/:
get:
operationId: core_users_used_by_list
@@ -18954,7 +18919,7 @@ paths:
schema:
type: boolean
- in: query
name: issuer_override
name: issuer
schema:
type: string
- in: query
@@ -20113,7 +20078,7 @@ paths:
schema:
type: boolean
- in: query
name: issuer_override
name: issuer
schema:
type: string
- in: query
@@ -34146,9 +34111,6 @@ components:
$ref: '#/components/schemas/PolicyEngineMode'
group:
type: string
meta_hide:
type: boolean
description: Hide this application from the user's My applications page.
required:
- backchannel_providers_obj
- launch_url
@@ -34230,9 +34192,6 @@ components:
$ref: '#/components/schemas/PolicyEngineMode'
group:
type: string
meta_hide:
type: boolean
description: Hide this application from the user's My applications page.
required:
- name
- slug
@@ -47469,9 +47428,6 @@ components:
$ref: '#/components/schemas/PolicyEngineMode'
group:
type: string
meta_hide:
type: boolean
description: Hide this application from the user's My applications page.
PatchedAuthenticatorDuoStageRequest:
type: object
description: AuthenticatorDuoStage Serializer
@@ -50237,10 +50193,10 @@ components:
type: string
description: Value of the audience restriction field of the assertion. When
left empty, no audience restriction will be added.
issuer_override:
issuer:
type: string
description: Also known as EntityID. Providing a value overrides the default
issuer generated by authentik.
minLength: 1
description: Also known as EntityID
assertion_valid_not_before:
type: string
minLength: 1
@@ -53759,10 +53715,9 @@ components:
type: string
description: Value of the audience restriction field of the assertion. When
left empty, no audience restriction will be added.
issuer_override:
issuer:
type: string
description: Also known as EntityID. Providing a value overrides the default
issuer generated by authentik.
description: Also known as EntityID
assertion_valid_not_before:
type: string
description: 'Assertion valid not before current time + this value (Format:
@@ -53852,10 +53807,6 @@ components:
type: string
description: Get metadata download URL
readOnly: true
url_issuer:
type: string
description: Get Issuer/EntityID URL
readOnly: true
url_sso_post:
type: string
description: Get SSO Post URL
@@ -53889,7 +53840,6 @@ components:
- name
- pk
- url_download_metadata
- url_issuer
- url_slo_post
- url_slo_redirect
- url_sso_init
@@ -53957,10 +53907,10 @@ components:
type: string
description: Value of the audience restriction field of the assertion. When
left empty, no audience restriction will be added.
issuer_override:
issuer:
type: string
description: Also known as EntityID. Providing a value overrides the default
issuer generated by authentik.
minLength: 1
description: Also known as EntityID
assertion_valid_not_before:
type: string
minLength: 1
@@ -57623,15 +57573,6 @@ components:
- identifier
- source
- user
UserPasswordHashSetRequest:
type: object
description: Payload to set a users' password hash directly
properties:
password:
type: string
minLength: 1
required:
- password
UserPasswordSetRequest:
type: object
description: Payload to set a users' password directly

View File

@@ -39,7 +39,7 @@ class TestProviderSAML(SeleniumTestCase):
"9009": "9009",
},
environment={
"SP_ENTITY_ID": provider.issuer_override,
"SP_ENTITY_ID": provider.issuer,
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"SP_METADATA_URL": metadata_url,
**kwargs,
@@ -68,7 +68,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
@@ -147,7 +147,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
@@ -226,7 +226,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
@@ -321,7 +321,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
@@ -415,7 +415,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
@@ -503,7 +503,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
@@ -553,7 +553,7 @@ class TestProviderSAML(SeleniumTestCase):
name=generate_id(),
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer_override="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
invalidation_flow=invalidation_flow,

889
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -77,8 +77,6 @@ export class FormFixture extends PageFixture {
/**
* Search for a row containing the given text.
*
* @returns A locator for the row entry matching the query.
*/
public search = async (
query: string,

View File

@@ -1,8 +1,6 @@
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
import { PageFixture, PageFixtureInit } from "#e2e/fixtures/PageFixture";
import { expect, Page } from "@playwright/test";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
@@ -13,8 +11,6 @@ export interface LoginInit {
username?: string;
password?: string;
to?: URL | string;
rememberMe?: boolean;
page?: Page;
}
export interface SessionFixtureInit extends PageFixtureInit {
@@ -40,10 +36,6 @@ export class SessionFixture extends PageFixture {
public $passwordStage = this.page.locator("ak-stage-password");
public $passwordField = this.page.getByLabel("Password");
public $rememberMeCheckbox = this.page.getByRole("checkbox", {
name: "Remember me on this device",
});
/**
* The button to submit the the login flow,
* typically redirecting to the authenticated interface.
@@ -74,45 +66,19 @@ export class SessionFixture extends PageFixture {
/**
* Log into the application.
*/
public async login(
{
username = GOOD_USERNAME,
password = GOOD_PASSWORD,
to = SessionFixture.pathname,
rememberMe,
}: LoginInit = {},
page = this.page,
): Promise<void> {
public async login({
username = GOOD_USERNAME,
password = GOOD_PASSWORD,
to = SessionFixture.pathname,
}: LoginInit = {}) {
this.logger.info("Logging in...");
const initialURL = new URL(page.url());
const initialURL = new URL(this.page.url());
if (initialURL.pathname === SessionFixture.pathname) {
this.logger.info("Skipping navigation because we're already in a authentication flow");
} else {
await page.goto(to.toString());
}
if (typeof rememberMe === "boolean") {
const rememberMeCheckboxVisible = await this.$rememberMeCheckbox.isVisible();
if (rememberMeCheckboxVisible) {
if (rememberMe) {
await this.$rememberMeCheckbox.check();
await expect(
this.$rememberMeCheckbox,
"Remember me checkbox is checked",
).toBeChecked();
} else {
await this.$rememberMeCheckbox.uncheck();
await expect(
this.$rememberMeCheckbox,
"Remember me checkbox is unchecked",
).not.toBeChecked();
}
}
await this.page.goto(to.toString());
}
await this.$usernameField.fill(username);
@@ -136,7 +102,7 @@ export class SessionFixture extends PageFixture {
//#region Navigation
public async toLoginPage(page: Page = this.page) {
await page.goto(SessionFixture.pathname);
public async toLoginPage() {
await this.page.goto(SessionFixture.pathname);
}
}

225
web/package-lock.json generated
View File

@@ -78,7 +78,7 @@
"globals": "^17.5.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"knip": "^6.6.0",
"knip": "^6.4.1",
"lex": "^2025.11.0",
"lit": "^3.3.2",
"lit-analyzer": "^2.0.3",
@@ -2141,9 +2141,9 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -2224,9 +2224,9 @@
}
},
"node_modules/@oxc-parser/binding-android-arm-eabi": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.126.0.tgz",
"integrity": "sha512-svyoHt25J4741QJ5aa4R+h0iiBeSRt63Lr3aAZcxy2c/NeSE1IfDeMnSij6rIg7EjxkdlXzz613wUjeCeilBNA==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.121.0.tgz",
"integrity": "sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==",
"cpu": [
"arm"
],
@@ -2240,9 +2240,9 @@
}
},
"node_modules/@oxc-parser/binding-android-arm64": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.126.0.tgz",
"integrity": "sha512-hPEBRKgplp1mG9GkINFsr4JVMDNrGJLOqfDaadTWpAoTnzYR5Rmv8RMvB3hJZpiNvbk1aacopdHUP1pggMQ/cw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.121.0.tgz",
"integrity": "sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==",
"cpu": [
"arm64"
],
@@ -2256,9 +2256,9 @@
}
},
"node_modules/@oxc-parser/binding-darwin-arm64": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.126.0.tgz",
"integrity": "sha512-ccRpu9sdYmznePJQG5halhs0FW5tw5a8zRSoZXOzM1OjoeZ4jiRRruFiPclsD59edoVAK1l83dvfjWz1nQi6lg==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.121.0.tgz",
"integrity": "sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==",
"cpu": [
"arm64"
],
@@ -2272,9 +2272,9 @@
}
},
"node_modules/@oxc-parser/binding-darwin-x64": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.126.0.tgz",
"integrity": "sha512-CHB4zVjNSKqx8Fw9pHowzQQnjjuq04i4Ng0Avj+DixlwhwAoMYqlFbocYIlbg+q3zOLGlm7vEHm83jqEMitnyg==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.121.0.tgz",
"integrity": "sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==",
"cpu": [
"x64"
],
@@ -2288,9 +2288,9 @@
}
},
"node_modules/@oxc-parser/binding-freebsd-x64": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.126.0.tgz",
"integrity": "sha512-RQ3nEJdcDKBfBjmLJ3Vl1d0KQERPV1P8eUrnBm7+VTYyoaJSPLVFuPg1mlD1hk3n0/879VLFMfusFkBal4ssWQ==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.121.0.tgz",
"integrity": "sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==",
"cpu": [
"x64"
],
@@ -2304,9 +2304,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.126.0.tgz",
"integrity": "sha512-onipc2wCDA7Bauzb4KK1mab0GsEDf4ujiIfWECdnmY/2LlzAoX3xdQRLAUyEDB1kn3yilHBrkmXDdHluyHXxiw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.121.0.tgz",
"integrity": "sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==",
"cpu": [
"arm"
],
@@ -2320,9 +2320,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.126.0.tgz",
"integrity": "sha512-5BuJJPohrV5NJ8lmcYOMbfRCUGoYH5J9HZHeuqOLwkHXWAuPMN3X1h8bC/2mWjmosdbfTtmyIdX3spS/TkqKNg==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.121.0.tgz",
"integrity": "sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==",
"cpu": [
"arm"
],
@@ -2336,9 +2336,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.126.0.tgz",
"integrity": "sha512-r2KApRgm2pOJaduRm6GOT8x0whcr67AyejNkSdzPt34GJ+Y3axcXN2mwlTs+8lfO/SSmpO5ZJGYiHYnxEE0jkw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.121.0.tgz",
"integrity": "sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==",
"cpu": [
"arm64"
],
@@ -2352,9 +2352,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.126.0.tgz",
"integrity": "sha512-FQ+MMh7MT0Dr/u8+RWmWKlfoeWPQyHDbhhxJShJlYtROXXPHsRs9EvmQOZZ3sx4Nn7JU8NX+oyw2YzQ7anBJcA==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.121.0.tgz",
"integrity": "sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==",
"cpu": [
"arm64"
],
@@ -2368,9 +2368,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.126.0.tgz",
"integrity": "sha512-Wv/T8C98hRQhGTlx2XFyLn5raRMp9U1lOQD+YnXNgAr7wHbJJpZ8mDBU7Rw+M3WytGcGTFcr6kqgfyQeHVtLbQ==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.121.0.tgz",
"integrity": "sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==",
"cpu": [
"ppc64"
],
@@ -2384,9 +2384,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.126.0.tgz",
"integrity": "sha512-DHx1rT1zauW0ZbLHOiQh5AC9Xs3UkWx2XmfZHs+7nnWYr3sagrufoUQC+/XPwwjMIlCFXiFGM0sFh3TyOCZwqA==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.121.0.tgz",
"integrity": "sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==",
"cpu": [
"riscv64"
],
@@ -2400,9 +2400,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.126.0.tgz",
"integrity": "sha512-umDc2mTShH0U2zcEYf8mIJ163seLJNn54ZUZYeI5jD4qlg9izPwoLrC2aNPKlMJTu6u/ysmQWiEvIiaAG+INkw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.121.0.tgz",
"integrity": "sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==",
"cpu": [
"riscv64"
],
@@ -2416,9 +2416,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.126.0.tgz",
"integrity": "sha512-PXXeWayclRtO1pxQEeCpiqIglQdhK2mAI2VX5xnsWdImzSB5GpoQ8TNw7vTCKk2k+GZuxl+q1knncidjCyUP9w==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.121.0.tgz",
"integrity": "sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==",
"cpu": [
"s390x"
],
@@ -2432,9 +2432,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.126.0.tgz",
"integrity": "sha512-wzocjxm34TbB3bFlqG65JiLtvf6ZDg2ZxRkLLbgXwDQUNU+0MPjQN8zy/0jBKNA5fnPLk3XeVdZ7Uin+7+CVkg==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.121.0.tgz",
"integrity": "sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==",
"cpu": [
"x64"
],
@@ -2448,9 +2448,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-x64-musl": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.126.0.tgz",
"integrity": "sha512-e83uftP60jmkPs2+CW6T6A1GYzN2H6IumDAiTntv9WyHR73PI3ImHNBkYqnA3ukeKI3xjcCbhSh9QeJWmufxGQ==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.121.0.tgz",
"integrity": "sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==",
"cpu": [
"x64"
],
@@ -2464,9 +2464,9 @@
}
},
"node_modules/@oxc-parser/binding-openharmony-arm64": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.126.0.tgz",
"integrity": "sha512-4WiOILHnPrTDY2/L4mE6PZCYwLN1d3ghma6BuTJ452CCgzRMt3uFplCtR+o3r9zdUWJYb370UizpI9CUcWXr1A==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.121.0.tgz",
"integrity": "sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==",
"cpu": [
"arm64"
],
@@ -2480,27 +2480,25 @@
}
},
"node_modules/@oxc-parser/binding-wasm32-wasi": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.126.0.tgz",
"integrity": "sha512-Y17hhnrQTrxgAxAyAq401vnN9URsAL4s5AjqpG1NDsXSlhe1yBNnns+rC2P6xcMoitgX5nKH2ryYt9oiFRlzLw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.121.0.tgz",
"integrity": "sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.4"
"@napi-rs/wasm-runtime": "^1.1.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.126.0.tgz",
"integrity": "sha512-Znug1u1iRvT4VC3jANz6nhGBHsFwEFMxuimYpJFwMtsB6H5FcEoZRMmH26tHkSTD03JvDmG+gB65W3ajLjPcSw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.121.0.tgz",
"integrity": "sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==",
"cpu": [
"arm64"
],
@@ -2514,9 +2512,9 @@
}
},
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.126.0.tgz",
"integrity": "sha512-qrw7mx5hFFTxVSXToOA40hpnjgNB/DJprZchtB4rDKNLKqkD3F26HbzaQeH1nxAKej0efSZfJd5Sw3qdtOLGhw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.121.0.tgz",
"integrity": "sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==",
"cpu": [
"ia32"
],
@@ -2530,9 +2528,9 @@
}
},
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.126.0.tgz",
"integrity": "sha512-ibB1s+mPUFXvS7MFJO2jpw/aCNs/P6ifnWlRyTYB+WYBpniOiCcHQQskZneJtwcjQMDRol3RGG3ihoYnzXSY4w==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.121.0.tgz",
"integrity": "sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==",
"cpu": [
"x64"
],
@@ -2546,9 +2544,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.121.0.tgz",
"integrity": "sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
@@ -10006,9 +10004,9 @@
}
},
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
"version": "4.13.7",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -11569,9 +11567,9 @@
}
},
"node_modules/knip": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/knip/-/knip-6.6.0.tgz",
"integrity": "sha512-IT1YDiHyRctYYsuZNBd/ZiGoa7HmCaxs+ZrWxCfYjQKPG6QyRqMfkteqC+rBuMymBJeLXyBnRa7hn95O+sGG8Q==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/knip/-/knip-6.4.1.tgz",
"integrity": "sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw==",
"funding": [
{
"type": "github",
@@ -11584,17 +11582,18 @@
],
"license": "ISC",
"dependencies": {
"fdir": "^6.5.0",
"@nodelib/fs.walk": "^1.2.3",
"fast-glob": "^3.3.3",
"formatly": "^0.3.0",
"get-tsconfig": "4.14.0",
"get-tsconfig": "4.13.7",
"jiti": "^2.6.0",
"minimist": "^1.2.8",
"oxc-parser": "^0.126.0",
"oxc-parser": "^0.121.0",
"oxc-resolver": "^11.19.1",
"picomatch": "^4.0.4",
"picocolors": "^1.1.1",
"picomatch": "^4.0.1",
"smol-toml": "^1.6.1",
"strip-json-comments": "5.0.3",
"tinyglobby": "^0.2.16",
"unbash": "^2.2.0",
"yaml": "^2.8.2",
"zod": "^4.1.11"
@@ -11619,22 +11618,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/knip/node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/langium": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz",
@@ -14283,12 +14266,12 @@
}
},
"node_modules/oxc-parser": {
"version": "0.126.0",
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.126.0.tgz",
"integrity": "sha512-FktCvLby/mOHyuijZt22+nOt10dS24gGUZE3XwIbUg7Kf4+rer3/5T7RgwzazlNuVsCjPloZ3p8E+4ONT3A8Kw==",
"version": "0.121.0",
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.121.0.tgz",
"integrity": "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "^0.126.0"
"@oxc-project/types": "^0.121.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -14297,26 +14280,26 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxc-parser/binding-android-arm-eabi": "0.126.0",
"@oxc-parser/binding-android-arm64": "0.126.0",
"@oxc-parser/binding-darwin-arm64": "0.126.0",
"@oxc-parser/binding-darwin-x64": "0.126.0",
"@oxc-parser/binding-freebsd-x64": "0.126.0",
"@oxc-parser/binding-linux-arm-gnueabihf": "0.126.0",
"@oxc-parser/binding-linux-arm-musleabihf": "0.126.0",
"@oxc-parser/binding-linux-arm64-gnu": "0.126.0",
"@oxc-parser/binding-linux-arm64-musl": "0.126.0",
"@oxc-parser/binding-linux-ppc64-gnu": "0.126.0",
"@oxc-parser/binding-linux-riscv64-gnu": "0.126.0",
"@oxc-parser/binding-linux-riscv64-musl": "0.126.0",
"@oxc-parser/binding-linux-s390x-gnu": "0.126.0",
"@oxc-parser/binding-linux-x64-gnu": "0.126.0",
"@oxc-parser/binding-linux-x64-musl": "0.126.0",
"@oxc-parser/binding-openharmony-arm64": "0.126.0",
"@oxc-parser/binding-wasm32-wasi": "0.126.0",
"@oxc-parser/binding-win32-arm64-msvc": "0.126.0",
"@oxc-parser/binding-win32-ia32-msvc": "0.126.0",
"@oxc-parser/binding-win32-x64-msvc": "0.126.0"
"@oxc-parser/binding-android-arm-eabi": "0.121.0",
"@oxc-parser/binding-android-arm64": "0.121.0",
"@oxc-parser/binding-darwin-arm64": "0.121.0",
"@oxc-parser/binding-darwin-x64": "0.121.0",
"@oxc-parser/binding-freebsd-x64": "0.121.0",
"@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0",
"@oxc-parser/binding-linux-arm-musleabihf": "0.121.0",
"@oxc-parser/binding-linux-arm64-gnu": "0.121.0",
"@oxc-parser/binding-linux-arm64-musl": "0.121.0",
"@oxc-parser/binding-linux-ppc64-gnu": "0.121.0",
"@oxc-parser/binding-linux-riscv64-gnu": "0.121.0",
"@oxc-parser/binding-linux-riscv64-musl": "0.121.0",
"@oxc-parser/binding-linux-s390x-gnu": "0.121.0",
"@oxc-parser/binding-linux-x64-gnu": "0.121.0",
"@oxc-parser/binding-linux-x64-musl": "0.121.0",
"@oxc-parser/binding-openharmony-arm64": "0.121.0",
"@oxc-parser/binding-wasm32-wasi": "0.121.0",
"@oxc-parser/binding-win32-arm64-msvc": "0.121.0",
"@oxc-parser/binding-win32-ia32-msvc": "0.121.0",
"@oxc-parser/binding-win32-x64-msvc": "0.121.0"
}
},
"node_modules/oxc-resolver": {

View File

@@ -154,7 +154,7 @@
"globals": "^17.5.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"knip": "^6.6.0",
"knip": "^6.4.1",
"lex": "^2025.11.0",
"lit": "^3.3.2",
"lit-analyzer": "^2.0.3",

View File

@@ -201,15 +201,6 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
)}
>
</ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${this.instance?.metaHide ?? false}
label=${msg("Hide from My applications")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
)}
>
</ak-switch-input>
<ak-file-search-input
name="metaIcon"
label=${msg("Icon")}

View File

@@ -42,7 +42,7 @@ const renderSAMLOverview: ProviderOverview<SAMLProvider> = (provider) => {
return renderSummary("SAML", provider.name, [
[msg("ACS URL"), provider.acsUrl],
[msg("Audience"), provider.audience || "-"],
[msg("Issuer"), provider.urlIssuer],
[msg("Issuer"), provider.issuer],
]);
};

View File

@@ -187,15 +187,6 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
)}
>
</ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${app.metaHide ?? false}
label=${msg("Hide from My applications")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
)}
>
</ak-switch-input>
<ak-file-search-input
name="metaIcon"
label=${msg("Icon")}

View File

@@ -38,20 +38,22 @@ export interface SelectedFeatureEventDetail {
@customElement("ak-map")
export class Map extends OlMap {
public styles: CSSResult[] = [
OlMap.styles,
OL,
css`
:host {
display: block;
}
#map {
height: 100%;
}
`,
];
public render() {
return html`
<style>
${OL}
</style>
<style>
:host {
display: block;
}
#map {
height: 100%;
}
</style>
<div id="map"></div>
<slot></slot>
`;

View File

@@ -3,7 +3,6 @@ import "#elements/forms/ModalForm";
import "#elements/sync/SyncObjectForm";
import { DEFAULT_CONFIG } from "#common/api/config";
import { formatUserDisplayName } from "#common/users";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
@@ -76,7 +75,7 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
}
protected override rowLabel(item: GoogleWorkspaceProviderUser): string {
return formatUserDisplayName(item.userObj);
return item.userObj.name || item.userObj.username;
}
protected columns: TableColumn[] = [

View File

@@ -3,7 +3,6 @@ import "#elements/forms/ModalForm";
import "#elements/sync/SyncObjectForm";
import { DEFAULT_CONFIG } from "#common/api/config";
import { formatUserDisplayName } from "#common/users";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
@@ -76,7 +75,7 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
}
protected override rowLabel(item: MicrosoftEntraProviderUser): string {
return formatUserDisplayName(item.userObj);
return item.userObj.name || item.userObj.username;
}
protected columns: TableColumn[] = [

View File

@@ -196,6 +196,15 @@ export function renderForm({
required
.errorMessages=${errors.acsUrl}
></ak-text-input>
<ak-text-input
label=${msg("Issuer")}
input-hint="code"
name="issuer"
value="${provider.issuer || "authentik"}"
required
.errorMessages=${errors.issuer}
help=${msg("Also known as Entity ID.")}
></ak-text-input>
<ak-text-input
name="audience"
label=${msg("Audience")}
@@ -427,15 +436,6 @@ export function renderForm({
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
></ak-text-input>
<ak-text-input
label=${msg("EntityID/Issuer override")}
name="issuerOverride"
value="${ifDefined(provider.issuerOverride ?? undefined)}"
.errorMessages=${errors.issuerOverride}
help=${msg(
"Sets a custom EntityID/Issuer to override the authentik generated default.",
)}
></ak-text-input>
<ak-radio-input
label=${msg("Service Provider Binding")}
name="spBinding"

View File

@@ -350,7 +350,7 @@ export class SAMLProviderViewPage extends AKElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.issuerOverride}
${this.provider.issuer}
</div>
</dd>
</div>
@@ -385,7 +385,7 @@ export class SAMLProviderViewPage extends AKElement {
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider?.urlIssuer)}"
value="${ifDefined(this.provider?.issuer)}"
/>
</div>
<div class="pf-c-form__group">

View File

@@ -4,7 +4,6 @@ import "#elements/sync/SyncObjectForm";
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
import { DEFAULT_CONFIG } from "#common/api/config";
import { formatUserDisplayName } from "#common/users";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
@@ -74,7 +73,7 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
}
protected override rowLabel(item: SCIMProviderUser): string {
return formatUserDisplayName(item.userObj);
return item.userObj.name || item.userObj.username;
}
protected columns: TableColumn[] = [

View File

@@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "#common/api/config";
import { formatUserDisplayName } from "#common/users";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
@@ -26,7 +25,7 @@ export class SCIMSourceUserList extends Table<SCIMSourceUser> {
}
protected override rowLabel(item: SCIMSourceUser): string {
return formatUserDisplayName(item.userObj);
return item.userObj.name || item.userObj.username;
}
protected columns: TableColumn[] = [

View File

@@ -2,30 +2,20 @@ import "#elements/buttons/SpinnerButton/index";
import "#elements/forms/HorizontalFormElement";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { Form } from "#elements/forms/Form";
import { ifPresent } from "#elements/utils/attributes";
import { FocusTarget } from "#elements/utils/focus";
import { AKLabel } from "#components/ak-label";
import { CoreApi, UserPasswordSetRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-user-password-form")
export class UserPasswordForm extends Form<UserPasswordSetRequest> {
public static shadowRootOptions: ShadowRootInit = {
...Form.shadowRootOptions,
delegatesFocus: true,
};
public static override verboseName = msg("Password");
public static override verboseNamePlural = msg("Passwords");
public static override submittingVerb = msg("Setting");
public override submitLabel = msg("Set Password");
protected autofocusTarget = new FocusTarget<HTMLInputElement>();
@@ -33,9 +23,6 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
//#region Properties
public override submitLabel = msg("Set Password");
public override successMessage = msg("Successfully updated password.");
@property({ type: Number })
public instancePk?: number;
@@ -43,15 +30,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
public label = msg("New Password");
@property({ type: String })
public placeholder = msg("Type a new password...");
public placeholder = msg("New Password");
@property({ type: String, useDefault: true })
public username: string | null = null;
@property({ type: String })
public username?: string;
@property({ type: String, useDefault: true })
public email: string | null = null;
public override size = PFSize.Medium;
@property({ type: String })
public email?: string;
/**
* The autocomplete attribute to use for the password field.
@@ -65,15 +50,17 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
//#endregion
public override getSuccessMessage(): string {
return msg("Successfully updated password.");
}
public override connectedCallback(): void {
super.connectedCallback();
this.addEventListener("focus", this.autofocusTarget.toEventListener());
}
public override firstUpdated(): void {
requestAnimationFrame(() => {
this.focus();
});
this.focus();
}
protected override async send(data: UserPasswordSetRequest): Promise<void> {
@@ -107,26 +94,17 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
/>`
: nothing}
<ak-form-element-horizontal required name="password">
${AKLabel(
{
slot: "label",
className: "pf-c-form__group-label",
htmlFor: "password",
required: true,
},
this.label,
)}
<ak-form-element-horizontal label=${this.label} required name="password">
<input
autofocus
${this.autofocusTarget.toRef()}
id="password"
type="password"
value=""
class="pf-c-form-control"
required
placeholder=${ifPresent(this.placeholder || this.label)}
autocomplete=${ifPresent(this.autocomplete)}
placeholder=${ifDefined(this.placeholder || this.label)}
aria-label=${this.label}
autocomplete=${ifDefined(this.autocomplete)}
/>
</ak-form-element-horizontal>`;
}

View File

@@ -1,5 +1,3 @@
import { formatUserDisplayName } from "#common/users";
import { modalInvoker } from "#elements/dialogs";
import { LitFC } from "#elements/types";
@@ -54,7 +52,7 @@ export const RecoveryButtons: LitFC<RecoveryButtonsProps> = ({
class="pf-c-button pf-m-secondary ${buttonClasses || ""}"
type="button"
${modalInvoker(UserPasswordForm, {
headline: msg(str`Update ${formatUserDisplayName(user)}'s password`),
headline: msg(str`Update ${user.name || user.username}'s password`),
username: user.username,
email: user.email,
instancePk: user.pk,

Some files were not shown because too many files have changed in this diff Show More