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
96 changed files with 812 additions and 18943 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

1
Cargo.lock generated
View File

@@ -198,7 +198,6 @@ dependencies = [
"metrics-exporter-prometheus",
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"sqlx",
"tokio",
"tracing",

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",
@@ -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
@@ -132,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(
@@ -169,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:
@@ -550,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(

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

@@ -735,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

@@ -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,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,13 +55,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"""SAMLProvider Serializer"""
url_download_metadata = SerializerMethodField()
url_issuer = SerializerMethodField()
# Unified SAML endpoint (primary)
url_unified = SerializerMethodField()
url_unified_init = SerializerMethodField()
# Legacy endpoints (for backward compatibility)
url_sso_post = SerializerMethodField()
url_sso_redirect = SerializerMethodField()
url_sso_init = SerializerMethodField()
@@ -95,53 +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_unified(self, instance: SAMLProvider) -> str:
"""Get unified SAML endpoint URL (handles SSO and SLO)"""
if "request" not in self._context:
return ""
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 "-"
def get_url_unified_init(self, instance: SAMLProvider) -> str:
"""Get IdP-initiated SAML URL"""
if "request" not in self._context:
return ""
request: HttpRequest = self._context["request"]._request
try:
return request.build_absolute_uri(
reverse(
"authentik_providers_saml:init",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return "-"
def get_url_sso_post(self, instance: SAMLProvider) -> str:
"""Get SSO Post URL"""
if "request" not in self._context:
@@ -255,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",
@@ -277,9 +220,6 @@ class SAMLProviderSerializer(ProviderSerializer):
"default_relay_state",
"default_name_id_policy",
"url_download_metadata",
"url_issuer",
"url_unified",
"url_unified_init",
"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"))],
@@ -241,7 +234,7 @@ class SAMLProvider(Provider):
"""Use IDP-Initiated SAML flow as launch URL"""
try:
return reverse(
"authentik_providers_saml:init",
"authentik_providers_saml:sso-init",
kwargs={"application_slug": self.application.slug},
)
except Provider.application.RelatedObjectDoesNotExist:
@@ -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"""
@@ -81,35 +68,54 @@ class MetadataProcessor:
element.text = name_id_format
yield element
def _get_unified_url(self) -> str:
"""Get the unified SAML endpoint URL"""
return self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:base",
kwargs={"application_slug": self.provider.application.slug},
)
)
def get_sso_bindings(self) -> Iterator[Element]:
"""Get all SSO Bindings - both point to unified endpoint"""
unified_url = self._get_unified_url()
for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]:
"""Get all Bindings supported"""
binding_url_map = {
(SAML_BINDING_REDIRECT, "SingleSignOnService"): self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:sso-redirect",
kwargs={"application_slug": self.provider.application.slug},
)
),
(SAML_BINDING_POST, "SingleSignOnService"): self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:sso-post",
kwargs={"application_slug": self.provider.application.slug},
)
),
}
for binding_svc, url in binding_url_map.items():
binding, svc = binding_svc
if self.force_binding and self.force_binding != binding:
continue
element = Element(f"{{{NS_SAML_METADATA}}}SingleSignOnService")
element = Element(f"{{{NS_SAML_METADATA}}}{svc}")
element.attrib["Binding"] = binding
element.attrib["Location"] = unified_url
element.attrib["Location"] = url
yield element
def get_slo_bindings(self) -> Iterator[Element]:
"""Get all SLO Bindings - both point to unified endpoint"""
unified_url = self._get_unified_url()
for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]:
"""Get all Bindings supported"""
binding_url_map = {
(SAML_BINDING_REDIRECT, "SingleLogoutService"): self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:slo-redirect",
kwargs={"application_slug": self.provider.application.slug},
)
),
(SAML_BINDING_POST, "SingleLogoutService"): self.http_request.build_absolute_uri(
reverse(
"authentik_providers_saml:slo-post",
kwargs={"application_slug": self.provider.application.slug},
)
),
}
for binding_svc, url in binding_url_map.items():
binding, svc = binding_svc
if self.force_binding and self.force_binding != binding:
continue
element = Element(f"{{{NS_SAML_METADATA}}}SingleLogoutService")
element = Element(f"{{{NS_SAML_METADATA}}}{svc}")
element.attrib["Binding"] = binding
element.attrib["Location"] = unified_url
element.attrib["Location"] = url
yield element
def _prepare_signature(self, entity_descriptor: _Element):
@@ -183,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

@@ -4,26 +4,13 @@ from django.urls import path
from authentik.providers.saml.api.property_mappings import SAMLPropertyMappingViewSet
from authentik.providers.saml.api.providers import SAMLProviderViewSet
from authentik.providers.saml.views import metadata, sso, unified
from authentik.providers.saml.views import metadata, sso
from authentik.providers.saml.views.sp_slo import (
SPInitiatedSLOBindingPOSTView,
SPInitiatedSLOBindingRedirectView,
)
urlpatterns = [
# Unified Endpoint - handles SSO and SLO based on message type
path(
"<slug:application_slug>/",
unified.SAMLUnifiedView.as_view(),
name="base",
),
# IdP-initiated
path(
"<slug:application_slug>/init/",
sso.SAMLSSOBindingInitView.as_view(),
name="init",
),
# LEGACY Endpoints (backward compatibility)
# 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

@@ -1,118 +0,0 @@
"""Unified SAML endpoint - handles SSO and SLO based on message type"""
from base64 import b64decode
from defusedxml.lxml import fromstring
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.common.saml.constants import NS_MAP
from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import decode_base64_and_inflate
from authentik.providers.saml.views.flows import (
REQUEST_KEY_SAML_REQUEST,
REQUEST_KEY_SAML_RESPONSE,
)
from authentik.providers.saml.views.sp_slo import (
SPInitiatedSLOBindingPOSTView,
SPInitiatedSLOBindingRedirectView,
)
from authentik.providers.saml.views.sso import (
SAMLSSOBindingPOSTView,
SAMLSSOBindingRedirectView,
)
LOGGER = get_logger()
# SAML message type constants
SAML_MESSAGE_TYPE_AUTHN_REQUEST = "AuthnRequest"
SAML_MESSAGE_TYPE_LOGOUT_REQUEST = "LogoutRequest"
def detect_saml_message_type(saml_request: str, is_post_binding: bool) -> str | None:
"""Parse SAML request to determine if AuthnRequest or LogoutRequest."""
try:
if is_post_binding:
decoded_xml = b64decode(saml_request.encode())
else:
decoded_xml = decode_base64_and_inflate(saml_request)
root = fromstring(decoded_xml)
if len(root.xpath("//samlp:AuthnRequest", namespaces=NS_MAP)):
return SAML_MESSAGE_TYPE_AUTHN_REQUEST
if len(root.xpath("//samlp:LogoutRequest", namespaces=NS_MAP)):
return SAML_MESSAGE_TYPE_LOGOUT_REQUEST
return None
except Exception: # noqa: BLE001
return None
@method_decorator(xframe_options_sameorigin, name="dispatch")
@method_decorator(csrf_exempt, name="dispatch")
class SAMLUnifiedView(View):
"""Unified SAML endpoint - handles SSO and SLO based on message type.
The operation type is determined by parsing
the incoming SAML message:
- AuthnRequest -> SSO flow (delegates to SAMLSSOBindingRedirectView/POSTView)
- LogoutRequest -> SLO flow (delegates to SPInitiatedSLOBindingRedirectView/POSTView)
- LogoutResponse -> SLO completion (delegates to SPInitiatedSLOBindingRedirectView/POSTView)
"""
def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""Route the request based on SAML message type."""
# ak user was not logged in, redirected to login, and is back w POST payload in session
if SESSION_KEY_POST in request.session:
return self._delegate_to_sso(request, application_slug, is_post_binding=True)
# Determine binding from HTTP method
is_post_binding = request.method == "POST"
data = request.POST if is_post_binding else request.GET
# LogoutResponse - delegate to SLO view (handles it in dispatch)
if REQUEST_KEY_SAML_RESPONSE in data:
return self._delegate_to_slo(request, application_slug, is_post_binding)
# Check for SAML request
if REQUEST_KEY_SAML_REQUEST not in data:
LOGGER.info("SAML payload missing")
return bad_request_message(request, "The SAML request payload is missing.")
# Detect message type and delegate
saml_request = data[REQUEST_KEY_SAML_REQUEST]
message_type = detect_saml_message_type(saml_request, is_post_binding)
if message_type == SAML_MESSAGE_TYPE_AUTHN_REQUEST:
return self._delegate_to_sso(request, application_slug, is_post_binding)
elif message_type == SAML_MESSAGE_TYPE_LOGOUT_REQUEST:
return self._delegate_to_slo(request, application_slug, is_post_binding)
else:
LOGGER.warning("Unknown SAML message type", message_type=message_type)
return bad_request_message(
request, f"Unsupported SAML message type: {message_type or 'unknown'}"
)
def _delegate_to_sso(
self, request: HttpRequest, application_slug: str, is_post_binding: bool
) -> HttpResponse:
"""Delegate to the appropriate SSO view."""
if is_post_binding:
view = SAMLSSOBindingPOSTView.as_view()
else:
view = SAMLSSOBindingRedirectView.as_view()
return view(request, application_slug=application_slug)
def _delegate_to_slo(
self, request: HttpRequest, application_slug: str, is_post_binding: bool
) -> HttpResponse:
"""Delegate to the appropriate SLO view."""
if is_post_binding:
view = SPInitiatedSLOBindingPOSTView.as_view()
else:
view = SPInitiatedSLOBindingRedirectView.as_view()
return view(request, application_slug=application_slug)

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,
@@ -10817,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

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

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-28 00:30+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"
@@ -2717,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 ""

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

@@ -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,24 +260,6 @@ export interface SAMLProvider {
* @memberof SAMLProvider
*/
readonly urlDownloadMetadata: string;
/**
* Get Issuer/EntityID URL
* @type {string}
* @memberof SAMLProvider
*/
readonly urlIssuer: string;
/**
* Get unified SAML endpoint URL (handles SSO and SLO)
* @type {string}
* @memberof SAMLProvider
*/
readonly urlUnified: string;
/**
* Get IdP-initiated SAML URL
* @type {string}
* @memberof SAMLProvider
*/
readonly urlUnifiedInit: string;
/**
* Get SSO Post URL
* @type {string}
@@ -339,9 +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 (!("urlUnified" in value) || value["urlUnified"] === undefined) return false;
if (!("urlUnifiedInit" in value) || value["urlUnifiedInit"] === 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;
@@ -377,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
@@ -427,9 +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"],
urlUnified: json["url_unified"],
urlUnifiedInit: json["url_unified_init"],
urlSsoPost: json["url_sso_post"],
urlSsoRedirect: json["url_sso_redirect"],
urlSsoInit: json["url_sso_init"],
@@ -455,9 +431,6 @@ export function SAMLProviderToJSONTyped(
| "verbose_name_plural"
| "meta_model_name"
| "url_download_metadata"
| "url_issuer"
| "url_unified"
| "url_unified_init"
| "url_sso_post"
| "url_sso_redirect"
| "url_sso_init"
@@ -479,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

@@ -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

@@ -18919,7 +18919,7 @@ paths:
schema:
type: boolean
- in: query
name: issuer_override
name: issuer
schema:
type: string
- in: query
@@ -20078,7 +20078,7 @@ paths:
schema:
type: boolean
- in: query
name: issuer_override
name: issuer
schema:
type: string
- in: query
@@ -34111,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
@@ -34195,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
@@ -47434,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
@@ -50202,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
@@ -53724,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:
@@ -53817,18 +53807,6 @@ components:
type: string
description: Get metadata download URL
readOnly: true
url_issuer:
type: string
description: Get Issuer/EntityID URL
readOnly: true
url_unified:
type: string
description: Get unified SAML endpoint URL (handles SSO and SLO)
readOnly: true
url_unified_init:
type: string
description: Get IdP-initiated SAML URL
readOnly: true
url_sso_post:
type: string
description: Get SSO Post URL
@@ -53862,14 +53840,11 @@ components:
- name
- pk
- url_download_metadata
- url_issuer
- url_slo_post
- url_slo_redirect
- url_sso_init
- url_sso_post
- url_sso_redirect
- url_unified
- url_unified_init
- verbose_name
- verbose_name_plural
SAMLProviderImportRequest:
@@ -53932,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

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,

881
uv.lock generated

File diff suppressed because it is too large Load Diff

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

@@ -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,26 +385,34 @@ 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">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SAML Endpoint")}</span
>${msg("SSO URL (Post)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlUnified)}"
value="${ifDefined(this.provider.urlSsoPost)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SSO URL (Redirect)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSsoRedirect)}"
/>
<p class="pf-c-form__helper-text">
${msg(
"SAML provider endpoint. Use this URL for SP configuration.",
)}
</p>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
@@ -416,7 +424,33 @@ export class SAMLProviderViewPage extends AKElement {
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlUnifiedInit)}"
value="${ifDefined(this.provider.urlSsoInit)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SLO URL (Post)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSloPost)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SLO URL (Redirect)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSloRedirect)}"
/>
</div>
</form>

View File

@@ -40,7 +40,11 @@ export class FlowMultitabController implements ReactiveController {
return;
}
if (isIdentificationChallenge(challenge) && challenge.applicationPreLaunch) {
if (
isIdentificationChallenge(challenge) &&
challenge.applicationPreLaunch &&
challenge.applicationPreLaunch !== "blank://blank"
) {
multiTabOrchestrateLeave();
window.location.assign(challenge.applicationPreLaunch);
return;

View File

@@ -23,6 +23,7 @@ export class AppleLoginInit extends BaseStage<AppleLoginChallenge, AppleChalleng
firstUpdated(): void {
const appleAuth = document.createElement("script");
appleAuth.nonce = window.litNonce;
appleAuth.src =
"https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
appleAuth.type = "text/javascript";

View File

@@ -17,6 +17,8 @@ export function loadTelegramWidget(
const widgetScript = document.createElement("script");
widgetScript.src = "https://telegram.org/js/telegram-widget.js?22";
widgetScript.type = "text/javascript";
widgetScript.nonce = window.litNonce || "";
widgetScript.setAttribute("data-radius", "0");
widgetScript.setAttribute("data-telegram-login", botUsername);
if (requestMessageAccess) {

View File

@@ -308,6 +308,7 @@ export class CaptchaStage
const scriptElement = document.createElement("script");
scriptElement.src = challengeURL.toString();
scriptElement.nonce = window.litNonce || "";
scriptElement.async = true;
scriptElement.defer = true;
scriptElement.onload = this.#scriptLoadListener;

View File

@@ -1,5 +1,4 @@
// sort-imports-ignore
import "@webcomponents/webcomponentsjs";
import "lit/polyfill-support.js";
import "core-js/actual";
import "@formatjs/intl-listformat/polyfill.js";

View File

@@ -10968,36 +10968,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -11001,36 +11001,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -8971,36 +8971,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10926,36 +10926,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -11168,36 +11168,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -11156,36 +11156,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10875,36 +10875,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -11157,36 +11157,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10523,36 +10523,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10207,36 +10207,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

View File

@@ -10549,36 +10549,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -11149,36 +11149,6 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10635,36 +10635,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10624,36 +10624,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -11429,36 +11429,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10260,36 +10260,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source><x id="0" equiv-text="${verboseName}"/> is not associated with any objects.</source>
<note from="lit-localize">Zero: no objects use this entity.</note>
</trans-unit>
<trans-unit id="s78a13f8f08d3a317">
<source>Authorization Code</source>
</trans-unit>
<trans-unit id="s9765a1c7821ec626">
<source>Implicit</source>
</trans-unit>
<trans-unit id="s6032372e5812d743">
<source>Hybrid</source>
</trans-unit>
<trans-unit id="s4c21579d264ff019">
<source>Refresh token</source>
</trans-unit>
<trans-unit id="sec04243bf971ba5c">
<source>Client credentials</source>
</trans-unit>
<trans-unit id="sf6625f1c093feb17">
<source>Device-code</source>
</trans-unit>
<trans-unit id="sccb6f4a3d1fb9434">
<source>Grant Types</source>
</trans-unit>
<trans-unit id="s89c63fb4a8cb6d2b">
<source>Grant types this provider may use.</source>
</trans-unit>
<trans-unit id="s86a9472dc09ecf48">
<source>vCenter</source>
</trans-unit>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -30,7 +30,7 @@ The following options can be configured:
For a reference of all fields available, see [the API schema for the User object](https://api.goauthentik.io/reference/core-users-retrieve/).
Only apps with launch URLs that begin with `http://` or `https://`, or that use relative paths, appear on the user's **My applications** page. To keep an app accessible but remove it from that page, use the **Hide from My applications** option (see [Hide applications](./manage_apps.mdx#hide-applications)).
Only applications whose launch URL starts with `http://` or `https://` or are relative URLs are shown on the users' **My applications** page. This can also be used to hide applications that shouldn't be visible on the **My applications** page but are still accessible by users, by setting the _Launch URL_ to `blank://blank`.
- _Icon (URL)_: Optionally configure an icon for the application. You can select from files uploaded to the [Files](../../customize/files.md) library or enter an absolute URL.

View File

@@ -104,14 +104,10 @@ return {
## Hide applications
To hide an application without modifying its policy settings or removing it, you can use the **Hide from My applications** option on the application. The application will no longer appear on the **My applications** page.
To hide an application without modifying its policy settings or removing it, you can simply set the _Launch URL_ to `blank://blank`, which will hide the application from users.
Keep in mind that users still have access, so they can still authorize access when the login process is started from the application.
:::info Hiding applications before 2026.5
Before authentik 2026.5, an application was hidden by setting its **Launch URL** to `blank://blank`. Existing applications using that value are automatically migrated to using the **Hide from My applications** option upon upgrading.
:::
## Launch URLs
To give users direct links to applications, you can now use a URL like `https://authentik.company/application/launch/<slug>/`. If the user is already logged in, they will be redirected to the application automatically. Otherwise, they'll be sent to the authentication flow and, if successful, forwarded to the application.