Compare commits

...

36 Commits

Author SHA1 Message Date
Dewi Roberts
d1c6d21c01 Merge branch 'main' into website/docs--add-enterprise-features-section 2026-02-13 12:37:10 +00:00
Alexander Tereshkin
3348ab34c3 enterprise/lifecycle: fix multiple reviews showing up in "Reviews" when the user is a member of multiple reviewer groups (#20266) 2026-02-13 13:34:00 +01:00
Marc 'risson' Schmitt
81b4256e3c ci: fix binary outpost build on release (#20248) 2026-02-13 13:33:35 +01:00
Dominic R
cc798f4425 web: add pretty names for lifecycle review events in event logs (#20264) 2026-02-12 21:38:33 +00:00
Dominic R
9903fd4d95 web: fix italic formatting in lifecycle rule help text (#20263)
* web: fix italic formatting in lifecycle rule help text

* r
2026-02-12 16:07:19 -05:00
Marc 'risson' Schmitt
20c2a33155 website/docs: 2025.8.6 release notes (#20243) 2026-02-12 15:54:07 +00:00
Marc 'risson' Schmitt
f1ee092930 website/docs: 2025.12.4 release notes (#20226) 2026-02-12 15:52:03 +00:00
Marc 'risson' Schmitt
611ddc2904 website/docs: 2025.10.4 release notes (#20242) 2026-02-12 15:50:06 +00:00
authentik-automation[bot]
aeb2457767 security: CVE-2026-25748 (#20240)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 15:17:01 +00:00
authentik-automation[bot]
97b6c9533f security: CVE-2026-25922 (#20241)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 14:51:19 +00:00
authentik-automation[bot]
c880c9f4ab security: CVE-2026-25227 (#20239)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 14:45:50 +00:00
Simonyi Gergő
af36cdc597 ci: fix release testing (#20207)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 12:39:00 +00:00
Teffen Ellis
4bd9b08cfb core: Apply CSpell corrections. (#20191) 2026-02-12 12:52:01 +01:00
authentik-automation[bot]
976df9e7da core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1770842608 (#20213)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 12:50:38 +01:00
authentik-automation[bot]
be64ed4281 core, web: update translations (#20215)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 12:49:35 +01:00
dependabot[bot]
5790316616 core: bump library/node from 25.6.0-trixie to 25.6.1-trixie in /website (#20220)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:48:35 +01:00
dependabot[bot]
53c376e4e9 core: bump google-api-python-client from 2.189.0 to 2.190.0 (#20217)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:48:22 +01:00
dependabot[bot]
38bde992b7 core: bump webauthn from 2.7.0 to 2.7.1 (#20218)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:47:43 +01:00
dependabot[bot]
c3353c1bf7 ci: bump docker/build-push-action from 6.18.0 to 6.19.1 (#20221)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:47:26 +01:00
tumpech
56f0df9d89 website/integrations: Update Komga instructions to add "email_verified" attribute to "email" claim. (#20135)
* Add email_verified to komga

* Fix minor spelling issues in Komga docs.

* Add email scope verification link

* Update index.md

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
2026-02-12 09:28:28 +00:00
Teffen Ellis
8aeedb6380 website: Apply CSpell corrections. (#20189)
* website: Apply CSpell corrections.

* Lint/spelling fix

---------

Co-authored-by: dewi-tik <dewi@goauthentik.io>
2026-02-11 21:57:36 +01:00
Connor Peshek
858a040dfb providers/saml: send logoutResponse on sp-init logout (#17691)
* providers/saml: send logoutResponse on sp-init logout

* Use first updated to fix multiple submits

* add backchannel logoutResponse

* tests

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local>
2026-02-11 14:18:39 -06:00
Dewi Roberts
0329b6e1ab website/docs: ssf: update SSF documentation (#20195)
* Update SSF documentation

* Fix tags

* Update website/docs/add-secure-apps/providers/ssf/create-ssf-provider.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/add-secure-apps/providers/ssf/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-02-11 19:44:47 +00:00
Tana M Berry
26eb34e17e website/docs: draft of new WS-Fed provider docs (#20091)
* first draft

* add table of parms

* tweak

* add section about certs

* a little more content

* more info on wa

* new procedurla file and edit sidebar

* tweaks

* dewi and jens edits

* tweak to remove bullet

* add docs link to the Rel Notes

* dewi edits thx

* ooops missed that last edit
2026-02-11 10:34:39 -06:00
Dewi Roberts
9d41d41b4f website/docs: add email verification scope doc (#20141)
* WIP

* Add link to 2025.10 release notes

* Apply suggestions from code review

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-11 15:55:25 +00:00
Georg
37432f43ba website/docs: correct reference to overriden S3 variable (#20156)
docs: correct reference to overriden S3 variable

Fixes: c30d1a478d ("files: rework (#17535)")

Signed-off-by: Georg Pfuetzenreuter <georg.pfuetzenreuter@suse.com>
2026-02-11 15:43:49 +00:00
Dewi Roberts
655e25e0d5 website/docs: rac: fixes the property mapping formatting (#20200)
Fixes the property mapping formatting
2026-02-11 10:20:45 -05:00
Simonyi Gergő
0356a30d65 api: fix test_build_schema (#20196)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-11 14:57:47 +01:00
Marc 'risson' Schmitt
15db2713ab ci: ensure schema is up-to-date (#20194) 2026-02-11 13:48:24 +00:00
authentik-automation[bot]
d9efce1002 core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1770771214 (#20178)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-11 13:42:51 +00:00
authentik-automation[bot]
a1a22978b3 core, web: update translations (#20177)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-11 14:13:40 +01:00
dependabot[bot]
95c9e5476e core: bump cachetools from 7.0.0 to 7.0.1 (#20183)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 14:09:13 +01:00
dependabot[bot]
93cf6e2cb1 core: bump cryptography from 46.0.4 to 46.0.5 (#20171)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.4 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.4...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 10:18:52 +01:00
Simonyi Gergő
0dd8ee073a core: fix test_docker.sh (#20179)
Broken by 646a0d3692
2026-02-11 09:54:40 +01:00
authentik-automation[bot]
7cb789e777 root: bump version to 2026.5.0-rc1 (#20174)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-11 01:43:16 +01:00
dewi-tik
09d0803d14 Create doc 2025-09-21 09:23:40 +01:00
151 changed files with 2153 additions and 357 deletions

View File

@@ -80,7 +80,7 @@ jobs:
make gen-client-ts
make gen-client-go
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
id: push
with:
context: .

View File

@@ -96,7 +96,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile

View File

@@ -42,6 +42,16 @@ jobs:
uses: ./.github/actions/setup
- name: run job
run: uv run make ci-${{ matrix.job }}
test-gen-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: generate schema
run: make migrate gen-build
- name: ensure schema is up-to-date
run: git diff --exit-code -- schema.yml blueprints/schema.json
test-migrations:
runs-on: ubuntu-latest
steps:
@@ -277,6 +287,7 @@ jobs:
if: always()
needs:
- lint
- test-gen-build
- test-migrations
- test-migrations-from-stable
- test-unittest

View File

@@ -111,7 +111,7 @@ jobs:
run: make gen-client-go
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: lifecycle/container/${{ matrix.type }}.Dockerfile

View File

@@ -51,7 +51,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
@@ -119,7 +119,7 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
id: push
with:
push: true
@@ -160,10 +160,17 @@ jobs:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Build web
- name: Install web dependencies
working-directory: web/
run: |
npm ci
- name: Generate API Clients
run: |
make gen-client-ts
make gen-client-go
- name: Build web
working-directory: web/
run: |
npm run build-proxy
- name: Build outpost
run: |
@@ -210,12 +217,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql
docker compose run -u root server test-all
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
docker compose -f lifecycle/container/compose.yml pull -q
docker compose -f lifecycle/container/compose.yml up --no-start
docker compose -f lifecycle/container/compose.yml start postgresql
docker compose -f lifecycle/container/compose.yml run -u root server test-all
sentry-release:
needs:
- build-server

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2026.2.0-rc1"
VERSION = "2026.5.0-rc1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -94,7 +94,7 @@ class Backend:
Args:
file_path: Relative file path
request: Optional Django HttpRequest for fully qualifed URL building
request: Optional Django HttpRequest for fully qualified URL building
use_cache: whether to retrieve the URL from cache
Returns:

View File

@@ -71,7 +71,7 @@ def postprocess_schema_responses(
def postprocess_schema_query_params(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
"""Optimize pagination parameters, instead of redeclaring parameters for each endpoint
declare them globally and refer to them"""
LOGGER.debug("Deduplicating query parameters")
for path in result["paths"].values():

View File

@@ -1,6 +1,8 @@
"""Schema generation tests"""
from pathlib import Path
from tempfile import gettempdir
from uuid import uuid4
from django.core.management import call_command
from django.urls import reverse
@@ -29,15 +31,14 @@ class TestSchemaGeneration(APITestCase):
def test_build_schema(self):
"""Test schema build command"""
blueprint_file = Path("blueprints/schema.json")
api_file = Path("schema.yml")
blueprint_file.unlink()
api_file.unlink()
tmp = Path(gettempdir())
blueprint_file = tmp / f"{str(uuid4())}.json"
api_file = tmp / f"{str(uuid4())}.yml"
with (
CONFIG.patch("debug", True),
CONFIG.patch("tenants.enabled", True),
CONFIG.patch("outposts.disable_embedded_outpost", True),
):
call_command("build_schema")
call_command("build_schema", blueprint_file=blueprint_file, api_file=api_file)
self.assertTrue(blueprint_file.exists())
self.assertTrue(api_file.exists())

View File

@@ -272,7 +272,7 @@ class Importer:
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
):
self.logger.debug(
"Initialise serializer with instance",
"Initialize serializer with instance",
model=model,
instance=model_instance,
pk=model_instance.pk,
@@ -290,7 +290,7 @@ class Importer:
)
else:
self.logger.debug(
"Initialised new serializer instance",
"Initialized new serializer instance",
model=model,
**cleanse_dict(updated_identifiers),
)

View File

@@ -154,14 +154,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
return queryset
def _get_allowed_applications(
self, pagined_apps: Iterator[Application], user: User | None = None
self, paginated_apps: Iterator[Application], user: User | None = None
) -> list[Application]:
applications = []
request = self.request._request
if user:
request = copy(request)
request.user = user
for application in pagined_apps:
for application in paginated_apps:
engine = PolicyEngine(application, request.user, request)
engine.build()
if engine.passing:

View File

@@ -63,7 +63,7 @@ class TestPropertyMappingAPI(APITestCase):
PropertyMappingSerializer().validate_expression("/")
def test_types(self):
"""Test PropertyMappigns's types endpoint"""
"""Test PropertyMapping's types endpoint"""
response = self.client.get(
reverse("authentik_api:propertymapping-types"),
)

View File

@@ -102,7 +102,7 @@ class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet
default=Value(False),
output_field=ModelBooleanField(),
)
)
).distinct()
@action(
detail=False,

View File

@@ -42,7 +42,7 @@ ARG_SANITIZE = re.compile(r"[:.-]")
def sanitize_arg(arg_name: str) -> str:
return re.sub(ARG_SANITIZE, "_", arg_name)
return re.sub(ARG_SANITIZE, "_", slugify(arg_name))
class BaseEvaluator:
@@ -311,7 +311,9 @@ class BaseEvaluator:
def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
handler_signature = ",".join(
[x for x in [sanitize_arg(x) for x in self._context.keys()] if x]
)
full_expression = ""
full_expression += f"def handler({handler_signature}):\n"
full_expression += indent(expression, " ")

View File

@@ -1,5 +1,6 @@
"""Test Evaluator base functions"""
from pathlib import Path
from unittest.mock import patch
from django.test import RequestFactory, TestCase
@@ -353,3 +354,18 @@ class TestEvaluator(TestCase):
self.assertEqual(message.to, ["to@example.com"])
self.assertEqual(message.cc, ["cc1@example.com", "cc2@example.com"])
self.assertEqual(message.bcc, ["bcc1@example.com", "bcc2@example.com"])
def test_expr_arg_escape(self):
"""Test escaping of arguments"""
eval = BaseEvaluator()
eval._context = {
'z=getattr(getattr(__import__("os"), "popen")("id > /tmp/test"), "read")()': "bar",
"@@": "baz",
"{{": "baz",
"aa@@": "baz",
}
res = eval.evaluate("return locals()")
self.assertEqual(
res, {"zgetattrgetattr__import__os_popenid_tmptest_read": "bar", "aa": "baz"}
)
self.assertFalse(Path("/tmp/test").exists())

View File

@@ -1,19 +1,31 @@
"""Shared logout stages for SAML and OIDC providers"""
from django.http import HttpResponse
from rest_framework.fields import CharField, DictField, ListField
from rest_framework.fields import CharField, ListField
from authentik.common.oauth.constants import PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.stage import ChallengeStageView
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS
class LogoutURL(PassiveSerializer):
"""Data for a single logout URL"""
url = CharField()
provider_name = CharField(required=False, allow_null=True)
binding = CharField(required=False, allow_null=True)
saml_request = CharField(required=False, allow_null=True)
saml_response = CharField(required=False, allow_null=True)
saml_relay_state = CharField(required=False, allow_null=True)
class IframeLogoutChallenge(Challenge):
"""Challenge for iframe logout"""
component = CharField(default="ak-provider-iframe-logout")
logout_urls = ListField(child=DictField(), default=list)
logout_urls = ListField(child=LogoutURL(), default=list)
class IframeLogoutChallengeResponse(ChallengeResponse):

View File

@@ -432,7 +432,7 @@ class AuthorizationFlowInitView(BufferedPolicyAccessView):
return response
def dispatch(self, request: HttpRequest, *args, **kwargs):
# Activate language before parsing params (error messages should be localised)
# Activate language before parsing params (error messages should be localized)
return self.dispatch_with_language(request, *args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@@ -368,7 +368,7 @@ class TokenParams:
) -> tuple[dict, OAuthSource] | tuple[None, None]:
# Fully decode the JWT without verifying the signature, so we can get access to
# the header.
# Get the Key ID from the header, and use that to optimise our source query to only find
# Get the Key ID from the header, and use that to optimize our source query to only find
# sources that have a JWK for that Key ID
# The Key ID doesn't have a fixed format, but must match between an issued JWT
# and whatever is returned by the JWKS endpoint

View File

@@ -213,6 +213,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"sign_assertion",
"sign_response",
"sign_logout_request",
"sign_logout_response",
"sp_binding",
"sls_binding",
"logout_method",

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-24 18:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0020_samlprovider_logout_method_and_more"),
]
operations = [
migrations.AddField(
model_name="samlprovider",
name="sign_logout_response",
field=models.BooleanField(default=False),
),
]

View File

@@ -227,6 +227,7 @@ class SAMLProvider(Provider):
sign_assertion = models.BooleanField(default=True)
sign_response = models.BooleanField(default=False)
sign_logout_request = models.BooleanField(default=False)
sign_logout_response = models.BooleanField(default=False)
@property
def launch_url(self) -> str | None:

View File

@@ -1,11 +1,12 @@
"""SAML Logout stages for automatic injection"""
from django.http import HttpResponse
from rest_framework.fields import BooleanField, CharField
from rest_framework.fields import BooleanField, CharField, ChoiceField
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse, HttpChallengeResponse
from authentik.flows.stage import ChallengeStageView
from authentik.providers.saml.models import SAMLBindings
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS
LOGGER = get_logger()
@@ -19,14 +20,17 @@ class NativeLogoutChallenge(Challenge):
"""Challenge for native browser logout"""
component = CharField(default="ak-provider-saml-native-logout")
post_url = CharField(required=False)
saml_request = CharField(required=False)
relay_state = CharField(required=False)
provider_name = CharField(required=False)
binding = CharField(required=False)
redirect_url = CharField(required=False)
is_complete = BooleanField(required=False, default=False)
post_url = CharField(required=False)
redirect_url = CharField(required=False)
saml_binding = ChoiceField(choices=SAMLBindings.choices, required=False)
saml_request = CharField(required=False)
saml_response = CharField(required=False)
saml_relay_state = CharField(required=False)
class NativeLogoutChallengeResponse(ChallengeResponse):
"""Response for native browser logout"""

View File

@@ -0,0 +1,196 @@
"""LogoutResponse processor"""
import base64
from urllib.parse import quote, urlencode
import xmlsec
from lxml import etree
from lxml.etree import Element, SubElement
from authentik.common.saml.constants import (
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_MAP,
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
SIGN_ALGORITHM_TRANSFORM_MAP,
)
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.utils import get_random_id
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
from authentik.providers.saml.utils.time import get_time_string
class LogoutResponseProcessor:
"""Generate a SAML LogoutResponse"""
provider: SAMLProvider
logout_request: LogoutRequest
destination: str | None
relay_state: str | None
_issue_instant: str
_response_id: str
def __init__(
self,
provider: SAMLProvider,
logout_request: LogoutRequest,
destination: str | None = None,
relay_state: 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._issue_instant = get_time_string()
self._response_id = get_random_id()
def get_issuer(self) -> Element:
"""Get Issuer element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self.provider.issuer
return issuer
def build(self, status: str = "Success") -> Element:
"""Build a SAML LogoutResponse as etree Element"""
response = Element(f"{{{NS_SAML_PROTOCOL}}}LogoutResponse", nsmap=NS_MAP)
response.attrib["Version"] = "2.0"
response.attrib["IssueInstant"] = self._issue_instant
response.attrib["ID"] = self._response_id
if self.destination:
response.attrib["Destination"] = self.destination
if self.logout_request and self.logout_request.id:
response.attrib["InResponseTo"] = self.logout_request.id
response.append(self.get_issuer())
# Add Status element
status_element = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
status_code = SubElement(status_element, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
status_code.attrib["Value"] = f"urn:oasis:names:tc:SAML:2.0:status:{status}"
return response
def build_response(self, status: str = "Success") -> str:
"""Build and sign LogoutResponse, return as XML string (not encoded)"""
response = self.build(status)
if self.provider.signing_kp and self.provider.sign_logout_response:
self._add_signature(response)
self._sign_response(response)
return etree.tostring(response).decode()
def encode_post(self, status: str = "Success") -> str:
"""Encode LogoutResponse for POST binding"""
response = self.build(status)
if self.provider.signing_kp and self.provider.sign_logout_response:
self._add_signature(response)
self._sign_response(response)
return base64.b64encode(etree.tostring(response)).decode()
def encode_redirect(self, status: str = "Success") -> str:
"""Encode LogoutResponse for Redirect binding"""
response = self.build(status)
# Note: For redirect binding, signatures are added as query parameters, not in XML
xml_str = etree.tostring(response, encoding="UTF-8", xml_declaration=True)
return deflate_and_base64_encode(xml_str.decode("UTF-8"))
def get_redirect_url(self, status: str = "Success") -> str:
"""Build complete logout response URL for redirect binding with signature if needed"""
encoded_response = self.encode_redirect(status)
params = {
"SAMLResponse": encoded_response,
}
if self.relay_state:
params["RelayState"] = self.relay_state
if self.provider.signing_kp and self.provider.sign_logout_response:
sig_alg = self.provider.signature_algorithm
params["SigAlg"] = sig_alg
# Build the string to sign
query_string = self._build_signable_query_string(params)
signature = self._sign_query_string(query_string)
params["Signature"] = base64.b64encode(signature).decode()
# Some SP's use query params on their sls endpoint
if not self.destination:
raise ValueError("destination is required for redirect URL")
separator = "&" if "?" in self.destination else "?"
return f"{self.destination}{separator}{urlencode(params)}"
def _add_signature(self, element: Element):
"""Add signature placeholder to element"""
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
element,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
element.insert(1, signature) # Insert after Issuer
def _sign_response(self, response: Element):
"""Sign the response element"""
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
self.provider.digest_algorithm, xmlsec.constants.TransformSha1
)
xmlsec.tree.add_ids(response, ["ID"])
signature_node = xmlsec.tree.find_node(response, xmlsec.constants.NodeSignature)
ref = xmlsec.template.add_reference(
signature_node,
digest_algorithm_transform,
uri="#" + response.attrib["ID"],
)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
key_info = xmlsec.template.ensure_key_info(signature_node)
xmlsec.template.add_x509_data(key_info)
ctx = xmlsec.SignatureContext()
ctx.key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data, # Use key_data for the private key
xmlsec.constants.KeyDataFormatPem,
)
ctx.key.load_cert_from_memory(
self.provider.signing_kp.certificate_data, xmlsec.constants.KeyDataFormatPem
)
ctx.sign(signature_node)
def _build_signable_query_string(self, params: dict) -> str:
"""Build query string for signing (order matters per SAML spec)"""
# SAML spec requires specific order: SAMLResponse, RelayState, SigAlg
# Values must be URL-encoded individually before concatenation
ordered = []
if "SAMLResponse" in params:
ordered.append(f"SAMLResponse={quote(params['SAMLResponse'], safe='')}")
if "RelayState" in params:
ordered.append(f"RelayState={quote(params['RelayState'], safe='')}")
if "SigAlg" in params:
ordered.append(f"SigAlg={quote(params['SigAlg'], safe='')}")
return "&".join(ordered)
def _sign_query_string(self, query_string: str) -> bytes:
"""Sign the query string for redirect binding"""
signature_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha256
)
key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data,
xmlsec.constants.KeyDataFormatPem,
None,
)
ctx = xmlsec.SignatureContext()
ctx.key = key
return ctx.sign_binary(query_string.encode("utf-8"), signature_algorithm_transform)

View File

@@ -175,16 +175,16 @@ def handle_flow_pre_user_logout(
logout_data = {
"post_url": session.provider.sls_url,
"saml_request": form_data["SAMLRequest"],
"relay_state": form_data["RelayState"],
"saml_relay_state": form_data["RelayState"],
"provider_name": session.provider.name,
"binding": SAMLBindings.POST,
"saml_binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"redirect_url": logout_url,
"provider_name": session.provider.name,
"binding": SAMLBindings.REDIRECT,
"saml_binding": SAMLBindings.REDIRECT,
}
native_sessions.append(logout_data)

View File

@@ -5,8 +5,11 @@ from django.contrib.auth import get_user_model
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
LOGGER = get_logger()
User = get_user_model()
@@ -78,3 +81,86 @@ def send_post_logout_request(provider: SAMLProvider, processor: LogoutRequestPro
)
return True
@actor(description="Send SAML LogoutResponse to a Service Provider (backchannel)")
def send_saml_logout_response(
provider_pk: int,
sls_url: str,
logout_request_id: str | None = None,
relay_state: str | None = None,
):
"""Send SAML LogoutResponse to a Service Provider using backchannel (server-to-server)"""
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
if not provider:
LOGGER.error(
"Provider not found for SAML logout response",
provider_pk=provider_pk,
)
return False
LOGGER.debug(
"Sending backchannel SAML logout response",
provider=provider.name,
sls_url=sls_url,
)
# Create a minimal LogoutRequest object for the response processor
# We only need the ID and relay_state for building the response
logout_request = None
if logout_request_id:
logout_request = LogoutRequest()
logout_request.id = logout_request_id
logout_request.relay_state = relay_state
# Build the logout response
processor = LogoutResponseProcessor(
provider=provider,
logout_request=logout_request,
destination=sls_url,
relay_state=relay_state,
)
encoded_response = processor.encode_post()
form_data = {
"SAMLResponse": encoded_response,
}
if relay_state:
form_data["RelayState"] = relay_state
# Send the logout response to the SP
try:
response = requests.post(
sls_url,
data=form_data,
timeout=10,
headers={
"Content-Type": "application/x-www-form-urlencoded",
},
allow_redirects=True,
)
response.raise_for_status()
LOGGER.info(
"Successfully sent backchannel logout response to SP",
provider=provider.name,
sls_url=sls_url,
status_code=response.status_code,
)
return True
except requests.exceptions.RequestException as exc:
LOGGER.warning(
"Failed to send backchannel logout response to SP",
provider=provider.name,
sls_url=sls_url,
error=str(exc),
)
Event.new(
EventAction.CONFIGURATION_ERROR,
provider=provider,
message=f"Backchannel logout response failed: {str(exc)}",
).save()
return False

View File

@@ -69,7 +69,7 @@ class TestNativeLogoutStageView(TestCase):
{
"redirect_url": "https://sp1.example.com/sls?SAMLRequest=encoded",
"provider_name": "test-provider-1",
"binding": "redirect",
"saml_binding": "redirect",
}
]
stage_view = NativeLogoutStageView(
@@ -85,7 +85,7 @@ class TestNativeLogoutStageView(TestCase):
# Should return a NativeLogoutChallenge
self.assertIsInstance(challenge, NativeLogoutChallenge)
self.assertEqual(challenge.initial_data["binding"], "redirect")
self.assertEqual(challenge.initial_data["saml_binding"], "redirect")
self.assertEqual(challenge.initial_data["provider_name"], "test-provider-1")
self.assertIn("redirect_url", challenge.initial_data)
@@ -102,9 +102,9 @@ class TestNativeLogoutStageView(TestCase):
{
"post_url": "https://sp2.example.com/sls",
"saml_request": "encoded_saml_request",
"relay_state": "https://idp.example.com/flow/test-flow",
"saml_relay_state": "https://idp.example.com/flow/test-flow",
"provider_name": "test-provider-2",
"binding": "post",
"saml_binding": "post",
}
]
stage_view = NativeLogoutStageView(
@@ -120,11 +120,11 @@ class TestNativeLogoutStageView(TestCase):
# Should return a NativeLogoutChallenge
self.assertIsInstance(challenge, NativeLogoutChallenge)
self.assertEqual(challenge.initial_data["binding"], "post")
self.assertEqual(challenge.initial_data["saml_binding"], "post")
self.assertEqual(challenge.initial_data["provider_name"], "test-provider-2")
self.assertEqual(challenge.initial_data["post_url"], "https://sp2.example.com/sls")
self.assertIn("saml_request", challenge.initial_data)
self.assertIn("relay_state", challenge.initial_data)
self.assertIn("saml_relay_state", challenge.initial_data)
def test_get_challenge_all_complete(self):
"""Test get_challenge when all providers are done"""

View File

@@ -0,0 +1,139 @@
"""logout response tests"""
from defusedxml import ElementTree
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.common.saml.constants import (
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
NS_SIGNATURE,
)
from authentik.core.tests.utils import create_test_cert, create_test_flow
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
class TestLogoutResponse(TestCase):
"""Test LogoutResponse processor"""
@apply_blueprint("system/providers-saml.yaml")
def setUp(self):
cert = create_test_cert()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(),
acs_url="http://testserver/source/saml/provider/acs/",
sls_url="http://testserver/source/saml/provider/sls/",
signing_kp=cert,
verification_kp=cert,
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
def test_build_response(self):
"""Test building a LogoutResponse"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
# Parse and verify
root = ElementTree.fromstring(response_xml)
self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
self.assertEqual(root.attrib["Version"], "2.0")
self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
# Check Issuer
issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
self.assertEqual(issuer.text, self.provider.issuer)
# Check Status
status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")
def test_build_response_signed(self):
"""Test building a signed LogoutResponse"""
self.provider.sign_logout_response = True
self.provider.save()
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
# Parse and verify signature is present
root = ElementTree.fromstring(response_xml)
signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
self.assertIsNotNone(signature)
# Verify signature structure
signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
self.assertIsNotNone(signed_info)
signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
self.assertIsNotNone(signature_value)
self.assertIsNotNone(signature_value.text)
def test_no_inresponseto(self):
"""Test building response without a logout request omits InResponseTo attribute"""
processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
response_xml = processor.build_response(status="Success")
root = ElementTree.fromstring(response_xml)
self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
self.assertNotIn("InResponseTo", root.attrib)
def test_no_destination(self):
"""Test building response without destination"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
)
processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
response_xml = processor.build_response(status="Success")
root = ElementTree.fromstring(response_xml)
self.assertNotIn("Destination", root.attrib)
def test_relay_state_from_logout_request(self):
"""Test that relay_state is taken from logout_request if not provided"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="request-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
self.assertEqual(processor.relay_state, "request-relay-state")
def test_relay_state_override(self):
"""Test that explicit relay_state overrides logout_request relay_state"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="request-relay-state",
)
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
relay_state="explicit-relay-state",
)
self.assertEqual(processor.relay_state, "explicit-relay-state")

View File

@@ -0,0 +1,291 @@
"""Tests for SAML provider tasks"""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from requests.exceptions import ConnectionError, HTTPError
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_EMAIL
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.tasks import (
send_post_logout_request,
send_saml_logout_request,
send_saml_logout_response,
)
class TestSendSamlLogoutResponse(TestCase):
"""Tests for send_saml_logout_response task"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_response(self, mock_post):
"""Test successful POST to SP returns True"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state="https://sp.example.com/return",
)
self.assertTrue(result)
mock_post.assert_called_once()
# Verify the POST was made with correct data
call_kwargs = mock_post.call_args[1]
self.assertEqual(call_kwargs["timeout"], 10)
self.assertEqual(
call_kwargs["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Verify form data contains SAMLResponse and RelayState
form_data = call_kwargs["data"]
self.assertIn("SAMLResponse", form_data)
self.assertIn("RelayState", form_data)
self.assertEqual(form_data["RelayState"], "https://sp.example.com/return")
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_response_no_relay_state(self, mock_post):
"""Test successful POST without relay_state"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state=None,
)
self.assertTrue(result)
# Verify form data does not contain RelayState
form_data = mock_post.call_args[1]["data"]
self.assertIn("SAMLResponse", form_data)
self.assertNotIn("RelayState", form_data)
def test_provider_not_found(self):
"""Test returns False when provider doesn't exist"""
result = send_saml_logout_response(
provider_pk=99999, # Non-existent provider
sls_url="https://sp.example.com/sls",
logout_request_id="test-request-id",
relay_state=None,
)
self.assertFalse(result)
@patch("authentik.providers.saml.tasks.Event")
@patch("authentik.providers.saml.tasks.requests.post")
def test_http_error_creates_event(self, mock_post, mock_event_class):
"""Test HTTP error creates an error event"""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = HTTPError("500 Server Error")
mock_post.return_value = mock_response
mock_event = MagicMock()
mock_event_class.new.return_value = mock_event
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state=None,
)
self.assertFalse(result)
# Verify error event was created
mock_event_class.new.assert_called_once()
call_kwargs = mock_event_class.new.call_args[1]
self.assertIn("Backchannel logout response failed", call_kwargs["message"])
mock_event.save.assert_called_once()
class TestSendSamlLogoutRequest(TestCase):
"""Tests for send_saml_logout_request task"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_request(self, mock_post):
"""Test successful POST logout request returns True"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_request(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
self.assertTrue(result)
mock_post.assert_called_once()
# Verify the POST was made with correct data
call_kwargs = mock_post.call_args[1]
self.assertEqual(call_kwargs["timeout"], 10)
self.assertEqual(
call_kwargs["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Verify form data contains SAMLRequest
form_data = call_kwargs["data"]
self.assertIn("SAMLRequest", form_data)
def test_provider_not_found(self):
"""Test returns False when provider doesn't exist"""
result = send_saml_logout_request(
provider_pk=99999, # Non-existent provider
sls_url="https://sp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
self.assertFalse(result)
@patch("authentik.providers.saml.tasks.requests.post")
def test_http_error_raises(self, mock_post):
"""Test HTTP error raises exception (no try/catch in send_post_logout_request)"""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = HTTPError("500 Server Error")
mock_post.return_value = mock_response
with self.assertRaises(HTTPError):
send_saml_logout_request(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
class TestSendPostLogoutRequest(TestCase):
"""Tests for send_post_logout_request function"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_post(self, mock_post):
"""Test successful POST returns True"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
result = send_post_logout_request(self.provider, processor)
self.assertTrue(result)
mock_post.assert_called_once()
@patch("authentik.providers.saml.tasks.requests.post")
def test_with_relay_state(self, mock_post):
"""Test POST includes RelayState when present"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state="https://sp.example.com/return",
)
result = send_post_logout_request(self.provider, processor)
self.assertTrue(result)
# Verify RelayState is included
form_data = mock_post.call_args[1]["data"]
self.assertIn("RelayState", form_data)
self.assertEqual(form_data["RelayState"], "https://sp.example.com/return")
@patch("authentik.providers.saml.tasks.requests.post")
def test_connection_error_raises(self, mock_post):
"""Test connection error raises exception"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_post.side_effect = ConnectionError("Connection refused")
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
with self.assertRaises(ConnectionError):
send_post_logout_request(self.provider, processor)

View File

@@ -8,13 +8,15 @@ from django.urls import reverse
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_EMAIL
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_brand, create_test_flow
from authentik.core.tests.utils import create_test_brand, create_test_cert, create_test_flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.models import SAMLBindings, SAMLLogoutMethods, SAMLProvider
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_RELAY_STATE
from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_RELAY_STATE,
)
from authentik.providers.saml.views.sp_slo import (
SPInitiatedSLOBindingPOSTView,
SPInitiatedSLOBindingRedirectView,
@@ -436,3 +438,290 @@ class TestSPInitiatedSLOViews(TestCase):
# Should treat it as plain URL and redirect to it
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/some/invalid/path")
class TestSPInitiatedSLOLogoutMethods(TestCase):
"""Test SP-initiated SAML SLO logout method branching"""
def setUp(self):
"""Set up test fixtures"""
self.factory = RequestFactory()
self.brand = create_test_brand()
self.flow = create_test_flow()
self.invalidation_flow = create_test_flow()
self.cert = create_test_cert()
# Create provider with sls_url
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
signing_kp=self.cert,
)
# Create application
self.application = Application.objects.create(
name="test-app",
slug="test-app-logout-methods",
provider=self.provider,
)
# Create logout request processor for generating test requests
self.processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination="https://idp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state="https://sp.example.com/return",
)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_native_post_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_NATIVE with POST binding parses request correctly"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_NATIVE
self.provider.sls_binding = SAMLBindings.POST
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed and provider is configured correctly
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
self.assertEqual(view.provider.logout_method, SAMLLogoutMethods.FRONTCHANNEL_NATIVE)
self.assertEqual(view.provider.sls_binding, SAMLBindings.POST)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_native_redirect_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_NATIVE with REDIRECT binding creates redirect URL"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_NATIVE
self.provider.sls_binding = SAMLBindings.REDIRECT
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_iframe_post_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_IFRAME with POST binding creates IframeLogoutStageView"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_IFRAME
self.provider.sls_binding = SAMLBindings.POST
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_iframe_redirect_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_IFRAME with REDIRECT binding"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_IFRAME
self.provider.sls_binding = SAMLBindings.REDIRECT
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_backchannel_parses_request(self, mock_auth_session):
"""Test BACKCHANNEL mode parses request correctly"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.BACKCHANNEL
self.provider.sls_binding = SAMLBindings.POST
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed and provider is configured correctly
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
self.assertEqual(view.provider.logout_method, SAMLLogoutMethods.BACKCHANNEL)
self.assertEqual(view.provider.sls_binding, SAMLBindings.POST)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_no_sls_url_only_session_end(self, mock_auth_session):
"""Test that only SessionEndStage is appended when sls_url is empty"""
mock_auth_session.from_request.return_value = None
# Create provider without sls_url
provider_no_sls = SAMLProvider.objects.create(
name="no-sls-provider",
authorization_flow=self.flow,
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="", # No SLS URL
issuer="https://idp.example.com",
)
app_no_sls = Application.objects.create(
name="no-sls-app",
slug="no-sls-app",
provider=provider_no_sls,
)
processor = LogoutRequestProcessor(
provider=provider_no_sls,
user=None,
destination="https://idp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
encoded_request = processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{app_no_sls.slug}/",
{
"SAMLRequest": encoded_request,
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=app_no_sls.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the provider has no sls_url
self.assertEqual(view.provider.sls_url, "")
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_relay_state_propagation(self, mock_auth_session):
"""Test that relay state from logout request is passed through to response"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_IFRAME
self.provider.save()
expected_relay_state = "https://sp.example.com/custom-return"
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination="https://idp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state=expected_relay_state,
)
encoded_request = processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": expected_relay_state,
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify relay state was captured
logout_request = view.plan_context.get("authentik/providers/saml/logout_request")
self.assertEqual(logout_request.relay_state, expected_relay_state)

View File

@@ -15,10 +15,22 @@ from authentik.flows.stage import SessionEndStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.iframe_logout import IframeLogoutStageView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLProvider, SAMLSession
from authentik.providers.saml.models import (
SAMLBindings,
SAMLLogoutMethods,
SAMLProvider,
SAMLSession,
)
from authentik.providers.saml.native_logout import NativeLogoutStageView
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
from authentik.providers.saml.tasks import send_saml_logout_response
from authentik.providers.saml.utils.encoding import nice64
from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS,
PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS,
PLAN_CONTEXT_SAML_LOGOUT_REQUEST,
PLAN_CONTEXT_SAML_RELAY_STATE,
REQUEST_KEY_RELAY_STATE,
@@ -68,7 +80,102 @@ class SPInitiatedSLOView(PolicyAccessView):
**self.plan_context,
},
)
plan.append_stage(in_memory_stage(SessionEndStage))
if self.provider.sls_url:
# Get logout request and extract relay state
logout_request = self.plan_context.get(PLAN_CONTEXT_SAML_LOGOUT_REQUEST)
relay_state = logout_request.relay_state if logout_request else None
# Store relay state for the logout response
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
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,
)
if self.provider.sls_binding == SAMLBindings.POST:
logout_response = processor.encode_post()
logout_data = {
"post_url": self.provider.sls_url,
"saml_response": logout_response,
"saml_relay_state": relay_state,
"provider_name": self.provider.name,
"saml_binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"redirect_url": logout_url,
"provider_name": self.provider.name,
"saml_binding": SAMLBindings.REDIRECT,
}
plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
plan.append_stage(in_memory_stage(NativeLogoutStageView))
elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
# Backchannel mode - server sends logout response directly to SP in background
# No user interaction needed
if self.provider.sls_binding != SAMLBindings.POST:
LOGGER.warning(
"Backchannel logout requires POST binding, but provider is configured "
"with %s binding",
self.provider.sls_binding,
provider=self.provider,
)
# Queue the logout response to be sent in the background
# This doesn't block the user's logout from completing
send_saml_logout_response.send(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id=logout_request.id if logout_request else None,
relay_state=relay_state,
)
LOGGER.debug(
"Queued backchannel logout response",
provider=self.provider,
sls_url=self.provider.sls_url,
)
# Just end the session - no user interaction needed
plan.append_stage(in_memory_stage(SessionEndStage))
else:
# Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
)
logout_response = processor.build_response()
if self.provider.sls_binding == SAMLBindings.POST:
logout_data = {
"url": self.provider.sls_url,
"saml_response": nice64(logout_response),
"saml_relay_state": relay_state,
"provider_name": self.provider.name,
"binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"url": logout_url,
"provider_name": self.provider.name,
"binding": SAMLBindings.REDIRECT,
}
plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
plan.append_stage(in_memory_stage(IframeLogoutStageView))
plan.append_stage(in_memory_stage(SessionEndStage))
else:
# No SLS URL configured, just end session
plan.append_stage(in_memory_stage(SessionEndStage))
# Remove samlsession from database
auth_session = AuthenticatedSession.from_request(self.request, self.request.user)

View File

@@ -65,7 +65,7 @@ class EnterpriseUser(BaseModel):
employeeNumber: str | None = Field(
None,
description="Numeric or alphanumeric identifier assigned to a person, "
"typically based on order of hire or association with anorganization.",
"typically based on order of hire or association with an organization.",
)
costCenter: str | None = Field(None, description="Identifies the name of a cost center.")
organization: str | None = Field(None, description="Identifies the name of an organization.")
@@ -73,7 +73,7 @@ class EnterpriseUser(BaseModel):
department: str | None = Field(
None,
description="Numeric or alphanumeric identifier assigned to a person,"
" typically based on order of hire or association with anorganization.",
" typically based on order of hire or association with an organization.",
)
manager: Manager | None = Field(
None,

View File

@@ -7,6 +7,7 @@ from django.http import HttpRequest
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from lxml.etree import _Element # nosec
from rest_framework.serializers import Serializer
from authentik.common.saml.constants import (
@@ -217,9 +218,8 @@ class SAMLSource(Source):
def property_mapping_type(self) -> type[PropertyMapping]:
return SAMLSourcePropertyMapping
def get_base_user_properties(self, root: Any, name_id: Any, **kwargs):
def get_base_user_properties(self, root: _Element, assertion: _Element, name_id: Any, **kwargs):
attributes = {}
assertion = root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
if assertion is None:
raise ValueError("Assertion element not found")
attribute_statement = assertion.find(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")

View File

@@ -66,6 +66,8 @@ class ResponseProcessor:
_http_request: HttpRequest
_assertion: _Element | None = None
def __init__(self, source: SAMLSource, request: HttpRequest):
self._source = source
self._http_request = request
@@ -122,6 +124,7 @@ class ResponseProcessor:
index_of,
decrypted_assertion,
)
self._assertion = decrypted_assertion
def _verify_signature(self, signature_node: _Element):
"""Verify a single signature node"""
@@ -162,6 +165,10 @@ class ResponseProcessor:
raise InvalidSignature("No Signature exists in the Assertion element.")
self._verify_signature(signature_nodes[0])
parent = signature_nodes[0].getparent()
if parent is None or parent.tag != f"{{{NS_SAML_ASSERTION}}}Assertion":
raise InvalidSignature("No Signature exists in the Assertion element.")
self._assertion = parent
def _verify_request_id(self):
if self._source.allow_idp_initiated:
@@ -239,14 +246,21 @@ class ResponseProcessor:
identifier=str(name_id.text),
user_info={
"root": self._root,
"assertion": self.get_assertion(),
"name_id": name_id,
},
policy_context={},
)
def get_assertion(self) -> Element | None:
"""Get assertion element, if we have a signed assertion"""
if self._assertion is not None:
return self._assertion
return self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
def _get_name_id(self) -> Element:
"""Get NameID Element"""
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
assertion = self.get_assertion()
if assertion is None:
raise ValueError("Assertion element not found")
subject = assertion.find(f"{{{NS_SAML_ASSERTION}}}Subject")
@@ -299,6 +313,7 @@ class ResponseProcessor:
identifier=str(name_id.text),
user_info={
"root": self._root,
"assertion": self.get_assertion(),
"name_id": name_id,
},
policy_context={

View File

@@ -0,0 +1,68 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_other_id_pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">bad</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>zNDuGxwP4gVkv/Dzt7kiKo/4gzk=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>GLP/vE8uxerB0uDpPslUgLPBL6ePQB619MoQ0I2Y5lAtFE6CB1zh8BnzChRx/bFjNy4byfOe8mFfM0r7WUi1PJOFWyUPoatdLl7wHHBIRTnPpYmu3Tb2Gz0sOP0F8wW7JkBft5gJfVw49nk5si9/3Q3o52jnJZ7dPtqfIOh8uNeopikK0HLF6sU05qCCtjcXfniEnLQFNBFMo9uY5GQqmR5n3nqPz1wYyyfFOAbVmGgBIoO2PfGX2GVLQhltc9qf2JMhks4jgZsZ8iLUIiH1lcLGWZEEs94k8k0P6gSv1uZ7Vbhksd/N9Jq9pCVuEJ/jRPcAdVjzbxqKQAj6ELwr8O6fepTzA+CAdwEolBnx/C6TmSbVZ+IWk6QUGe4x4+IAukC+0hkKENlO0ELOScksvyhpgHbxNA4rp+DhGupCaO/I2RrsQkmvavbqm+wSEspK7scK112SDunjDvqPHsPYgukD33T/97PxTLorg2kKP9HHJwPJKoXXeyOGcA6vwK+RqrAlZ2dLGAgcXo+sJcdCLuvxDNz9VXofBjBZIKVKdmYhm0QJaPYHtuQsAyFavQhdOBOmGHb7QX3YE3Xy4dX4LymtT+Jlb1I4FJSht/9HUIHW1FdhfDak4f7gUgjuMamMddLD0jVgeESupSREzFv/gj2IrctkbgjAO0iuuiBgKMg=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>

View File

@@ -36,7 +36,9 @@ class TestPropertyMappings(TestCase):
def test_user_base_properties(self):
"""Test user base properties"""
properties = self.source.get_base_user_properties(root=ROOT, name_id=NAME_ID)
properties = self.source.get_base_user_properties(
root=ROOT, assertion=ROOT.find(f"{{{NS_SAML_ASSERTION}}}Assertion"), name_id=NAME_ID
)
self.assertEqual(
properties,
{
@@ -49,7 +51,11 @@ class TestPropertyMappings(TestCase):
def test_group_base_properties(self):
"""Test group base properties"""
properties = self.source.get_base_user_properties(root=ROOT_GROUPS, name_id=NAME_ID)
properties = self.source.get_base_user_properties(
root=ROOT_GROUPS,
assertion=ROOT_GROUPS.find(f"{{{NS_SAML_ASSERTION}}}Assertion"),
name_id=NAME_ID,
)
self.assertEqual(properties["groups"], ["group 1", "group 2"])
for group_id in ["group 1", "group 2"]:
properties = self.source.get_base_group_properties(root=ROOT, group_id=group_id)

View File

@@ -164,6 +164,31 @@ class TestResponseProcessor(TestCase):
parser = ResponseProcessor(self.source, request)
parser.parse()
def test_verification_assertion_duplicate(self):
"""Test verifying signature inside assertion, where the response has another assertion
before our signed assertion"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_assertion = True
self.source.signed_response = False
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_signed_assertion_dup.xml").encode()
).decode()
},
)
parser = ResponseProcessor(self.source, request)
parser.parse()
self.assertNotEqual(parser._get_name_id().text, "bad")
self.assertEqual(parser._get_name_id().text, "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
def test_verification_response(self):
"""Test verifying signature inside response"""
key = load_fixture("fixtures/signature_cert.pem")

View File

@@ -58,7 +58,7 @@ def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: s
# [reCAPTCHA](https://developers.google.com/recaptcha/docs/verify#error_code_reference)
# [hCaptcha](https://docs.hcaptcha.com/#siteverify-error-codes-table)
# [Turnstile](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes)
retriable_error_codes = [
retryable_error_codes = [
"missing-input-response",
"invalid-input-response",
"timeout-or-duplicate",
@@ -66,7 +66,7 @@ def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: s
"already-seen-response",
]
if set(error_codes).issubset(set(retriable_error_codes)):
if set(error_codes).issubset(set(retryable_error_codes)):
error_message = _("Invalid captcha response. Retrying may solve this issue.")
else:
error_message = _("Invalid captcha response")

View File

@@ -80,7 +80,7 @@ class ConsentStageView(ChallengeStageView):
def should_always_prompt(self) -> bool:
"""Check if the current request should require a prompt for non consent reasons,
i.e. this stage injected from another stage, mode is always requireed or no application
i.e. this stage injected from another stage, mode is always required or no application
is set."""
current_stage: ConsentStage = self.executor.current_stage
# Make this StageView work when injected, in which case `current_stage` is an instance

View File

@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2026.2.0-rc1 Blueprint schema",
"title": "authentik 2026.5.0-rc1 Blueprint schema",
"required": [
"version",
"entries"
@@ -10759,6 +10759,10 @@
"type": "boolean",
"title": "Sign logout request"
},
"sign_logout_response": {
"type": "boolean",
"title": "Sign logout response"
},
"sp_binding": {
"type": "string",
"enum": [

2
go.mod
View File

@@ -30,7 +30,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.35.0
golang.org/x/sync v0.19.0

4
go.sum
View File

@@ -218,6 +218,10 @@ goauthentik.io/api/v3 v3.2026020.17-0.20260205232234-280022b0a8de h1:X1ELA34R1N+
goauthentik.io/api/v3 v3.2026020.17-0.20260205232234-280022b0a8de/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535 h1:DPk8z6SGesp0gbmaD2zTAKVSd/NQ++Nu+lu3UrCkNvE=
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
goauthentik.io/api/v3 v3.2026020.17-0.20260211005401-cdd71ec2f62f h1:KK5lBHSvZSlMbUViB7KStlkP9kC1t9JeiMawa7wyI6Q=
goauthentik.io/api/v3 v3.2026020.17-0.20260211005401-cdd71ec2f62f/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393 h1:eLRd2GC+pxvwd3m2msJRNB9upH7pcIZH5V4L9/WhRcw=
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=

View File

@@ -1 +1 @@
2026.2.0-rc1
2026.5.0-rc1

View File

@@ -18,7 +18,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2026.2.0-rc1
Default: 2026.5.0-rc1
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

View File

@@ -31,7 +31,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.0-rc1}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
@@ -53,7 +53,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.0-rc1}
restart: unless-stopped
shm_size: 512mb
user: root

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.5.0-rc1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.5.0-rc1",
"dependencies": {
"@eslint/js": "^9.39.1",
"@goauthentik/eslint-config": "./packages/eslint-config",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.5.0-rc1",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,15 +1,15 @@
[project]
name = "authentik"
version = "2026.2.0-rc1"
version = "2026.5.0-rc1"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.14.*"
dependencies = [
"ak-guardian==3.2.0",
"argon2-cffi==25.1.0",
"cachetools==7.0.0",
"cachetools==7.0.1",
"channels==4.3.2",
"cryptography==46.0.4",
"cryptography==46.0.5",
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
@@ -37,7 +37,7 @@ dependencies = [
"fido2==2.1.1",
"geoip2==5.2.0",
"geopy==2.4.1",
"google-api-python-client==2.189.0",
"google-api-python-client==2.190.0",
"gssapi==1.11.1",
"gunicorn==25.0.3",
"jsonpatch==1.33",
@@ -69,7 +69,7 @@ dependencies = [
"urllib3<3",
"uvicorn[standard]==0.40.0",
"watchdog==6.0.0",
"webauthn==2.7.0",
"webauthn==2.7.1",
"wsproto==1.3.2",
"xmlsec==1.3.17",
"zxcvbn==4.5.0",

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2026.2.0-rc1
version: 2026.5.0-rc1
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@@ -18677,6 +18677,10 @@ paths:
name: sign_logout_request
schema:
type: boolean
- in: query
name: sign_logout_response
schema:
type: boolean
- in: query
name: sign_response
schema:
@@ -19863,6 +19867,10 @@ paths:
name: sign_logout_request
schema:
type: boolean
- in: query
name: sign_logout_response
schema:
type: boolean
- in: query
name: sign_response
schema:
@@ -40696,8 +40704,7 @@ components:
logout_urls:
type: array
items:
type: object
additionalProperties: {}
$ref: '#/components/schemas/LogoutURL'
IframeLogoutChallengeResponseRequest:
type: object
description: Response for iframe logout
@@ -42393,6 +42400,29 @@ components:
required:
- challenge
- name
LogoutURL:
type: object
description: Data for a single logout URL
properties:
url:
type: string
provider_name:
type: string
nullable: true
binding:
type: string
nullable: true
saml_request:
type: string
nullable: true
saml_response:
type: string
nullable: true
saml_relay_state:
type: string
nullable: true
required:
- url
MDMConfigRequest:
type: object
description: Base serializer class which doesn't implement create/update methods
@@ -42956,21 +42986,23 @@ components:
type: array
items:
$ref: '#/components/schemas/ErrorDetail'
post_url:
type: string
saml_request:
type: string
relay_state:
type: string
provider_name:
type: string
binding:
type: string
redirect_url:
type: string
is_complete:
type: boolean
default: false
post_url:
type: string
redirect_url:
type: string
saml_binding:
$ref: '#/components/schemas/SAMLBindingsEnum'
saml_request:
type: string
saml_response:
type: string
saml_relay_state:
type: string
NativeLogoutChallengeResponseRequest:
type: object
description: Response for native browser logout
@@ -49905,6 +49937,8 @@ components:
type: boolean
sign_logout_request:
type: boolean
sign_logout_response:
type: boolean
sp_binding:
allOf:
- $ref: '#/components/schemas/SAMLBindingsEnum'
@@ -53397,6 +53431,8 @@ components:
type: boolean
sign_logout_request:
type: boolean
sign_logout_response:
type: boolean
sp_binding:
allOf:
- $ref: '#/components/schemas/SAMLBindingsEnum'
@@ -53591,6 +53627,8 @@ components:
type: boolean
sign_logout_request:
type: boolean
sign_logout_response:
type: boolean
sp_binding:
allOf:
- $ref: '#/components/schemas/SAMLBindingsEnum'

View File

@@ -2,7 +2,7 @@
set -e -x -o pipefail
hash="$(git rev-parse HEAD || openssl rand -base64 36 | sha256sum)"
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
AUTHENTIK_IMAGE="authentik.invalid/goauthentik/server"
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
if [ -f lifecycle/container/.env ]; then
@@ -24,7 +24,7 @@ if [[ -v BUILD ]]; then
make gen-client-go
touch lifecycle/container/.env
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" -f lifecycle/container/Dockerfile .
fi
docker compose -f lifecycle/container/compose.yml up --no-start

127
uv.lock generated
View File

@@ -192,15 +192,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
]
[[package]]
name = "asn1crypto"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" },
]
[[package]]
name = "async-generator"
version = "1.10"
@@ -221,7 +212,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2026.2.0rc1"
version = "2026.5.0rc1"
source = { editable = "." }
dependencies = [
{ name = "ak-guardian" },
@@ -336,9 +327,9 @@ dev = [
requires-dist = [
{ name = "ak-guardian", editable = "packages/ak-guardian" },
{ name = "argon2-cffi", specifier = "==25.1.0" },
{ name = "cachetools", specifier = "==7.0.0" },
{ name = "cachetools", specifier = "==7.0.1" },
{ name = "channels", specifier = "==4.3.2" },
{ name = "cryptography", specifier = "==46.0.4" },
{ name = "cryptography", specifier = "==46.0.5" },
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
@@ -366,7 +357,7 @@ requires-dist = [
{ name = "fido2", specifier = "==2.1.1" },
{ name = "geoip2", specifier = "==5.2.0" },
{ name = "geopy", specifier = "==2.4.1" },
{ name = "google-api-python-client", specifier = "==2.189.0" },
{ name = "google-api-python-client", specifier = "==2.190.0" },
{ name = "gssapi", specifier = "==1.11.1" },
{ name = "gunicorn", specifier = "==25.0.3" },
{ name = "jsonpatch", specifier = "==1.33" },
@@ -398,7 +389,7 @@ requires-dist = [
{ name = "urllib3", specifier = "<3" },
{ name = "uvicorn", extras = ["standard"], specifier = "==0.40.0" },
{ name = "watchdog", specifier = "==6.0.0" },
{ name = "webauthn", specifier = "==2.7.0" },
{ name = "webauthn", specifier = "==2.7.1" },
{ name = "wsproto", specifier = "==1.3.2" },
{ name = "xmlsec", specifier = "==1.3.17" },
{ name = "zxcvbn", specifier = "==4.5.0" },
@@ -710,11 +701,11 @@ wheels = [
[[package]]
name = "cachetools"
version = "7.0.0"
version = "7.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" },
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
]
[[package]]
@@ -932,55 +923,55 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.4"
version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
{ url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
{ url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
{ url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
{ url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
{ url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
{ url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
{ url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
{ url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
{ url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
{ url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
{ url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
{ url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
{ url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
{ url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
{ url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
{ url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
{ url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
{ url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
{ url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
{ url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
{ url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
{ url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
{ url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
{ url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
{ url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
{ url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
{ url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
{ url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
{ url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
{ url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
{ url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
{ url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
{ url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
{ url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
{ url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
{ url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
]
[[package]]
@@ -1655,7 +1646,7 @@ wheels = [
[[package]]
name = "google-api-python-client"
version = "2.189.0"
version = "2.190.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
@@ -1664,9 +1655,9 @@ dependencies = [
{ name = "httplib2" },
{ name = "uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/f8/0783aeca3410ee053d4dd1fccafd85197847b8f84dd038e036634605d083/google_api_python_client-2.189.0.tar.gz", hash = "sha256:45f2d8559b5c895dde6ad3fb33de025f5cb2c197fa5862f18df7f5295a172741", size = 13979470, upload-time = "2026-02-03T19:24:55.432Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/4ab3e3516b93bb50ed7814738ea61d49cba3f72f4e331dc9518ae2731e92/google_api_python_client-2.190.0.tar.gz", hash = "sha256:5357f34552e3724d80d2604c8fa146766e0a9d6bb0afada886fafed9feafeef6", size = 14111143, upload-time = "2026-02-12T00:38:03.37Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl", hash = "sha256:a258c09660a49c6159173f8bbece171278e917e104a11f0640b34751b79c8a1a", size = 14547633, upload-time = "2026-02-03T19:24:52.845Z" },
{ url = "https://files.pythonhosted.org/packages/07/ad/223d5f4b0b987669ffeb3eadd7e9f85ece633aa7fd3246f1e2f6238e1e05/google_api_python_client-2.190.0-py3-none-any.whl", hash = "sha256:d9b5266758f96c39b8c21d9bbfeb4e58c14dbfba3c931f7c5a8d7fdcd292dd57", size = 14682070, upload-time = "2026-02-12T00:38:00.974Z" },
]
[[package]]
@@ -4003,17 +3994,17 @@ wheels = [
[[package]]
name = "webauthn"
version = "2.7.0"
version = "2.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asn1crypto" },
{ name = "cbor2" },
{ name = "cryptography" },
{ name = "pyasn1" },
{ name = "pyopenssl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a4/f0/e1036df8842782a2947e5f41e76a4accb92e3dba972dba882321ebe15af0/webauthn-2.7.0.tar.gz", hash = "sha256:3c45c25e75a7d7d419220ccd10b8b899984de8012732e10d898f0a8f8c480575", size = 123770, upload-time = "2025-09-04T23:19:21.602Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/f4/9529bcf85ef46c76842b84c66ffa8ec31f18e3aacd1330b62f440077b45b/webauthn-2.7.1.tar.gz", hash = "sha256:2a1ebbfffc4a83e31d3db5d69113944bc49d05fae77770c2d4e388386cb9656e", size = 124256, upload-time = "2026-02-11T23:36:02.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/b1/3f380d02552f1d75d3db789f761a1ee0dafd6181ebc07dd4b9ded61225a4/webauthn-2.7.0-py3-none-any.whl", hash = "sha256:2ecfee7959b09ebeaaffee9f8982ecdbbdc369a11766d20d4bc0637b36e235b7", size = 71311, upload-time = "2025-09-04T23:19:20.269Z" },
{ url = "https://files.pythonhosted.org/packages/90/5b/f73513367a9d34b199de916b44306acfa4027b57f7e22200421212b1f763/webauthn-2.7.1-py3-none-any.whl", hash = "sha256:d57e9613c65e0c6a4db7ee715fb49ebdf3c4a6eb3979729eeb497c99105e8181", size = 71684, upload-time = "2026-02-11T23:36:00.864Z" },
]
[[package]]

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.5.0-rc1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.5.0-rc1",
"license": "MIT",
"workspaces": [
"./packages/*"

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.5.0-rc1",
"license": "MIT",
"private": true,
"scripts": {

View File

@@ -243,8 +243,10 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
name="minReviewersIsPerGroup"
?checked=${this.instance?.minReviewersIsPerGroup ?? false}
label=${msg("Min reviewers is per-group")}
help=${msg(
"If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.",
.help=${msg(
html`If checked, approving a review will require at least that many users from
<em>each</em> of the selected groups. When disabled, the value is a total
across all groups.`,
)}
>
</ak-switch-input>

View File

@@ -72,6 +72,13 @@ function renderHasSigningKp(provider: Partial<SAMLProvider>) {
?checked=${provider?.signLogoutRequest ?? false}
help=${msg("When enabled, SAML logout requests will be signed.")}
>
</ak-switch-input>
<ak-switch-input
name="signLogoutResponse"
label=${msg("Sign logout response")}
?checked=${provider?.signLogoutResponse ?? false}
help=${msg("When enabled, SAML logout responses will be signed.")}
>
</ak-switch-input>`;
}

View File

@@ -53,6 +53,10 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
[EventActions.EmailSent, msg("Email sent")],
[EventActions.UpdateAvailable, msg("Update available")],
[EventActions.ExportReady, msg("Data export ready")],
[EventActions.ReviewInitiated, msg("Review initiated")],
[EventActions.ReviewOverdue, msg("Review overdue")],
[EventActions.ReviewAttested, msg("Review attested")],
[EventActions.ReviewCompleted, msg("Review completed")],
]);
export const actionToLabel = (action?: EventActions): string =>

View File

@@ -33,7 +33,7 @@ export class AkSwitchInput extends AKElement {
required = false;
@property({ type: String })
help = "";
help: string | TemplateResult = "";
/**
* For more complex help instructions, provide a template result.
@@ -47,11 +47,13 @@ export class AkSwitchInput extends AKElement {
#fieldID: string = IDGenerator.randomID();
protected renderHelp() {
const helpText = this.help.trim();
const helpContent = typeof this.help === "string" ? this.help.trim() : this.help;
return [
helpText
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">${helpText}</p>`
helpContent
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">
${helpContent}
</p>`
: nothing,
this.bighelp ? this.bighelp : nothing,
];

View File

@@ -5,6 +5,7 @@ import { BaseStage } from "#flow/stages/base";
import {
FlowChallengeResponseRequest,
IframeLogoutChallenge,
LogoutURL,
SAMLBindingsEnum,
} from "@goauthentik/api";
@@ -19,28 +20,26 @@ import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFProgress from "@patternfly/patternfly/components/Progress/progress.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
enum LogoutStatusStatus {
Pending = "pending",
Success = "success",
Error = "error",
}
interface LogoutStatus {
providerName: string;
status: "pending" | "success" | "error";
status: LogoutStatusStatus;
}
interface LogoutURLData {
url: string;
saml_request?: string;
provider_name?: string;
binding?: string;
}
function renderStatusIcon(status: string): TemplateResult | typeof nothing {
function renderStatusIcon(status: LogoutStatusStatus): TemplateResult | typeof nothing {
switch (status) {
case "pending":
case LogoutStatusStatus.Pending:
return html`<i class="fas fa-spinner pf-c-spinner status-icon status-pending"></i>`;
case "success":
case LogoutStatusStatus.Success:
return html`<i class="fas fa-check-circle status-icon status-success"></i>`;
case "error":
case LogoutStatusStatus.Error:
return html`<i class="fas fa-times-circle status-icon status-error"></i>`;
}
return nothing;
}
@customElement("ak-provider-iframe-logout")
@@ -110,12 +109,12 @@ export class IFrameLogoutStage extends BaseStage<
super.firstUpdated(changedProperties);
// Initialize status tracking
const logoutUrls = (this.challenge?.logoutUrls as LogoutURLData[]) || [];
const logoutUrls = (this.challenge?.logoutUrls as LogoutURL[]) || [];
this.logoutStatuses = logoutUrls.map(
(url): LogoutStatus => ({
providerName: url.provider_name || msg("Unknown Provider"),
status: "pending",
providerName: url.providerName || msg("Unknown Provider"),
status: LogoutStatusStatus.Pending,
}),
);
@@ -124,7 +123,7 @@ export class IFrameLogoutStage extends BaseStage<
}
protected async performLogouts(): Promise<void> {
const logoutUrls = (this.challenge?.logoutUrls as LogoutURLData[]) || [];
const logoutUrls = (this.challenge?.logoutUrls as LogoutURL[]) || [];
// Create iframes for each logout URL
logoutUrls.forEach((logoutData, index) => {
@@ -140,7 +139,7 @@ export class IFrameLogoutStage extends BaseStage<
}, 6000); // 6 seconds (5 second timeout + 1 second buffer)
}
protected createLogoutIframe(logoutData: LogoutURLData, index: number): void {
protected createLogoutIframe(logoutData: LogoutURL, index: number): void {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.name = `saml-logout-${index}`;
@@ -167,7 +166,10 @@ export class IFrameLogoutStage extends BaseStage<
});
// Handle based on binding type
if (logoutData.binding === SAMLBindingsEnum.Redirect || !logoutData.saml_request) {
if (
logoutData.binding === SAMLBindingsEnum.Redirect ||
(!logoutData.samlRequest && !logoutData.samlResponse)
) {
// For REDIRECT binding, just navigate the iframe to the URL
iframe.src = logoutData.url;
} else {
@@ -177,12 +179,29 @@ export class IFrameLogoutStage extends BaseStage<
form.action = logoutData.url;
form.target = iframe.name;
// Add SAML request
const samlInput = document.createElement("input");
samlInput.type = "hidden";
samlInput.name = "SAMLRequest";
samlInput.value = logoutData.saml_request;
form.appendChild(samlInput);
// Add SAML request OR response (depending on which is present)
if (logoutData.samlRequest) {
const samlInput = document.createElement("input");
samlInput.type = "hidden";
samlInput.name = "SAMLRequest";
samlInput.value = logoutData.samlRequest;
form.appendChild(samlInput);
} else if (logoutData.samlResponse) {
const samlInput = document.createElement("input");
samlInput.type = "hidden";
samlInput.name = "SAMLResponse";
samlInput.value = logoutData.samlResponse;
form.appendChild(samlInput);
}
// Add RelayState if present
if (logoutData.samlRelayState) {
const relayInput = document.createElement("input");
relayInput.type = "hidden";
relayInput.name = "RelayState";
relayInput.value = logoutData.samlRelayState;
form.appendChild(relayInput);
}
// Add to document and submit
document.body.appendChild(form);
@@ -198,7 +217,7 @@ export class IFrameLogoutStage extends BaseStage<
const statuses = [...this.logoutStatuses];
statuses[index] = {
...statuses[index],
status: success ? "success" : "error",
status: success ? LogoutStatusStatus.Success : LogoutStatusStatus.Error,
};
this.logoutStatuses = statuses;

View File

@@ -46,12 +46,12 @@ export class NativeLogoutStage extends BaseStage<
}
// If POST binding, auto-submit the form
if (this.challenge.binding === SAMLBindingsEnum.Post && this.#formRef.value) {
if (this.challenge.samlBinding === SAMLBindingsEnum.Post && this.#formRef.value) {
this.#formRef.value.submit();
}
// If redirect binding, perform the redirect
if (this.challenge.binding === SAMLBindingsEnum.Redirect) {
if (this.challenge.samlBinding === SAMLBindingsEnum.Redirect) {
if (!this.challenge.redirectUrl) {
throw new TypeError(`Binding challenge does not a have a redirect URL`);
}
@@ -78,38 +78,48 @@ export class NativeLogoutStage extends BaseStage<
}
// For redirect binding, just show loading and firstUpdated will redirect for us
if (this.challenge.binding === SAMLBindingsEnum.Redirect) {
if (this.challenge.samlBinding === SAMLBindingsEnum.Redirect) {
return html`<ak-flow-card .challenge=${this.challenge} loading>
<span slot="title">${msg(str`Redirecting to SAML provider: ${providerName}`)}</span>
</ak-flow-card>`;
}
if (this.challenge.binding !== SAMLBindingsEnum.Post) {
throw new TypeError(`Unknown challenge binding type ${this.challenge.binding}`);
if (this.challenge.samlBinding !== SAMLBindingsEnum.Post) {
throw new TypeError(`Unknown challenge binding type ${this.challenge.samlBinding}`);
}
// For POST binding, render auto-submit form
if (this.challenge.binding === SAMLBindingsEnum.Post) {
if (this.challenge.samlBinding === SAMLBindingsEnum.Post) {
const title = this.challenge.samlResponse
? msg(str`Posting logout response to SAML provider: ${providerName}`)
: msg(str`Posting logout request to SAML provider: ${providerName}`);
return html`<ak-flow-card .challenge=${this.challenge} loading>
<span slot="title"
>${msg(str`Posting logout request to SAML provider: ${providerName}`)}</span
>
<span slot="title">${title}</span>
<form
class="pf-c-form"
action="${ifDefined(this.challenge.postUrl)}"
method="post"
${ref(this.#formRef)}
>
<input
type="hidden"
name="SAMLRequest"
value="${ifDefined(this.challenge.samlRequest)}"
/>
${this.challenge.relayState
${this.challenge.samlRequest
? html`<input
type="hidden"
name="SAMLRequest"
value="${this.challenge.samlRequest}"
/>`
: nothing}
${this.challenge.samlResponse
? html`<input
type="hidden"
name="SAMLResponse"
value="${this.challenge.samlResponse}"
/>`
: nothing}
${this.challenge.samlRelayState
? html`<input
type="hidden"
name="RelayState"
value="${this.challenge.relayState}"
value="${this.challenge.samlRelayState}"
/>`
: nothing}
</form>

View File

@@ -10321,6 +10321,15 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10361,6 +10361,15 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -8202,6 +8202,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10281,6 +10281,15 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10537,6 +10537,15 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10526,6 +10526,15 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10229,6 +10229,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10517,6 +10517,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9861,6 +9861,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9519,6 +9519,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9889,6 +9889,15 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10510,6 +10510,15 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9977,6 +9977,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9956,6 +9956,15 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10817,6 +10817,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9575,6 +9575,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s804ab712b1add3d3">
<source>An unknown error occurred while submitting the form.</source>
</trans-unit>
<trans-unit id="s78adb00480380ad9">
<source>Sign logout response</source>
</trans-unit>
<trans-unit id="s7faa5610b9493a92">
<source>When enabled, SAML logout responses will be signed.</source>
</trans-unit>
<trans-unit id="saa088a2d0f90d5a9">
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,4 +1,4 @@
FROM --platform=${BUILDPLATFORM} docker.io/library/node:25.6.0-trixie@sha256:3b6ef8bf8ea0d849a07b3a24b0f64f25381ba2c9abb8825bc424e92a87b5e797 AS docs-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/node:25.6.1-trixie@sha256:43d1f7ab99f6174684d79f7a574025c48946967ff8e63206ce770c3c81d65430 AS docs-builder
ENV NODE_ENV=production

View File

@@ -1,6 +1,8 @@
/// <reference types="docusaurus-theme-openapi-docs" />
/// <reference types="docusaurus-plugin-openapi-docs" />
// cspell:ignore persistence
declare module "@docusaurus/plugin-content-docs/src/sidebars/types" {
export * from "@docusaurus/plugin-content-docs/src/sidebars/types.ts";
}

View File

@@ -105,7 +105,7 @@ For example, you can create a binding for a specific group, and then [bind that
Flow-stage bindings can have policy bindings bound to them; this can be used to conditionally run or skip stages within a flow. There are two settings in a flow-stage binding that configure _when_ these policies are executed:
- **Evaluate when flow is planned**
Policies are evaluated when authentik creates a flow plan that contains a reference to all of the stages that the user will need to go through to complete the flow. In this case,user-specific attributes are only available if the user is already authentiticated before beginning the flow.
Policies are evaluated when authentik creates a flow plan that contains a reference to all of the stages that the user will need to go through to complete the flow. In this case,user-specific attributes are only available if the user is already authenticated before beginning the flow.
- **Evaluate when the stage is run**
Policies bound to a flow-stage binding are evaluated before the stage is run (i.e after the flow has started but before the stage is reached in the flow). Therefore the context with which policy bindings to the flow-stage binding are evaluated reflects the current state of the flow.

View File

@@ -12,7 +12,7 @@ The Mutual TLS stage enables authentik to use client certificates to enroll and
For mTLS, note that you should NOT use a globally known CA.
Using private PKI certificates that are trusted by the end-device is best practise. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via mTLS, and in addition you should implement [custom validation](../../flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
Using private PKI certificates that are trusted by the end-device is best practice. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via mTLS, and in addition you should implement [custom validation](../../flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
:::
## Reverse-proxy configuration

View File

@@ -17,4 +17,4 @@ When the user reaches this stage, they are redirected to a static URL.
When the user reaches this stage, they are redirected to a specified flow, retaining all [flow context](../../flow/context/index.mdx).
Optionally, untoggle the "Keep flow context" switch. If this is untoggled, all flow context is cleared with the exception of the [is_redirected](../../flow/context#is_redirected-flow-object) key.
Optionally, toggle the "Keep flow context" switch to "off". When this control is set to "off", all flow context is cleared with the exception of the [is_redirected](../../flow/context#is_redirected-flow-object) key.

View File

@@ -43,7 +43,7 @@ The main steps to configure your Google Workspace organization are:
A pop-up displays with the private key. The key can be saved to your computer as a JSON file. This key will be required when creating the Google Workspace provider in authentik.
:::info Allow key creation
By default, the Google Cloud organization policy `iam.disableSerivceAccountKeyCreation` prevents creating service account keys. To allow key creation:
By default, the Google Cloud organization policy `iam.disableServiceAccountKeyCreation` prevents creating service account keys. To allow key creation:
1. Navigate to **IAM & Admin** > **Organization Policies** and select the **Disable service account key creation** policy.
2. Click **Manage policy** and disable the policy.
3. Click **Set policy** to save your changes.
@@ -76,7 +76,7 @@ We do not recommend using an administrator account for the Delegated Subject use
The Delagated Subject user requires the following permissions:
##### Admin console privilieges
##### Admin console privileges
- Users
- Groups

View File

@@ -1,5 +1,5 @@
---
title: OAuth2/OpenID Connect front-channel and back-channel logout
title: Front-channel and back-channel logout
description: Configure front-channel and back-channel logout for OAuth2/OpenID Connect providers
authentik_version: "2025.8.0"
authentik_preview: true

View File

@@ -23,7 +23,7 @@ OAuth 2.0 is an authorization protocol that allows an application (the RP) to de
1. An authorization request is prepared by the RP and contains parameters for its implementation of OAuth and which data it requires, and then the User's browser is redirected to that URL.
2. The RP sends a request to authentik in the background to exchange the access code for an access token (and optionally a refresh token).
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorised (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorized (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
The image below shows a typical authorization code flow.
@@ -102,7 +102,7 @@ The flows and grant types used in this case are those used for a typical authori
The authorization code is for environments with both a Client and a application server, where the back and forth happens between the client and an app server (the logic lives on app server). The RP needs to authorise itself to the OP. Client ID (public, identifies which app is talking to it) and client secret (the password) that the RP uses to authenticate.
If you configure authentik to use "Offline access" then during the initial auth the OP sends two tokens, an access token (short-lived, hours, can be customised) and a refresh token (typically longer validity, days or infinite). The RP (the app) saves both tokens. When the access token is about to expire, the RP sends the saved refresh token back to the OP, and requests a new access token. When the refresh token itself is about to expire, the RP can also ask for a new refresh token. This can all happen without user interaction if you configured the offline access.
If you configure authentik to use "Offline access" then during the initial auth the OP sends two tokens, an access token (short-lived, hours, can be customized) and a refresh token (typically longer validity, days or infinite). The RP (the app) saves both tokens. When the access token is about to expire, the RP sends the saved refresh token back to the OP, and requests a new access token. When the refresh token itself is about to expire, the RP can also ask for a new refresh token. This can all happen without user interaction if you configured the offline access.
:::info
Starting with authentik 2024.2, applications only receive an access token. To receive a refresh token, both applications and authentik must be configured to request the `offline_access` scope. In authentik this can be done by selecting the `offline_access` Scope mapping in the provider settings.
@@ -183,6 +183,28 @@ This does _not_ apply to special scopes, as those are not configurable in the pr
- `user:email`: Allows read-only access to `/user`, including email address
- `read:org`: Allows read-only access to `/user/teams`, listing all the user's groups as teams.
### Email scope verification
In authentik releases prior to 2025.10, the email scope always set the `email_verified` claim to `True`. Since authentik does not have a single authoritative source to determine whether a user's email is actually verified, asserting this claim could have security implications. As of 2025.10, `email_verified` now defaults to `False`.
Some applications require this claim to be `True` in order to authenticate users. In those cases, you can create a custom email scope mapping (**Customization** > **Property Mappings**) that always returns `email_verified` as `True`:
```python
return {
"email": request.user.email,
"email_verified": True
}
```
For greater security guarantees, verify users' email addresses and store the verification status as a user attribute (for example, `email_verified` set to `True` or `False`). You can then configure the scope mapping to return this value dynamically:
```python
return {
"email": request.user.email,
"email_verified": request.user.attributes.get("email_verified", False)
}
```
## Signing & Encryption
[JWTs](https://jwt.io/introduction) created by authentik will always be signed.

View File

@@ -15,15 +15,7 @@ Scope mappings are used by the OAuth2 provider to map information from authentik
:::info Default value for `email_verified`
By default, authentik sets the `email_verified` claim to `False`, since it has no way to confirm whether a user's email is verified. Setting this claim to `True` by default could introduce unintended security risks.
Be aware that some applications might require this claim to be true to successfully authenticate users. In this case you should create a custom email scope mapping that returns `email_verified` as `True`, using the following expression:
```
return {
"email": user.email,
"email_verified": True,
}
```
Be aware that some applications might require this claim to be true to successfully authenticate users. See [Email scope verification](../oauth2/index.mdx#email-scope-verification) for more information.
:::
## Skip objects during synchronization

View File

@@ -51,7 +51,7 @@ A new connection is created every time an endpoint is selected in the [User Inte
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
The RAC provider utilizes [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
For a full list of possible connection configurations, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).

View File

@@ -66,8 +66,10 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
- **Expression**:
```python
return {
"private-key": "-----BEGIN SSH PRIVATE KEY-----
import textwrap
private_key = textwrap.dedent("""
-----BEGIN SSH PRIVATE KEY-----
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
@@ -75,7 +77,12 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
-----END SSH PRIVATE KEY-----",
-----END SSH PRIVATE KEY-----
""")
return {
"username": "<your_username>",
"private-key": private_key
}
```

View File

@@ -42,7 +42,7 @@ The RADIUS provider supports EAP-TLS and [PAP](https://en.wikipedia.org/wiki/Pas
<details>
<summary>RADIUS compatibility matrix for password-based authentication:</summary>
This table represents the password-hash compatibillity with various RADIUS protocols.
This table represents the password-hash compatibility with various RADIUS protocols.
<HashSupport />
</details>
@@ -61,7 +61,7 @@ For certificates, ensure that you use a client certificate and a server certific
For EAP-TLS, note that you should NOT use a globally known CA.
Using private PKI certificates that are trusted by the end-device is best practise. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via EAP-TLS, and in addition you should implement [custom validation](../../flows-stages/flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
Using private PKI certificates that are trusted by the end-device is best practice. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via EAP-TLS, and in addition you should implement [custom validation](../../flows-stages/flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
:::
### RADIUS attributes

View File

@@ -9,7 +9,7 @@ authentik SAML providers can be created either from scratch or by using SAML met
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to solely create the provider by navigating to **Applications** > **Providers** and clicking **Create**.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
2. Navigate to **Applications** > **Applications** and click **Create with provider** to create an application and provider pair.
3. On the **New application** page, define the application details, and then click **Next**.
4. Select **SAML Provider** as the **Provider Type**, and then click **Next**.
5. On the **Configure SAML Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
@@ -19,7 +19,7 @@ To create a provider along with the corresponding application that uses it for a
If you have exported SAML metadata from your SP, you can optionally create the authentik SAML provider by importing this metadata.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers** and click **Create** to create a provider.
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
3. Select **SAML Provider from Metadata** as the **Provider Type**, and then click **Next**.
4. On the **Create SAML Provider from Metadata** page, provide the configuration settings along with an SP metadata file and then click **Finish** to create the provider.
5. (Optional) Edit the created SAML provider and configure any further settings.
@@ -33,7 +33,7 @@ After an authentik SAML provider has been created via any of the above methods,
To download the metadata of an authentik SAML provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
2. Navigate to **Applications** > **Providers**.
3. Click the name of the provider you want metadata from to open its overview tab.
4. In the **Related objects** section, under **Metadata** click on **Download**. This will download the metadata XML file for that provider.
@@ -42,7 +42,7 @@ To download the metadata of an authentik SAML provider, follow these steps:
To view and optionally download the metadata of an authentik SAML provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
2. Navigate to **Applications** > **Providers**.
3. Click the name of the provider you want metadata from to open its overview tab.
4. Navigate to the **Metadata** tab.
5. The metadata for the provider will be shown in a codebox. You can optionally use the **Download** button to obtain the metadata as a file.

View File

@@ -1,56 +1,42 @@
---
title: Configure an SSF provider
authentik_version: "2025.2.0"
description: "How to create and configure an SSF provider in authentik"
authentik_enterprise: true
authentik_preview: true
tags:
- backchannel
- provider
tags: [Shared Signals Framework, SSF, Apple Business Manager, backchannel]
---
The workflow to implement an SSF provider as a [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers) for an application/provider pair is as follows:
Follow this workflow to create and configure an SSF provider for an application:
1. Create the SSF provider (which serves as the backchannel provider).
1. Create the SSF provider (which serves as the [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers)).
2. Create an OIDC provider (which serves as the protocol provider for the application).
3. Create the application, and assign both the OIDC provider and the SSF provider.
## Create the SSF provider
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
2. Click **Create**.
3. In the modal, select the **Provider Type** of **SSF**, and then click **Next**.
4. On the **New provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
5. Click **Finish** to create and save the provider.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
3. Select **Shared Signals Framework Provider** as the **Provider Type**, and then click **Next**.
4. On the **Create SSF Provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
5. Click **Finish** to create the provider.
## Create the OIDC provider
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
2. Click **Create**.
3. In the modal, select the **Provider Type** of **OIDC**, and then click **Next**.
4. Define the settings for the provider, and then click **Finish** to save the new provider.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
3. Select **OAuth2/OpenID Provider** as the **Provider Type**, and then click **Next**.
4. On the **Create OAuth2/OpenID Provider** page, provide the configuration settings and then click **Finish** to create the provider.
## Create the application
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Applications**.
2. Click **Create**.
3. Define the settings for the application:
- **Name**: define a descriptive name of the application.
- **Slug**: optionally define the internal application name used in URLs.
- **Group**: optionally select a group that you want to have access to this application.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Applications** and click **Create** to create an application.
3. Configure the following required settings for the application:
- **Name**: provide a descriptive name of the application.
- **Slug**: provide the application slug used in URLs.
- **Provider**: select the OIDC provider that you created.
- **Backchannel Providers**: select the SSF provider you created.
- **Policy engine mode**: define policy-based access.
- **UI Settings**: optionally define a launch URL, an icon, and other UI elements.
- **Backchannel Providers**: select the SSF provider that you created.
4. Click **Create** to save the new application.
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your list of Applications.
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your application list.

View File

@@ -1,49 +1,53 @@
---
title: Shared Signals Framework (SSF) Provider
sidebar_label: SSF Provider
description: "Overview of SSF and the authentik SSF provider"
authentik_version: "2025.2.0"
authentik_enterprise: true
authentik_preview: true
tags: [Shared Signals Framework, SSF, Apple Business Manager]
---
Shared Signals Framework (SSF) is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected, secure webhooks.
The Shared Signals Framework (SSF) provider allows you to integrate applications with the Shared Signals Framework protocol.
## About Shared Signals Framework
SSF is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected and secure webhooks.
In authentik, an SSF provider allows applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
The authentik SSF provider allows OIDC applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
Events in authentik that are tracked via SSF include when an MFA device is added or removed, logouts, sessions being revoked by Admin or user clicking logout, or credentials changed.
Refer to our documentation to learn how to [create a SSF provider](./create-ssf-provider.md).
## Example use cases
One important use case for SFF is to [integrate Apple Business Manager](https://integrations.goauthentik.io/device-management/apple/) or any of the Apple device management platforms with authentik, so that users can enroll their Apple devices using their authentik credentials. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
Another use case for SSF is when an Admin wants to know if a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
Another use case for SSF is when an administrator wants to know when a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
Another example use case is when an application uses SSF to subscribe to authorization events because the application needs to know if a user changed their password in authentik. If a user did change their password, then the application receives a POST request to write the fact that the password was changed.
## About using SSF in authentik
## Using the authentik SSF provider
Let's look at a few details about using SSF in authentik.
The SSF provider serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application.
The SSF provider in authentik serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application. Thus you will still need to [create a typical application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair) (using an OIDC provider), and when creating the application, assign the SSF provider as a backchannel provider.
Therefore you still need to [create a typical OIDC application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair), and when creating the application, assign the SSF provider as a [backchannel provider](../../applications/manage_apps#backchannel-providers).
When an authentik Admin [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
When an authentik administrator [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
### The application (the receiver)
Within the application, the admin creates an SSF stream (which comprises all the signals that the app wants to subscribe to) and defines the audience, called `aud` in the specification (the URL that identifies the stream). A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can change or delete the stream.
Within the application, the administrator creates an SSF stream which lists all the signals that the application wants to subscribe to, and defines the audience (`aud`), which is the URL that identifies the stream. A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can also change or delete the stream.
Note that authentik doesn't specify which events to subscribe to; instead the application defines which they want to listen for.
authentik does not specify which events to subscribe to; instead the application defines which events they want to listen for.
### authentik (the transmitter)
To configure authentik as a shared signals transmitter, the authentik Admin [creates a new provider](./create-ssf-provider), selecting the type "SSF", to serve as the backchannelprovider for the application.
To configure authentik as a shared signals transmitter, the authentik administrator [creates a new SSF provider](./create-ssf-provider), to serve as the backchannelprovider for the application.
When creating the SSF provider you will need to select a signing key. This is the key that the Security Event Tokens (SET) is signed with.
When creating the SSF provider you will need to select a signing key that is used to sign the Security Event Tokens (SET).
Optionally, you can specify a event retention time period: this value determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
Optionally, you can specify a event retention time period, which determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
:::info
:::note SET events
Be aware that the SET events are different events than those displayed in the authentik Admin interface under **Events**.
:::

View File

@@ -0,0 +1,38 @@
---
title: Create a WS-Federation provider
---
An authentik WS-Federation provider is typically created as part of an application/provider pair, using the steps below. You can also create a standalone provider, and then later assign an application to use it.
## Create a WS-Federation provider and application pair
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
3. On the **New application** page, define the application details, and then click **Next**.
4. Select **WS-Federation Provider** as the **Provider Type**, and then click **Next**.
5. On the **Configure WS-Federation Provider** page, provide a name for the provider, select an authorization flow, and the two required configuration settings:
- **Reply URL**: Enter the application callback URL, where the token should be sent. This is the specific endpoint on an RP (application) where an Identity Provider (STS) sends the security token and authentication response after after a successful log in.
- **Realm**: Enter the identifier (string) of the requesting realm; that is, the Relying Party (RP) or application receiving the token. Realm is similar to the SAML 2.0 Entity ID.
6. Click **Submit** to create both the application and the provider.
## Export authentik WS-Federation provider metadata
After an authentik WS-Federation provider has been created via any of the above methods, you can access its metadata in one of two ways:
### Download authentik metadata for a WS-Federation provider
To download the metadata of an authentik WS-Federation provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
3. Click the name of the provider you want metadata for.
4. On the **Overview** tab, in the **Related objects** section, click on **Download** under **Metadata**. This will download the metadata XML file for that provider.
### Access the Metadata tab for a WS-Federation provider
To view and optionally download the metadata of an authentik WS-Federation provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
3. Click the name of the provider you want metadata for, and then click the the **Metadata** tab.
4. The metadata for the provider will be shown in a codebox. You can optionally use the **Download** button to obtain the metadata as a file.

View File

@@ -0,0 +1,53 @@
---
title: WS-Federation Provider
---
The WS-Federation provider is used to integrate with applications and service providers that use [WS-Federation protocol](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adfsod/204de335-ea34-4f9b-ae73-8b7d4c8152d1). WS-Fedederation is an XML-based identity federation protocol that uses token exchange for federated Single Sign-On (SSO) and IdP authentication, specifically for Windows applications such as Sharepoint.
There are similarities between WS-Federation and SAML protocols, but there are several key differences in terminology, most importantly:
- WS-Federation term: **STS (Security Token Service)**
- SAML term: **IdP (Identity Provider)**
:::info SAML2 token support
Note that we only support the SAML2 token type within WS-Federation providers, and that using the WS-Federation provider with Entra ID is not supported because Entra ID requires a SAML 1.0 token.
:::
## Supported URL request parameters
The following URL request parameters are supported in the authentik WS-Federation provider:
- **wa**: The is a required parameter that represents the action being requested, typically wsignin1.0 for signing in. The parameter's value tells the Security Token Service (STS) which operation to execute.
- **wtrealm**: The unique identifier (realm) of the Relying Party (RP) or application requesting the security token, for example, urn:my-app:rp. It defines the trust relationship between the RP and the Identity Provider (IdP) and indicates which application is initiating the WS-Federation request. This is a required query parameter that tells the Security Token Service (STS) which relying party the token is intended for.
- **wreply**: The target URL to which the Identity Provider (IdP) sends the WS-Federation response containing the security token. This URL is supplied by the Service Provider (SP). authentik verifies that the received `wreply` parameter matches the URL configured by the administrator and stored in the database.
- **wctx**: A context value that is used to maintain state between the Relying Party (RP) and the Identity Provider (IdP) across redirects. It serves the same purpose as the `RelayState` parameter in SAML. The RP includes this value in the authentication request, and the IdP returns it unchanged in the response, allowing the RP to validate and restore the original session or request context.
## WS-Federation bindings and endpoints
_Bindings_ define how an Identity Provider (IdP) and the WS-Federation STS (Security Token Service), or IdP in SAML terms, communicate; how messages are transported over network protocols, specifying transport (like HTTP), encoding, and security detail that allow WS-Federation to facilitate secure identity sharing across systems. Both the IdP and STS define various endpoints in their metadata, each associated with a specific WS-Federation binding.
| Endpoint | URL |
| -------- | --------------------- |
| SSO/SLO | `/application/wsfed/` |
## WS-Federation metadata
Using metadata ensures that WS-Federation single sign-on works reliably by exchanging and maintaining identity and connection information. WS-Federation metadata is an XML document that defines how IdPs and SPs securely interact for authentication. It includes information such as endpoints, bindings, certificates, and unique identifiers. The metadata is what you provide the application to configure it for authenticating with authentik.
You can [export WS-Federation metadata](./create-wsfed-provider.md#export-authentik-ws-federation-provider-metadata) from an authentik WS-Federation provider to an STS to automatically provide important endpoint and certificate information to the SP.
## Certificates
The certificates used with WS-Federation to sign Request Security Token Response (RSTR), which contains the assertion, are the same certificates that are used by SAML.
For details, refer to our [SAML certificates documentation](../saml/index.md#certificates).
## WS-Federation property mappings
Property mappings are used during the authentication process to align, or "map", user attributes values between the SP and STS (Security Token Service), the latter being the equivalent of SAML's IdP.
The same property mappings that are used in WS-Federation are used in SAML. For details, refer to our [SAML property mapping documentation](../saml/index.md#certificates).
## Attributes for WS-Federation
Ws-Federation and SAML also share the use of the [NameID](../saml/index.md#nameid) and the [AuthnContextClassRef](../saml/index.md#authncontextclassref) attributes.

View File

@@ -8,7 +8,7 @@ To migrate existing configurations to blueprints, run `ak export_blueprint` with
Exported blueprints don't use any of the YAML Tags, they just contain a list of entries as they are in the database.
Note that fields which are write-only (for example, OAuth Provider's Secret Key) will not be added to the blueprint, as the serialisation logic from the API is used for blueprints.
Note that fields which are write-only (for example, OAuth Provider's Secret Key) will not be added to the blueprint, as the serialization logic from the API is used for blueprints.
Additionally, default values will be skipped and not added to the blueprint.

View File

@@ -0,0 +1,3 @@
---
title: Enterprise Features
---

View File

@@ -429,7 +429,7 @@ Overrides [`AUTHENTIK_STORAGE__FILE__[...]`](#file-storage-backend-settings) set
#### `AUTHENTIK_STORAGE__MEDIA__S3__[...]`
Overrides [`AUTHENTIK_STORAGE__FILE__[...]`](#file-storage-backend-settings) settings.
Overrides [`AUTHENTIK_STORAGE__S3__[...]`](#s3-storage-backend-settings) settings.
These settings affect where media files are stored. Those files include applications and sources icons. By default, they use the same storage settings as the main storage configuration. S3 storage is also supported.

View File

@@ -81,7 +81,7 @@ Every application that you add to authentik requires a provider, which is used t
- **Configure the Application**:
- **Name**: provide a descriptive name (such as Grafana).
- **Group**: select an optional group for the application; groups are used to visually separate applications. For example, you can choose to group applications that you use for coding from those you use for internal communication.
- **Policy engine mode**: select **Any** for this tutorial; the mode determnes how strictly policies are adhered to.
- **Policy engine mode**: select **Any** for this tutorial; the mode determines how strictly policies are adhered to.
- <strong className="tip">TIP</strong>: in authentik,
[policies](../../customize/policies/working_with_policies.md) are used in authentik to
fine-tune access to applications, flows, stages and many other authentik components. It is

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