Compare commits

..

40 Commits

Author SHA1 Message Date
authentik-automation[bot]
15ad260333 core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1770992049 (#20285)
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-13 14:43:27 +00:00
Marcelo Elizeche Landó
b76539e73f stage/invitation: Send invite via email UI (#19823)
* first approach

* add cc and bcc support, better ui

* remove unnecessary data return

* add template support

* fix linting

* do the ui

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* display invite info in InvitationSendEmailForm.ts

* Select the invitation template by default

* Fix linting

* fix tests

* Add tests, clean code

* Add docs

* fix link

* Make the UI less disgusting

* Make the UI less disgusting

* Apply suggestions from code review

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* small formatting fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Use writeToClipboard function, better wording for CC and BCC

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-13 11:00:31 -03:00
Simonyi Gergő
c205a41cb5 root: remove unused django-cte (#20090) 2026-02-13 14:10:22 +01:00
dependabot[bot]
32f2d3ad30 core: bump ruff from 0.15.0 to 0.15.1 (#20273)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 12:54:07 +00:00
authentik-automation[bot]
a9e382d6c5 core, web: update translations (#20271)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-13 13:48:42 +01:00
dependabot[bot]
a35005416b ci: bump docker/build-push-action from 6.19.1 to 6.19.2 (#20274)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 13:45:02 +01: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
245 changed files with 2963 additions and 1168 deletions

View File

@@ -58,7 +58,7 @@ runs:
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/compose.yml up -d
cd web && npm ci
cd web && npm i
- name: Generate config
if: ${{ contains(inputs.dependencies, 'python') }}
shell: uv run python {0}

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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
id: push
with:
push: true

View File

@@ -148,11 +148,11 @@ bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
ifndef version
$(error Usage: make bump version=20xx.xx.xx )
endif
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' pyproject.toml
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
$(MAKE) gen-build gen-compose aws-cfn
$(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
npm version --no-git-tag-version --allow-same-version $(version)
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
echo -n $(version) > ${PWD}/internal/constants/VERSION
#########################

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2026.2.0-rc4"
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

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

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

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

@@ -40,6 +40,10 @@ class EmailTemplates(models.TextChoices):
"email/event_notification.html",
_("Event Notification"),
)
INVITATION = (
"email/invitation.html",
_("Invitation"),
)
def get_template_choices():

View File

@@ -0,0 +1,55 @@
{% extends "email/base.html" %}
{% load i18n %}
{% load humanize %}
{% block content %}
<tr>
<td align="center">
<h1>
{% blocktrans %}
You're Invited!
{% endblocktrans %}
</h1>
</td>
</tr>
<tr>
<td align="center">
<table border="0">
<tr>
<td align="center" style="max-width: 300px; padding: 20px 0; color: #212124;">
{% blocktrans %}
You have been invited to join {{ host }}. Click the button below to get started.
{% endblocktrans %}
</td>
</tr>
{% if expires %}
<tr>
<td align="center" style="max-width: 300px; padding: 10px 0; color: #212124; font-size: 12px;">
{% blocktrans with expires=expires|naturaltime %}
This invitation expires {{ expires }}.
{% endblocktrans %}
</td>
</tr>
{% endif %}
<tr>
<td align="center" class="btn btn-primary">
<a id="confirm" href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Accept Invitation' %}</a>
</td>
</tr>
</table>
</td>
</tr>
{% endblock %}
{% block sub_content %}
<tr>
<td style="padding: 20px; font-size: 12px; color: #212124;" align="center">
{% blocktrans %}
If you cannot click the button above, please copy and paste the following URL into your browser:
{% endblocktrans %}
<br>
<a href="{{ url }}" rel="noopener noreferrer" target="_blank">{{ url }}</a>
</td>
</tr>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% load i18n %}
{% load humanize %}
{% blocktrans %}You're Invited!{% endblocktrans %}
{% blocktrans %}You have been invited to join {{ host }}. Use the link below to get started.{% endblocktrans %}
{% trans 'Accept Invitation' %}: {{ url }}
{% if expires %}
{% blocktrans with expires=expires|naturaltime %}This invitation expires {{ expires }}.{% endblocktrans %}
{% endif %}
{% blocktrans %}If you cannot click the link above, please copy and paste the following URL into your browser:{% endblocktrans %}
{{ url }}

View File

@@ -54,7 +54,7 @@ class TestEmailStageTemplates(FlowTestCase):
chmod(file2, 0o000) # Remove all permissions so we can't read the file
choices = get_template_choices()
self.assertEqual(choices[-1][0], Path(file).name)
self.assertEqual(len(choices), 5)
self.assertEqual(len(choices), 6)
unlink(file)
unlink(file2)

View File

@@ -1,10 +1,21 @@
"""Invitation Stage API Views"""
from django.http import HttpRequest
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import PrimaryKeyRelatedField
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
CharField,
ListField,
PrimaryKeyRelatedField,
Serializer,
)
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.groups import PartialUserSerializer
@@ -13,8 +24,11 @@ from authentik.core.api.utils import JSONDictField, ModelSerializer
from authentik.core.models import User
from authentik.flows.api.flows import FlowSerializer
from authentik.flows.api.stages import StageSerializer
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.stages.invitation.models import Invitation, InvitationStage
LOGGER = get_logger()
class InvitationStageSerializer(StageSerializer):
"""InvitationStage Serializer"""
@@ -77,6 +91,15 @@ class InvitationSerializer(ModelSerializer):
]
class InvitationSendEmailSerializer(Serializer):
"""Serializer for sending invitation emails"""
email_addresses = ListField(required=True)
cc_addresses = ListField(required=False)
bcc_addresses = ListField(required=False)
template = CharField(required=False, default="invitation")
class InvitationViewSet(UsedByMixin, ModelViewSet):
"""Invitation Viewset"""
@@ -91,3 +114,61 @@ class InvitationViewSet(UsedByMixin, ModelViewSet):
if SERIALIZER_CONTEXT_BLUEPRINT not in serializer.context:
kwargs["created_by"] = self.request.user
serializer.save(**kwargs)
@extend_schema(
request=InvitationSendEmailSerializer,
responses={204: None},
)
@action(
detail=True,
methods=["post"],
serializer_class=InvitationSendEmailSerializer,
)
def send_email(self, request: Request, pk: str) -> Response:
"""Send invitation link via email to one or more addresses"""
invitation = self.get_object()
email_addresses = request.data.get("email_addresses", [])
cc_addresses = request.data.get("cc_addresses", [])
bcc_addresses = request.data.get("bcc_addresses", [])
template = request.data.get("template", "email/invitation.html")
if not email_addresses:
return Response({"error": "No email addresses provided"}, status=400)
# Build the invitation link
http_request: HttpRequest = request._request
protocol = "https" if http_request.is_secure() else "http"
host = http_request.get_host()
# Determine the flow slug
flow_slug = invitation.flow.slug if invitation.flow else None
if not flow_slug:
return Response({"error": "Invitation has no associated flow"}, status=400)
invitation_link = f"{protocol}://{host}/if/flow/{flow_slug}/?itoken={invitation.pk}"
# Prepare template context
context = {
"url": invitation_link,
"expires": invitation.expires,
"host": host,
}
# Prepare email content
subject = f"You have been invited to {host}"
# Queue emails for sending via async ak_send_email
evaluator = BaseEvaluator()
for email in email_addresses:
evaluator.expr_send_email(
address=email,
subject=subject,
template=template,
context=context,
stage=None,
cc=cc_addresses if cc_addresses else None,
bcc=bcc_addresses if bcc_addresses else None,
)
return Response(status=204)

View File

@@ -217,3 +217,105 @@ class TestInvitationsAPI(APITestCase):
self.assertEqual(invitation.created_by, get_anonymous_user())
self.assertEqual(invitation.name, "test-blueprint-invitation")
self.assertEqual(invitation.fixed_data, {"email": "test@example.com"})
def test_send_email_no_addresses(self):
"""Test send_email endpoint with no email addresses"""
flow = create_test_flow(FlowDesignation.ENROLLMENT)
invite = Invitation.objects.create(
name="test-invite",
created_by=self.user,
flow=flow,
)
response = self.client.post(
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
{"email_addresses": []},
format="json",
)
self.assertEqual(response.status_code, 400)
self.assertIn("error", response.data)
def test_send_email_no_flow(self):
"""Test send_email endpoint with invitation without flow"""
invite = Invitation.objects.create(
name="test-invite-no-flow",
created_by=self.user,
flow=None,
)
response = self.client.post(
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
{"email_addresses": ["test@example.com"]},
format="json",
)
self.assertEqual(response.status_code, 400)
self.assertIn("error", response.data)
@patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email")
def test_send_email_success(self, mock_send_email: MagicMock):
"""Test send_email endpoint successfully queues emails"""
flow = create_test_flow(FlowDesignation.ENROLLMENT)
invite = Invitation.objects.create(
name="test-invite",
created_by=self.user,
flow=flow,
)
response = self.client.post(
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
{
"email_addresses": ["user1@example.com", "user2@example.com"],
"template": "email/invitation.html",
},
format="json",
)
self.assertEqual(response.status_code, 204)
self.assertEqual(mock_send_email.call_count, 2)
@patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email")
def test_send_email_with_cc_bcc(self, mock_send_email: MagicMock):
"""Test send_email endpoint with CC and BCC addresses"""
flow = create_test_flow(FlowDesignation.ENROLLMENT)
invite = Invitation.objects.create(
name="test-invite",
created_by=self.user,
flow=flow,
)
response = self.client.post(
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
{
"email_addresses": ["user@example.com"],
"cc_addresses": ["cc@example.com"],
"bcc_addresses": ["bcc@example.com"],
"template": "email/invitation.html",
},
format="json",
)
self.assertEqual(response.status_code, 204)
mock_send_email.assert_called_once()
call_kwargs = mock_send_email.call_args.kwargs
self.assertEqual(call_kwargs["cc"], ["cc@example.com"])
self.assertEqual(call_kwargs["bcc"], ["bcc@example.com"])
@patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email")
def test_send_email_context(self, mock_send_email: MagicMock):
"""Test send_email endpoint passes correct context to email"""
flow = create_test_flow(FlowDesignation.ENROLLMENT)
invite = Invitation.objects.create(
name="test-invite",
created_by=self.user,
flow=flow,
)
response = self.client.post(
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
{"email_addresses": ["user@example.com"]},
format="json",
)
self.assertEqual(response.status_code, 204)
mock_send_email.assert_called_once()
call_kwargs = mock_send_email.call_args.kwargs
self.assertIn("url", call_kwargs["context"])
self.assertIn(str(invite.pk), call_kwargs["context"]["url"])
self.assertIn(flow.slug, call_kwargs["context"]["url"])

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-rc4 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.20260213141435-0db2228fbd47
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.35.0
golang.org/x/sync v0.19.0

6
go.sum
View File

@@ -218,6 +218,12 @@ 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=
goauthentik.io/api/v3 v3.2026020.17-0.20260213141435-0db2228fbd47 h1:quNPFsxsMNKICxrJP3dFehxgCvt3Qi9UeV8HzcEk17c=
goauthentik.io/api/v3 v3.2026020.17-0.20260213141435-0db2228fbd47/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-rc4
2026.5.0-rc1

View File

@@ -18,7 +18,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2026.2.0-rc4
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-rc4}
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-rc4}
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.

Binary file not shown.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc4",
"version": "2026.5.0-rc1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc4",
"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-rc4",
"version": "2026.5.0-rc1",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,13 +1,13 @@
[project]
name = "authentik"
version = "2026.2.0-rc4"
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.5",
"dacite==1.9.2",
@@ -15,7 +15,6 @@ dependencies = [
"defusedxml==0.7.1",
"django-channels-postgres",
"django-countries==7.6.1",
"django-cte==3.0.0",
"django-dramatiq-postgres",
"django-filter==25.2",
"django-model-utils==5.0.0",
@@ -37,7 +36,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 +68,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",
@@ -103,7 +102,7 @@ dev = [
"pytest-timeout==2.4.0",
"pytest==9.0.2",
"requests-mock==1.12.1",
"ruff==0.15.0",
"ruff==0.15.1",
"selenium==4.40.0",
"types-channels==4.3.0.20250822",
"types-docker==7.1.0.20260109",

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2026.2.0-rc4
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:
@@ -30597,6 +30605,35 @@ paths:
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/stages/invitation/invitations/{invite_uuid}/send_email/:
post:
operationId: stages_invitation_invitations_send_email_create
description: Send invitation link via email to one or more addresses
parameters:
- in: path
name: invite_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Invitation.
required: true
tags:
- stages
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InvitationSendEmailRequest'
required: true
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/stages/invitation/invitations/{invite_uuid}/used_by/:
get:
operationId: stages_invitation_invitations_used_by_list
@@ -40696,8 +40733,7 @@ components:
logout_urls:
type: array
items:
type: object
additionalProperties: {}
$ref: '#/components/schemas/LogoutURL'
IframeLogoutChallengeResponseRequest:
type: object
description: Response for iframe logout
@@ -40845,6 +40881,25 @@ components:
description: When set, only the configured flow can use this invitation.
required:
- name
InvitationSendEmailRequest:
type: object
description: Serializer for sending invitation emails
properties:
email_addresses:
type: array
items: {}
cc_addresses:
type: array
items: {}
bcc_addresses:
type: array
items: {}
template:
type: string
minLength: 1
default: invitation
required:
- email_addresses
InvitationStage:
type: object
description: InvitationStage Serializer
@@ -42393,6 +42448,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 +43034,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 +49985,8 @@ components:
type: boolean
sign_logout_request:
type: boolean
sign_logout_response:
type: boolean
sp_binding:
allOf:
- $ref: '#/components/schemas/SAMLBindingsEnum'
@@ -53397,6 +53479,8 @@ components:
type: boolean
sign_logout_request:
type: boolean
sign_logout_response:
type: boolean
sp_binding:
allOf:
- $ref: '#/components/schemas/SAMLBindingsEnum'
@@ -53591,6 +53675,8 @@ components:
type: boolean
sign_logout_request:
type: boolean
sign_logout_response:
type: boolean
sp_binding:
allOf:
- $ref: '#/components/schemas/SAMLBindingsEnum'

91
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.0rc4"
version = "2026.5.0rc1"
source = { editable = "." }
dependencies = [
{ name = "ak-guardian" },
@@ -235,7 +226,6 @@ dependencies = [
{ name = "django" },
{ name = "django-channels-postgres" },
{ name = "django-countries" },
{ name = "django-cte" },
{ name = "django-dramatiq-postgres" },
{ name = "django-filter" },
{ name = "django-model-utils" },
@@ -336,7 +326,7 @@ 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.5" },
{ name = "dacite", specifier = "==1.9.2" },
@@ -345,7 +335,6 @@ requires-dist = [
{ name = "django", specifier = "==5.2.11" },
{ name = "django-channels-postgres", editable = "packages/django-channels-postgres" },
{ name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==3.0.0" },
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
{ name = "django-filter", specifier = "==25.2" },
{ name = "django-model-utils", specifier = "==5.0.0" },
@@ -366,7 +355,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 +387,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" },
@@ -432,7 +421,7 @@ dev = [
{ name = "pytest-randomly", specifier = "==4.0.1" },
{ name = "pytest-timeout", specifier = "==2.4.0" },
{ name = "requests-mock", specifier = "==1.12.1" },
{ name = "ruff", specifier = "==0.15.0" },
{ name = "ruff", specifier = "==0.15.1" },
{ name = "selenium", specifier = "==4.40.0" },
{ name = "types-channels", specifier = "==4.3.0.20250822" },
{ name = "types-docker", specifier = "==7.1.0.20260109" },
@@ -710,11 +699,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]]
@@ -1138,18 +1127,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/46/b6931858e5161e5d9166bfcfde3af0b7d60ba89e4f7dd8f033e591c68794/django_countries-7.6.1-py3-none-any.whl", hash = "sha256:1ed20842fe0f6194f91faca21076649513846a8787c9eb5aeec3cbe1656b8acc", size = 864507, upload-time = "2024-04-01T21:01:05.702Z" },
]
[[package]]
name = "django-cte"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/c0/64cda7c7b3e5641160a4c9dd1030b3a567592ba2b6c64f5303678a780084/django_cte-3.0.0.tar.gz", hash = "sha256:888710bb7109559621a34ab890f0f87d54188c9678f874e61e82112b59bbccb4", size = 11422, upload-time = "2026-02-05T13:08:53.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/8a/5fdf282b8496e485b007d48ac211a0e2b203b8623e67c32ed2cf65faedc4/django_cte-3.0.0-py3-none-any.whl", hash = "sha256:3eabb89b68d328a6a97c695a21f45ed6ee7a6602193670db74e70c2e17bf2cd5", size = 13211, upload-time = "2026-02-05T13:08:51.908Z" },
]
[[package]]
name = "django-dramatiq-postgres"
version = "0.1.0"
@@ -1655,7 +1632,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 +1641,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]]
@@ -3308,27 +3285,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.0"
version = "0.15.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
{ url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
{ url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
]
[[package]]
@@ -4003,17 +3980,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]]

77
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/web",
"version": "2026.2.0-rc4",
"version": "2026.5.0-rc1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/web",
"version": "2026.2.0-rc4",
"version": "2026.5.0-rc1",
"license": "MIT",
"workspaces": [
"./packages/*"
@@ -188,6 +188,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -2609,7 +2610,6 @@
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
@@ -3879,18 +3879,6 @@
}
}
},
"node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": {
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
}
},
"node_modules/@swagger-api/apidom-reference": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.1.tgz",
@@ -4029,6 +4017,7 @@
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
@@ -4358,8 +4347,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/chai": {
"version": "5.2.3",
@@ -4734,6 +4722,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz",
"integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4752,6 +4741,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4841,6 +4831,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@@ -5081,6 +5072,7 @@
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz",
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/browser": "4.0.18",
"@vitest/mocker": "4.0.18",
@@ -5546,6 +5538,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6088,6 +6081,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6376,6 +6370,7 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -6407,6 +6402,7 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -6677,6 +6673,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -7071,6 +7068,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -7231,6 +7229,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -7498,7 +7497,6 @@
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz",
"integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.20"
},
@@ -7511,7 +7509,6 @@
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
@@ -7562,8 +7559,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.3.1",
@@ -7858,6 +7854,7 @@
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -7951,6 +7948,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9242,7 +9240,6 @@
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz",
"integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
}
@@ -10852,6 +10849,7 @@
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
"integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@lit/reactive-element": "^2.1.0",
"lit-element": "^4.2.0",
@@ -11114,7 +11112,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -13558,6 +13555,7 @@
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright-core": "1.58.2"
},
@@ -13659,6 +13657,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -13693,7 +13692,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -13708,7 +13706,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -13720,8 +13717,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/prismjs": {
"version": "1.30.0",
@@ -13929,6 +13925,7 @@
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
"integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
@@ -14008,6 +14005,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14017,6 +14015,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -14553,6 +14552,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -15153,15 +15153,13 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.0.1.tgz",
"integrity": "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/sort-package-json": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.5.0.tgz",
"integrity": "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"detect-indent": "^7.0.1",
"detect-newline": "^4.0.1",
@@ -15296,6 +15294,7 @@
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.7.tgz",
"integrity": "sha512-LFKSuZyF6EW2/Kkl5d7CvqgwhXXfuWv+aLBuoc616boLKJ3mxXuea+GxIgfk02NEyTKctJ0QsnSh5pAomf6Qkg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^2.0.1",
@@ -15696,7 +15695,6 @@
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pkgr/core": "^0.2.9"
},
@@ -15902,18 +15900,6 @@
"node": ">=6"
}
},
"node_modules/tree-sitter": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
"integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0"
}
},
"node_modules/tree-sitter-json": {
"version": "0.24.8",
"resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz",
@@ -16156,6 +16142,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16169,6 +16156,7 @@
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
@@ -16587,6 +16575,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -16675,6 +16664,7 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
@@ -17367,6 +17357,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

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

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

@@ -1,4 +1,9 @@
import "#admin/stages/invitation/InvitationSendEmailForm";
import "#elements/forms/ModalForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { writeToClipboard } from "#common/clipboard";
import { AKElement } from "#elements/Base";
@@ -9,6 +14,7 @@ import { CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@@ -21,7 +27,7 @@ export class InvitationListLink extends AKElement {
@property()
selectedFlow?: string;
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList];
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList, PFButton];
renderLink(): string {
if (this.invitation?.flowObj) {
@@ -102,6 +108,35 @@ export class InvitationListLink extends AKElement {
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Actions")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<button
class="pf-c-button pf-m-secondary"
@click=${() => {
writeToClipboard(this.renderLink());
}}
>
${msg("Copy Link")}
</button>
<ak-forms-modal>
<span slot="submit">${msg("Send")}</span>
<span slot="header">${msg("Send Invitation via Email")}</span>
<ak-invitation-send-email-form
slot="form"
.invitation=${this.invitation}
>
</ak-invitation-send-email-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Send via Email")}
</button>
</ak-forms-modal>
</div>
</dd>
</div>
</dl>`;
}
}

View File

@@ -124,7 +124,7 @@ export class InvitationListPage extends TablePage<Invitation> {
</div>
<small>${item.createdBy.name}</small>`,
html`${item.expires?.toLocaleString() || msg("-")}`,
html` <ak-forms-modal>
html`<ak-forms-modal>
<span slot="submit">${msg("Update")}</span>
<span slot="header">${msg("Update Invitation")}</span>
<ak-invitation-form slot="form" .instancePk=${item.pk}> </ak-invitation-form>

View File

@@ -0,0 +1,167 @@
import "#elements/buttons/SpinnerButton/index";
import "#components/ak-textarea-input";
import { DEFAULT_CONFIG } from "#common/api/config";
import { MessageLevel } from "#common/messages";
import { Form } from "#elements/forms/Form";
import { showMessage } from "#elements/messages/MessageContainer";
import { type DescriptionPair, renderDescriptionList } from "#components/DescriptionList";
import { Invitation, StagesApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
interface InvitationSendEmailRequestWithTemplate {
emailAddresses: string;
ccAddresses?: string;
bccAddresses?: string;
template?: TypeCreate;
}
@customElement("ak-invitation-send-email-form")
export class InvitationSendEmailForm extends Form<InvitationSendEmailRequestWithTemplate> {
static get styles(): CSSResult[] {
return [...super.styles, PFDescriptionList];
}
@property({ attribute: false })
invitation?: Invitation;
@state()
availableTemplates: TypeCreate[] = [];
@state()
selectedTemplate = "email/invitation.html";
fetchAvailableTemplates = async (): Promise<void> => {
try {
this.availableTemplates = await new StagesApi(
DEFAULT_CONFIG,
).stagesEmailTemplatesList();
} catch (error) {
console.error("Failed to fetch email templates:", error);
}
};
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("ak-modal-show", this.fetchAvailableTemplates);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener("ak-modal-show", this.fetchAvailableTemplates);
}
parseEmailAddresses(addresses: string): string[] {
return addresses
.split(/[\n,;]/)
.map((email) => email.trim())
.filter((email) => email.length > 0);
}
async send(data: InvitationSendEmailRequestWithTemplate): Promise<void> {
const addresses = this.parseEmailAddresses(data.emailAddresses);
const ccAddresses = this.parseEmailAddresses(data.ccAddresses ?? "");
const bccAddresses = this.parseEmailAddresses(data.bccAddresses ?? "");
if (addresses.length === 0) {
showMessage({
message: msg("Please enter at least one email address"),
level: MessageLevel.error,
});
return;
}
try {
await new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsSendEmailCreate({
inviteUuid: this.invitation?.pk || "",
invitationSendEmailRequest: {
emailAddresses: addresses,
ccAddresses: ccAddresses.length > 0 ? ccAddresses : undefined,
bccAddresses: bccAddresses.length > 0 ? bccAddresses : undefined,
template: data.template?.name,
},
});
showMessage({
message: msg(
str`Invitation emails queued for sending to ${addresses.length} recipient(s). Check the System Tasks for more information.`,
),
level: MessageLevel.success,
});
} catch (error) {
showMessage({
message: msg(str`Failed to queue invitation emails: ${error}`),
level: MessageLevel.error,
});
}
}
protected override renderForm(): TemplateResult {
const expiresDisplay = this.invitation?.expires
? this.invitation.expires.toLocaleString()
: msg("Never");
const invitationInfo: DescriptionPair[] = [
[msg("Name"), this.invitation?.name ?? "-"],
[msg("Expires"), expiresDisplay],
[msg("Flow"), this.invitation?.flowObj?.slug ?? msg("No flow set")],
[msg("Single use"), this.invitation?.singleUse ? msg("Yes") : msg("No")],
];
return html`${renderDescriptionList(invitationInfo, { horizontal: true, twocolumn: true })}
<ak-textarea-input
label=${msg("To")}
name="emailAddresses"
required
help=${msg(
"One email address per line, or comma/semicolon separated. Each recipient will receive a separate email with an invitation link.",
)}
>
</ak-textarea-input>
<ak-textarea-input
label=${msg("CC")}
name="ccAddresses"
help=${msg(
"A comma-separated list of addresses to receive copies of the invitation. Recipients will receive the full list of other addresses in this list.",
)}
>
</ak-textarea-input>
<ak-textarea-input
label=${msg("BCC")}
name="bccAddresses"
help=${msg(
"A comma-separated list of addresses to receive copies of the invitation. Recipients will not receive the addresses of other recipients.",
)}
>
</ak-textarea-input>
<ak-form-element-horizontal label=${msg("Template")} required name="template">
<select class="pf-c-form-control">
${this.availableTemplates?.map((template) => {
return html`<option
value=${template.name}
?selected=${template.name === this.selectedTemplate}
>
${template.description}
</option>`;
})}
</select>
<p class="pf-c-form__helper-text">
${msg("Select the email template to use for sending invitations.")}
</p>
</ak-form-element-horizontal>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-send-email-form": InvitationSendEmailForm;
}
}

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

@@ -421,7 +421,7 @@ export class IdentificationStage extends BaseStage<
? html`
<p>
${msg(
"Enter the email address or username associated with your account.",
"Enter the email associated with your account, and we'll send you a link to reset your password.",
)}
</p>
`

View File

@@ -10189,9 +10189,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10321,6 +10318,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10229,9 +10229,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10361,6 +10358,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -8070,9 +8070,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -8202,6 +8199,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10149,9 +10149,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10281,6 +10278,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10405,9 +10405,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10537,6 +10534,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10394,9 +10394,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10526,6 +10523,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10097,9 +10097,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10229,6 +10226,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10385,9 +10385,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10517,6 +10514,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9729,9 +9729,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -9861,6 +9858,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9387,9 +9387,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -9519,6 +9516,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9757,9 +9757,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -9889,6 +9886,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10378,9 +10378,6 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10510,6 +10507,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9845,9 +9845,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -9977,6 +9974,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9824,9 +9824,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -9956,6 +9953,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10685,9 +10685,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -10817,6 +10814,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -9443,9 +9443,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s20c950d20c8aec84">
<source>Min reviewers is per-group</source>
</trans-unit>
<trans-unit id="se4f63fe867f1714a">
<source>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.</source>
</trans-unit>
<trans-unit id="s74906f78877111cb">
<source>Reviewers</source>
</trans-unit>
@@ -9575,6 +9572,32 @@ 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>
<trans-unit id="h903c2b1c21bb9fc7">
<source>If checked, approving a review will require at least that many users from
<x id="0" equiv-text="&lt;em&gt;"/>each<x id="1" equiv-text="&lt;/em&gt;"/> of the selected groups. When disabled, the value is a total
across all groups.</source>
</trans-unit>
<trans-unit id="s776d93092ad9ce90">
<source>Review initiated</source>
</trans-unit>
<trans-unit id="sad46bcad1a343b51">
<source>Review overdue</source>
</trans-unit>
<trans-unit id="s59d168d966cecbaf">
<source>Review attested</source>
</trans-unit>
<trans-unit id="s5158b7f014cecefc">
<source>Review completed</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

@@ -31,7 +31,7 @@ Keys prefixed with `goauthentik.io` are used internally by authentik and are sub
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../../stages/identification/index.mdx).
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx), and others will use this value if it is set, and fall back to the request's user when possible.
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx) and others will use this value if it is set, and fallback to the request's users when possible.
#### `prompt_data` (Dictionary)
@@ -55,6 +55,8 @@ Stores the final redirect URL that the user's browser will be sent to after the
If _Show matched user_ is disabled, this key will hold the user identifier entered by the user in the identification stage.
Stores the final redirect URL that the user's browser will be sent to after the flow is finished executing successfully. This is set when an un-authenticated user attempts to access a secured application, and when a user authenticates/enrolls with an external source.
#### `application` (Application object)
When an unauthenticated user attempts to access a secured resource, they are redirected to an authentication flow. The application they attempted to access will be stored in the key attached to this object. For example: `application.github`, with `application` being the key and `github` the value.
@@ -149,7 +151,7 @@ Type the `pending_user` will be created as. Must be one of `internal`, `external
##### `user_backend` (string)
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating in the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
##### `auth_method` (string)

View File

@@ -2,9 +2,9 @@
title: Default flows
---
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that overrides the defaults configured on the brand.
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that override the defaults configured on the brand.
If no default flow is selected when the provider is created, authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands/index.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug`, evaluate policies bound directly to the flows, and pick the first flow whose policies allow access.
If no default flow is selected when the provider is created, to determine which flow should be used authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug` and evaluate policies bound directly to the flows, and the first flow whose policies allow access will be picked.
import DefaultFlowList from "../../flow/flow_list/\_defaultflowlist.mdx";

View File

@@ -6,4 +6,4 @@ The user interface (/if/user/) uses a specialized flow executor to allow individ
Because the stages in a flow can change during its execution, be aware that configuring this executor to use any stage type other than Prompt or User Write will automatically trigger a redirect to the standard executor.
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands/index.md) on the same authentik instance.
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands.md) on the same authentik instance.

View File

@@ -4,7 +4,7 @@ title: Flows
Flows are a major component in authentik. In conjunction with stages and [policies](../../../customize/policies/index.md), flows are at the heart of our system of building blocks, used to define and execute the workflows of authentication, authorization, enrollment, and user settings.
There are over a dozen default, out-of-the-box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
There are over a dozen default, out-of-the box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
A flow is a method of describing a sequence of stages. A stage represents a single verification or logic step. By connecting a series of stages within a flow (and optionally attaching policies as needed) you can build a highly flexible process for authenticating users, enrolling them, and more.
@@ -54,7 +54,7 @@ To create a flow, follow these steps:
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands/index.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
## Flow configuration options
@@ -78,9 +78,9 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
**Behavior settings**:
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled when accessing the flow from a desktop browser. However, accessing the flow from a mobile device might necessitate this setting to be enabled.
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled, when accessing the flow from a desktop browser. However accessing the flow from a mobile device might necessitate this setting to be enabled.
The technical reason for this setting's existence is the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
The technical reasons for this settings' existence is due to the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
When the compatibility mode is enabled, authentik uses a polyfill which emulates the Shadow DOM APIs without actually using the feature, and instead a traditional DOM is rendered. This increases support for password managers, especially on mobile devices.
@@ -95,7 +95,7 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
- **Layout**: select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands/index.md#branding-settings).
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands.md#branding-settings).
## Edit or delete a flow

View File

@@ -80,4 +80,4 @@ For detailed instructions, refer to Google documentation.
4. Click **Finish**.
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the user's browser.
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the users' browser.

View File

@@ -66,7 +66,7 @@ return {
## Verify only
To only verify the validity of a user's phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
To only verify the validity of a users' phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
## Limiting phone numbers

View File

@@ -26,7 +26,7 @@ Keep in mind that when using Code-based devices (TOTP, Static and SMS), values l
#### Less-frequent validation
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the user's devices within the selected classes are checked.
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the users devices within the selected classes are checked.
#### Passwordless authentication

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
@@ -97,7 +97,7 @@ See the [Envoy mTLS documentation](https://www.envoyproxy.io/docs/envoy/latest/s
#### No reverse proxy
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands/index.md#client-certificates) for the domain, under **Other global settings**.
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands.md#client-certificates) for the domain, under **Other global settings**.
## Stage configuration

View File

@@ -6,7 +6,7 @@ This is a generic password prompt which authenticates the current `pending_user`
## Passwordless login
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the user's device, which is documented here.
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the users device, which is documented here.
If you want users to be able to pick a passkey from the browser's passkey/autofill UI without entering a username first, configure **Passkey autofill (WebAuthn conditional UI)** in the [Identification stage](../identification/index.mdx#passkey-autofill-webauthn-conditional-ui). This is separate from configuring a dedicated passwordless flow, and can be used alongside normal identification flows.

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

@@ -8,7 +8,7 @@ The device code flow is also known as _device flow_ or _device authorization gra
### Requirements
This device flow is only possible if the active [brand](../../../sys-mgmt/brands/index.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
This device flow is only possible if the active [brand](../../../sys-mgmt/brands.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
authentik does not include a default flow for this use case, so it is necessary to create a new one with a **Designation** of `Stage Configuration`.

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 user'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.
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.

View File

@@ -8,6 +8,6 @@ The [WebFinger protocol](https://webfinger.net/) allows for the discovery of inf
## authentik WebFinger support
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands/index.md#external-user-settings).
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands.md#external-user-settings).
The WebFinger endpoint is available at: `https://authentik.company/.well-known/webfinger` (where authentik.company is the FQDN of your authentik instance)

View File

@@ -1,87 +0,0 @@
---
title: Create a Remote Access Control (RAC) provider
---
For an overview of Remote Access Control (RAC), see the [RAC provider](./index.md) documentation.
You can also watch our video on YouTube for setting up RAC:
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
## Workflow to create an RAC provider
Follow this workflow to create and configure an RAC provider:
1. Create a RAC provider and application pair.
2. Create RAC property mappings (that define the access credentials to each remote machine).
3. Create endpoints for each remote machine you want to connect to.
4. Create an RAC outpost to service the provider.
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
### Create a RAC provider and application pair
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 create only 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.
3. On the **New application** page, define the application details, and then click **Next**.
4. Select the **RAC** provider type, and then click **Next**.
5. On the **Configure Remote Access Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
### Create RAC property mappings
Next, you need to add property mappings for each remote machine you want to access. RAC property mappings can be used to pass the access credentials and connection settings of the remote machine.
Refer to the [RAC Credentials Prompt](./rac_credentials_prompt.md) and [RAC SSH Public Key Authentication](./rac-public-key.md) documentation for alternative methods of handling RAC authentication.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Property Mappings**, and click **Create**.
3. Select **RAC Provider Property Mapping** as the property mapping type, and then click **Next**.
4. On the **Create RAC Provider Property Mapping** page, provide the following configuration settings:
- **Name**: provide a name for the property mapping
- Under **General settings**:
- **Username**: the username for the remote machine
- **Password**: the password for the remote machine
- Under **Advanced settings**:
- **Expression _(optional)_**: define other connection settings to be used, such as an SSH key. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation.
5. Click **Finish**.
### Create endpoints for the provider
Then, you need to create an endpoint corresponding to each remote machine you want to connect to. Endpoints define the IP address, port, protocol, and other settings used for connecting to a remote machine.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers**.
3. Click the **Edit** button on the RAC provider that you previously created.
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
- **Provider Name** (endpoint name): define a name for the endpoint
- **Protocol**: select the appropriate protocol
- **Host**: enter the host name or IP address of the remote machine. Optionally include the port.
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation
- **Property mappings**: select either the property mapping that you previously created, or use one of the default RAC property mappings
- **Advanced settings _(optional)_**: define other connection settings to be used. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation
5. Click **Create**.
### Create an RAC outpost
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Outposts**.
3. Click **Create** and set the following values:
- **Name**: define a name for the outpost.
- **Type**: `RAC`
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
- **Applications**: select the RAC application that you previously created.
- **Advanced settings _(optional)_**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
4. Click **Create** to save your new outpost.
## Access the remote machine
To verify your configuration and access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page, click the **Remote Access** application to start a secure session on the remote machine in your web browser.
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.

View File

@@ -0,0 +1,88 @@
---
title: Create a Remote Access Control (RAC) provider
---
The Remote Access Control (RAC) provider is a highly flexible feature for accessing remote machines.
For overview information, see the [RAC provider](./index.md) documentation. You can also view our video on YouTube for setting up RAC.
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
## Overview workflow to create an RAC provider
The typical workflow to create and configure a RAC provider is:
1. Create an application and provider.
2. Create property mappings (that define the access credentials to each remote machine).
3. Create an endpoint for each remote machine you want to connect to.
4. Create an RAC outpost to service the provider.
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
### Create an application and RAC provider
The first step is to create the RAC application and provider 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**.
3. Follow these [instructions](../../applications/manage_apps.mdx#create-an-application-and-provider-pair) to create your RAC application and provider.
### Create RAC property mappings
Next, you need to add property mappings for each remote machine you want to access. Property mappings allow you to pass information to external applications, and with RAC they are used to pass the host name, IP address, and access credentials of the remote machine.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
- **Select Type**: `RAC Provider Property Mapping`
- **Create RAC Property Mapping**:
- **Name**s: define a name for the property mapping, perhaps include the type of connection (RDP, SSH, VNC)
- **General settings**:
- **Username**: the username for the remote machine
- **Password**: the password for the remote machine
- **RDP settings**:
- **Ignore server certificate**: select **Enabled** (Depending on the setup of your RDP Server, it might be required to enable this setting.)
- **Enable wallpaper**: optional
- **Enable font smoothing**: optional
- **Enable full window dragging**: optional
- Advanced settings:
- **Expressions**: optional, using Python you can define custom [expressions](../property-mappings/expression.mdx).
3. Click **Finish**.
### Create endpoints for the provider
Then, you need to create an endpoint for each remote machine. Endpoints are defined within providers; connections between the remote machine and authentik are enabled through communication between the provider's endpoint and the remote machine.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers**.
3. Click the **Edit** button on the RAC provider that you previously created.
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
- **Name**: define a name for the endpoint, perhaps include the type of connection (RDP, SSH, VNC).
- **Protocol**: select the appropriate protocol.
- **Host**: enter the host name or IP address of the remote machine.
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation.
- **Property mapping**: select either the property mapping that you previously created, or use one of the default settings.
- **Advance settings**: (_optional_)
5. Click **Create**.
### Create an RAC outpost
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Outposts**.
3. Click **Create** and set the following values:
- **Name**: define a name for the outpost.
- **Type**: `RAC`
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
- **Applications**: select the RAC application that you previously created.
- **Advanced settings (optional)**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
4. Click Create to save your new outpost.
## Access the remote machine
To verify your configuration and then access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page click the **Remote Access** application and authentik then connects you to a secure session on the remote machine, in your web browser.
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.

View File

@@ -2,85 +2,67 @@
title: Remote Access Control (RAC) Provider
---
:::info
This provider requires the deployment of the [RAC Outpost](../../outposts/index.mdx).
:::
## About the Remote Access Control (RAC) Provider
The RAC provider allows users to access remote Windows, macOS, and Linux machines via [RDP](https://en.wikipedia.org/wiki/Remote_Desktop_Protocol)/[SSH](https://en.wikipedia.org/wiki/Secure_Shell)/[VNC](https://en.wikipedia.org/wiki/Virtual_Network_Computing). Just like other providers in authentik, the RAC provider is associated with an application that appears on a user's **My applications** page.
For instructions on creating a RAC provider, refer to the [Create a Remote Access Control (RAC) provider](./create-rac-provider.md) documentation. Alternatively, watch our ["Remote Access Control (RAC) in authentik" video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts).
:::info
Note that with RAC, you create a single application and associated provider that serves to connect with _all remote machines_ that you want to configure for access via RAC.
:::
## RAC components
For instructions on creating a RAC provider, refer to the [Managing RAC providers](./how-to-rac.md) documentation. You can also view our [video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts) for setting up a RAC.
A RAC provider uses several components:
For an example of how to configure RAC connections settings, refer to the [RAC SSH Public Key Authentication](./rac-public-key.md) documentation.
```mermaid
architecture-beta
service application(mdi:application-outline)[Application]
service provider(mdi:application-cog-outline)[Provider]
service endpoint(mdi:network-pos)[Endpoint Settings]
service server(mdi:server)[authentik Server]
service outpost(mdi:server-plus)[RAC Outpost]
There are several components used with a RAC provider; let's take a closer look at the high-level configuration layout of these components and how they are managed using endpoints and connections.
service machine(mdi:desktop-classic)[Remote Machine]
![](./rac-v3.png)
application:R --> L:provider
provider:B -- T:endpoint
provider:R --> L:server
server:R <--> L:outpost
outpost:B <--> T:machine
```
The provider-application pair, the authentik server, and the authentik API are typical to all configurations. With RAC, there are some new components, namely the endpoints, the outpost, and of course the target remote machines.
When a user starts the RAC application, it communicates with the authentik server, which then connects to the RAC outpost and sends instructions (based on the endpoint data you defined) on how to connect to the remote machine.
When a user starts the RAC application, the app communicates with the authentik server, which then connects to an instance of the outpost (the exact instance is selected dynamically based on connection load). After the outpost is selected, then the authentik server sends the outpost the instructions (based on the data you defined in the endpoint) required to connect to the remote machine.
After connecting to the remote machine, the outpost sends a message back to the authentik server (via WebSockets), and the web browser opens the WebSocket connection to the remote machine.
After the connection to the remote machine is made, the outpost sends a message back to the authentik server (via websockets), and the web browser opens the websocket connection to the remote machine.
## Endpoints
### Endpoints
Unlike other providers, where an application-provider pair is created for each resource you wish to access, RAC works differently. RAC uses a single application connected to one RAC provider. The RAC provider then has an _Endpoint_ object for each remote machine (computer/server) you want to connect to.
Unlike other providers, where one provider-application pair must be created for each resource you wish to access, the RAC provider handles this slightly differently. For each remote machine (computer/server) that should be accessible, you create an _Endpoint_ object within a single RAC provider. (And as mentioned above, a single provider-application pair is used for all remote connections.)
The _Endpoint_ object specifies:
The _Endpoint_ object specifies the hostname/IP of the machine to connect to, as well as the protocol to use. Additionally it is possible to bind policies to _endpoint_ objects to restrict access. Users must have access to both the application that the RAC Provider is using as well as the individual endpoint.
- Hostname, IP address, and port of the remote machine
- Protocol to use: SSH, RDP, or VNC
- RDP connection settings
- [RAC Property mappings](#rac-property-mappings) to apply
- [Connection settings](#connection-settings) to apply
Additionally, it is possible to bind policies to _Endpoint_ objects to restrict user access. To connect to a remote machine, users must have access to both the application that the RAC provider is using and the corresponding endpoint.
## Connection management
A new connection is created every time an RAC application/endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, you can configure connection expiry in the RAC provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
## RAC Property Mappings
You can create RAC property mappings via **Customization** > **Property Mappings**.
RAC property mappings allow you to configure the following settings:
- **Username**: the username for the remote machine
- **Password**: the password for the remote machine
- **Ignore Server certificate**: set whether the validity of the returned RDP server certificate will be ignored
- **Enable wallpaper**: enable/disable the desktop wallpaper of the RDP server
- **Enable font-smoothing**: enable/disable font-smoothing (anti-aliasing) on the RDP server
- **Enable full window dragging**: enable/disable whether the full content of a window is visible while moving it on the RDP server
- **Advanced settings**: set [connection settings](#connection-settings) via a Python expression
## Connection settings
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.
Connection settings can include `username`, `password`, `domain`, `private-key`, `security`, `enable-audio`, and more.
For a full list of possible connection settings, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
RAC connection settings can be set via several methods and are all merged together when connecting:
Configuration details such as credentials can be specified through _settings_, which can be specified on different levels and are all merged together when connecting:
1. Default settings
2. RAC Provider settings
3. RAC Endpoint settings
4. RAC Provider property mapping settings
5. RAC Endpoint property mapping settings
6. The `connection_settings` object in the flow plan
2. Provider settings
3. Endpoint settings
4. Provider property mapping settings
5. Endpoint property mapping settings
6. Connection settings
For examples of how to configure connection settings, see the [RAC SSH public key authentication](./rac-public-key.md) and [RAC Credentials Prompt](./rac_credentials_prompt.md) documentation.
### Connection settings
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
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 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).
RAC connection settings can be set via several methods:
1. The settings of the RAC provider
2. RAC endpoint settings
3. RAC property mappings
4. Retrieved from user or group attributes via RAC property mappings
For an example of how to set a connection setting see the [RAC SSH public key authentication](./rac-public-key.md) page.
## Capabilities

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

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