mirror of
https://github.com/goauthentik/authentik
synced 2026-05-15 11:26:31 +02:00
Compare commits
36 Commits
version/20
...
website/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c6d21c01 | ||
|
|
3348ab34c3 | ||
|
|
81b4256e3c | ||
|
|
cc798f4425 | ||
|
|
9903fd4d95 | ||
|
|
20c2a33155 | ||
|
|
f1ee092930 | ||
|
|
611ddc2904 | ||
|
|
aeb2457767 | ||
|
|
97b6c9533f | ||
|
|
c880c9f4ab | ||
|
|
af36cdc597 | ||
|
|
4bd9b08cfb | ||
|
|
976df9e7da | ||
|
|
be64ed4281 | ||
|
|
5790316616 | ||
|
|
53c376e4e9 | ||
|
|
38bde992b7 | ||
|
|
c3353c1bf7 | ||
|
|
56f0df9d89 | ||
|
|
8aeedb6380 | ||
|
|
858a040dfb | ||
|
|
0329b6e1ab | ||
|
|
26eb34e17e | ||
|
|
9d41d41b4f | ||
|
|
37432f43ba | ||
|
|
655e25e0d5 | ||
|
|
0356a30d65 | ||
|
|
15db2713ab | ||
|
|
d9efce1002 | ||
|
|
a1a22978b3 | ||
|
|
95c9e5476e | ||
|
|
93cf6e2cb1 | ||
|
|
0dd8ee073a | ||
|
|
7cb789e777 | ||
|
|
09d0803d14 |
@@ -80,7 +80,7 @@ jobs:
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
|
||||
2
.github/workflows/ci-docs.yml
vendored
2
.github/workflows/ci-docs.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
|
||||
11
.github/workflows/ci-main.yml
vendored
11
.github/workflows/ci-main.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
run: make gen-client-go
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
|
||||
25
.github/workflows/release-publish.yml
vendored
25
.github/workflows/release-publish.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6
|
||||
id: push
|
||||
with:
|
||||
push: true
|
||||
@@ -160,10 +160,17 @@ jobs:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build web
|
||||
- name: Install web dependencies
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
@@ -210,12 +217,12 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
docker compose pull -q
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql
|
||||
docker compose run -u root server test-all
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
|
||||
docker compose -f lifecycle/container/compose.yml pull -q
|
||||
docker compose -f lifecycle/container/compose.yml up --no-start
|
||||
docker compose -f lifecycle/container/compose.yml start postgresql
|
||||
docker compose -f lifecycle/container/compose.yml run -u root server test-all
|
||||
sentry-release:
|
||||
needs:
|
||||
- build-server
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.2.0-rc1"
|
||||
VERSION = "2026.5.0-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Schema generation tests"""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
@@ -29,15 +31,14 @@ class TestSchemaGeneration(APITestCase):
|
||||
|
||||
def test_build_schema(self):
|
||||
"""Test schema build command"""
|
||||
blueprint_file = Path("blueprints/schema.json")
|
||||
api_file = Path("schema.yml")
|
||||
blueprint_file.unlink()
|
||||
api_file.unlink()
|
||||
tmp = Path(gettempdir())
|
||||
blueprint_file = tmp / f"{str(uuid4())}.json"
|
||||
api_file = tmp / f"{str(uuid4())}.yml"
|
||||
with (
|
||||
CONFIG.patch("debug", True),
|
||||
CONFIG.patch("tenants.enabled", True),
|
||||
CONFIG.patch("outposts.disable_embedded_outpost", True),
|
||||
):
|
||||
call_command("build_schema")
|
||||
call_command("build_schema", blueprint_file=blueprint_file, api_file=api_file)
|
||||
self.assertTrue(blueprint_file.exists())
|
||||
self.assertTrue(api_file.exists())
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet
|
||||
default=Value(False),
|
||||
output_field=ModelBooleanField(),
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
|
||||
@@ -42,7 +42,7 @@ ARG_SANITIZE = re.compile(r"[:.-]")
|
||||
|
||||
|
||||
def sanitize_arg(arg_name: str) -> str:
|
||||
return re.sub(ARG_SANITIZE, "_", arg_name)
|
||||
return re.sub(ARG_SANITIZE, "_", slugify(arg_name))
|
||||
|
||||
|
||||
class BaseEvaluator:
|
||||
@@ -311,7 +311,9 @@ class BaseEvaluator:
|
||||
|
||||
def wrap_expression(self, expression: str) -> str:
|
||||
"""Wrap expression in a function, call it, and save the result as `result`"""
|
||||
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
|
||||
handler_signature = ",".join(
|
||||
[x for x in [sanitize_arg(x) for x in self._context.keys()] if x]
|
||||
)
|
||||
full_expression = ""
|
||||
full_expression += f"def handler({handler_signature}):\n"
|
||||
full_expression += indent(expression, " ")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test Evaluator base functions"""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
@@ -353,3 +354,18 @@ class TestEvaluator(TestCase):
|
||||
self.assertEqual(message.to, ["to@example.com"])
|
||||
self.assertEqual(message.cc, ["cc1@example.com", "cc2@example.com"])
|
||||
self.assertEqual(message.bcc, ["bcc1@example.com", "bcc2@example.com"])
|
||||
|
||||
def test_expr_arg_escape(self):
|
||||
"""Test escaping of arguments"""
|
||||
eval = BaseEvaluator()
|
||||
eval._context = {
|
||||
'z=getattr(getattr(__import__("os"), "popen")("id > /tmp/test"), "read")()': "bar",
|
||||
"@@": "baz",
|
||||
"{{": "baz",
|
||||
"aa@@": "baz",
|
||||
}
|
||||
res = eval.evaluate("return locals()")
|
||||
self.assertEqual(
|
||||
res, {"zgetattrgetattr__import__os_popenid_tmptest_read": "bar", "aa": "baz"}
|
||||
)
|
||||
self.assertFalse(Path("/tmp/test").exists())
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -213,6 +213,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"sign_assertion",
|
||||
"sign_response",
|
||||
"sign_logout_request",
|
||||
"sign_logout_response",
|
||||
"sp_binding",
|
||||
"sls_binding",
|
||||
"logout_method",
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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"""
|
||||
|
||||
196
authentik/providers/saml/processors/logout_response_processor.py
Normal file
196
authentik/providers/saml/processors/logout_response_processor.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
139
authentik/providers/saml/tests/test_logout_response_processor.py
Normal file
139
authentik/providers/saml/tests/test_logout_response_processor.py
Normal 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")
|
||||
291
authentik/providers/saml/tests/test_tasks.py
Normal file
291
authentik/providers/saml/tests/test_tasks.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from lxml.etree import _Element # nosec
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.common.saml.constants import (
|
||||
@@ -217,9 +218,8 @@ class SAMLSource(Source):
|
||||
def property_mapping_type(self) -> type[PropertyMapping]:
|
||||
return SAMLSourcePropertyMapping
|
||||
|
||||
def get_base_user_properties(self, root: Any, name_id: Any, **kwargs):
|
||||
def get_base_user_properties(self, root: _Element, assertion: _Element, name_id: Any, **kwargs):
|
||||
attributes = {}
|
||||
assertion = root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
if assertion is None:
|
||||
raise ValueError("Assertion element not found")
|
||||
attribute_statement = assertion.find(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
|
||||
|
||||
@@ -66,6 +66,8 @@ class ResponseProcessor:
|
||||
|
||||
_http_request: HttpRequest
|
||||
|
||||
_assertion: _Element | None = None
|
||||
|
||||
def __init__(self, source: SAMLSource, request: HttpRequest):
|
||||
self._source = source
|
||||
self._http_request = request
|
||||
@@ -122,6 +124,7 @@ class ResponseProcessor:
|
||||
index_of,
|
||||
decrypted_assertion,
|
||||
)
|
||||
self._assertion = decrypted_assertion
|
||||
|
||||
def _verify_signature(self, signature_node: _Element):
|
||||
"""Verify a single signature node"""
|
||||
@@ -162,6 +165,10 @@ class ResponseProcessor:
|
||||
raise InvalidSignature("No Signature exists in the Assertion element.")
|
||||
|
||||
self._verify_signature(signature_nodes[0])
|
||||
parent = signature_nodes[0].getparent()
|
||||
if parent is None or parent.tag != f"{{{NS_SAML_ASSERTION}}}Assertion":
|
||||
raise InvalidSignature("No Signature exists in the Assertion element.")
|
||||
self._assertion = parent
|
||||
|
||||
def _verify_request_id(self):
|
||||
if self._source.allow_idp_initiated:
|
||||
@@ -239,14 +246,21 @@ class ResponseProcessor:
|
||||
identifier=str(name_id.text),
|
||||
user_info={
|
||||
"root": self._root,
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
},
|
||||
policy_context={},
|
||||
)
|
||||
|
||||
def get_assertion(self) -> Element | None:
|
||||
"""Get assertion element, if we have a signed assertion"""
|
||||
if self._assertion is not None:
|
||||
return self._assertion
|
||||
return self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
|
||||
def _get_name_id(self) -> Element:
|
||||
"""Get NameID Element"""
|
||||
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
assertion = self.get_assertion()
|
||||
if assertion is None:
|
||||
raise ValueError("Assertion element not found")
|
||||
subject = assertion.find(f"{{{NS_SAML_ASSERTION}}}Subject")
|
||||
@@ -299,6 +313,7 @@ class ResponseProcessor:
|
||||
identifier=str(name_id.text),
|
||||
user_info={
|
||||
"root": self._root,
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
},
|
||||
policy_context={
|
||||
|
||||
68
authentik/sources/saml/tests/fixtures/response_signed_assertion_dup.xml
vendored
Normal file
68
authentik/sources/saml/tests/fixtures/response_signed_assertion_dup.xml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
|
||||
<samlp:Status>
|
||||
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||
</samlp:Status>
|
||||
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_other_id_pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
|
||||
<saml:Subject>
|
||||
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">bad</saml:NameID>
|
||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
|
||||
<ds:Reference URI="#pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>zNDuGxwP4gVkv/Dzt7kiKo/4gzk=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>GLP/vE8uxerB0uDpPslUgLPBL6ePQB619MoQ0I2Y5lAtFE6CB1zh8BnzChRx/bFjNy4byfOe8mFfM0r7WUi1PJOFWyUPoatdLl7wHHBIRTnPpYmu3Tb2Gz0sOP0F8wW7JkBft5gJfVw49nk5si9/3Q3o52jnJZ7dPtqfIOh8uNeopikK0HLF6sU05qCCtjcXfniEnLQFNBFMo9uY5GQqmR5n3nqPz1wYyyfFOAbVmGgBIoO2PfGX2GVLQhltc9qf2JMhks4jgZsZ8iLUIiH1lcLGWZEEs94k8k0P6gSv1uZ7Vbhksd/N9Jq9pCVuEJ/jRPcAdVjzbxqKQAj6ELwr8O6fepTzA+CAdwEolBnx/C6TmSbVZ+IWk6QUGe4x4+IAukC+0hkKENlO0ELOScksvyhpgHbxNA4rp+DhGupCaO/I2RrsQkmvavbqm+wSEspK7scK112SDunjDvqPHsPYgukD33T/97PxTLorg2kKP9HHJwPJKoXXeyOGcA6vwK+RqrAlZ2dLGAgcXo+sJcdCLuvxDNz9VXofBjBZIKVKdmYhm0QJaPYHtuQsAyFavQhdOBOmGHb7QX3YE3Xy4dX4LymtT+Jlb1I4FJSht/9HUIHW1FdhfDak4f7gUgjuMamMddLD0jVgeESupSREzFv/gj2IrctkbgjAO0iuuiBgKMg=</ds:SignatureValue>
|
||||
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
|
||||
<saml:Subject>
|
||||
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
|
||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
|
||||
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
</samlp:Response>
|
||||
@@ -36,7 +36,9 @@ class TestPropertyMappings(TestCase):
|
||||
|
||||
def test_user_base_properties(self):
|
||||
"""Test user base properties"""
|
||||
properties = self.source.get_base_user_properties(root=ROOT, name_id=NAME_ID)
|
||||
properties = self.source.get_base_user_properties(
|
||||
root=ROOT, assertion=ROOT.find(f"{{{NS_SAML_ASSERTION}}}Assertion"), name_id=NAME_ID
|
||||
)
|
||||
self.assertEqual(
|
||||
properties,
|
||||
{
|
||||
@@ -49,7 +51,11 @@ class TestPropertyMappings(TestCase):
|
||||
|
||||
def test_group_base_properties(self):
|
||||
"""Test group base properties"""
|
||||
properties = self.source.get_base_user_properties(root=ROOT_GROUPS, name_id=NAME_ID)
|
||||
properties = self.source.get_base_user_properties(
|
||||
root=ROOT_GROUPS,
|
||||
assertion=ROOT_GROUPS.find(f"{{{NS_SAML_ASSERTION}}}Assertion"),
|
||||
name_id=NAME_ID,
|
||||
)
|
||||
self.assertEqual(properties["groups"], ["group 1", "group 2"])
|
||||
for group_id in ["group 1", "group 2"]:
|
||||
properties = self.source.get_base_group_properties(root=ROOT, group_id=group_id)
|
||||
|
||||
@@ -164,6 +164,31 @@ class TestResponseProcessor(TestCase):
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
|
||||
def test_verification_assertion_duplicate(self):
|
||||
"""Test verifying signature inside assertion, where the response has another assertion
|
||||
before our signed assertion"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
kp = CertificateKeyPair.objects.create(
|
||||
name=generate_id(),
|
||||
certificate_data=key,
|
||||
)
|
||||
self.source.verification_kp = kp
|
||||
self.source.signed_assertion = True
|
||||
self.source.signed_response = False
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
data={
|
||||
"SAMLResponse": b64encode(
|
||||
load_fixture("fixtures/response_signed_assertion_dup.xml").encode()
|
||||
).decode()
|
||||
},
|
||||
)
|
||||
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
self.assertNotEqual(parser._get_name_id().text, "bad")
|
||||
self.assertEqual(parser._get_name_id().text, "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
|
||||
|
||||
def test_verification_response(self):
|
||||
"""Test verifying signature inside response"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2026.2.0-rc1 Blueprint schema",
|
||||
"title": "authentik 2026.5.0-rc1 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@@ -10759,6 +10759,10 @@
|
||||
"type": "boolean",
|
||||
"title": "Sign logout request"
|
||||
},
|
||||
"sign_logout_response": {
|
||||
"type": "boolean",
|
||||
"title": "Sign logout response"
|
||||
},
|
||||
"sp_binding": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
2
go.mod
2
go.mod
@@ -30,7 +30,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/sync v0.19.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -218,6 +218,10 @@ goauthentik.io/api/v3 v3.2026020.17-0.20260205232234-280022b0a8de h1:X1ELA34R1N+
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260205232234-280022b0a8de/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535 h1:DPk8z6SGesp0gbmaD2zTAKVSd/NQ++Nu+lu3UrCkNvE=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211005401-cdd71ec2f62f h1:KK5lBHSvZSlMbUViB7KStlkP9kC1t9JeiMawa7wyI6Q=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211005401-cdd71ec2f62f/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393 h1:eLRd2GC+pxvwd3m2msJRNB9upH7pcIZH5V4L9/WhRcw=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026.2.0-rc1
|
||||
2026.5.0-rc1
|
||||
@@ -18,7 +18,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2026.2.0-rc1
|
||||
Default: 2026.5.0-rc1
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.0-rc1}
|
||||
ports:
|
||||
- ${COMPOSE_PORT_HTTP:-9000}:9000
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.0-rc1}
|
||||
restart: unless-stopped
|
||||
shm_size: 512mb
|
||||
user: root
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.5.0-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.5.0-rc1",
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@goauthentik/eslint-config": "./packages/eslint-config",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.5.0-rc1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2026.2.0-rc1"
|
||||
version = "2026.5.0-rc1"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.14.*"
|
||||
dependencies = [
|
||||
"ak-guardian==3.2.0",
|
||||
"argon2-cffi==25.1.0",
|
||||
"cachetools==7.0.0",
|
||||
"cachetools==7.0.1",
|
||||
"channels==4.3.2",
|
||||
"cryptography==46.0.4",
|
||||
"cryptography==46.0.5",
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
@@ -37,7 +37,7 @@ dependencies = [
|
||||
"fido2==2.1.1",
|
||||
"geoip2==5.2.0",
|
||||
"geopy==2.4.1",
|
||||
"google-api-python-client==2.189.0",
|
||||
"google-api-python-client==2.190.0",
|
||||
"gssapi==1.11.1",
|
||||
"gunicorn==25.0.3",
|
||||
"jsonpatch==1.33",
|
||||
@@ -69,7 +69,7 @@ dependencies = [
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.40.0",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.7.0",
|
||||
"webauthn==2.7.1",
|
||||
"wsproto==1.3.2",
|
||||
"xmlsec==1.3.17",
|
||||
"zxcvbn==4.5.0",
|
||||
|
||||
64
schema.yml
64
schema.yml
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2026.2.0-rc1
|
||||
version: 2026.5.0-rc1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@@ -18677,6 +18677,10 @@ paths:
|
||||
name: sign_logout_request
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_logout_response
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_response
|
||||
schema:
|
||||
@@ -19863,6 +19867,10 @@ paths:
|
||||
name: sign_logout_request
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_logout_response
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_response
|
||||
schema:
|
||||
@@ -40696,8 +40704,7 @@ components:
|
||||
logout_urls:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
$ref: '#/components/schemas/LogoutURL'
|
||||
IframeLogoutChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response for iframe logout
|
||||
@@ -42393,6 +42400,29 @@ components:
|
||||
required:
|
||||
- challenge
|
||||
- name
|
||||
LogoutURL:
|
||||
type: object
|
||||
description: Data for a single logout URL
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
nullable: true
|
||||
binding:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_request:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_response:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_relay_state:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- url
|
||||
MDMConfigRequest:
|
||||
type: object
|
||||
description: Base serializer class which doesn't implement create/update methods
|
||||
@@ -42956,21 +42986,23 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
post_url:
|
||||
type: string
|
||||
saml_request:
|
||||
type: string
|
||||
relay_state:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
binding:
|
||||
type: string
|
||||
redirect_url:
|
||||
type: string
|
||||
is_complete:
|
||||
type: boolean
|
||||
default: false
|
||||
post_url:
|
||||
type: string
|
||||
redirect_url:
|
||||
type: string
|
||||
saml_binding:
|
||||
$ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
saml_request:
|
||||
type: string
|
||||
saml_response:
|
||||
type: string
|
||||
saml_relay_state:
|
||||
type: string
|
||||
NativeLogoutChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response for native browser logout
|
||||
@@ -49905,6 +49937,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
@@ -53397,6 +53431,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
@@ -53591,6 +53627,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e -x -o pipefail
|
||||
hash="$(git rev-parse HEAD || openssl rand -base64 36 | sha256sum)"
|
||||
|
||||
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
|
||||
AUTHENTIK_IMAGE="authentik.invalid/goauthentik/server"
|
||||
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
|
||||
|
||||
if [ -f lifecycle/container/.env ]; then
|
||||
@@ -24,7 +24,7 @@ if [[ -v BUILD ]]; then
|
||||
make gen-client-go
|
||||
touch lifecycle/container/.env
|
||||
|
||||
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
|
||||
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" -f lifecycle/container/Dockerfile .
|
||||
fi
|
||||
|
||||
docker compose -f lifecycle/container/compose.yml up --no-start
|
||||
|
||||
127
uv.lock
generated
127
uv.lock
generated
@@ -192,15 +192,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1crypto"
|
||||
version = "1.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-generator"
|
||||
version = "1.10"
|
||||
@@ -221,7 +212,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authentik"
|
||||
version = "2026.2.0rc1"
|
||||
version = "2026.5.0rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "ak-guardian" },
|
||||
@@ -336,9 +327,9 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "ak-guardian", editable = "packages/ak-guardian" },
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.1" },
|
||||
{ name = "channels", specifier = "==4.3.2" },
|
||||
{ name = "cryptography", specifier = "==46.0.4" },
|
||||
{ name = "cryptography", specifier = "==46.0.5" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
{ name = "deepmerge", specifier = "==2.0" },
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
@@ -366,7 +357,7 @@ requires-dist = [
|
||||
{ name = "fido2", specifier = "==2.1.1" },
|
||||
{ name = "geoip2", specifier = "==5.2.0" },
|
||||
{ name = "geopy", specifier = "==2.4.1" },
|
||||
{ name = "google-api-python-client", specifier = "==2.189.0" },
|
||||
{ name = "google-api-python-client", specifier = "==2.190.0" },
|
||||
{ name = "gssapi", specifier = "==1.11.1" },
|
||||
{ name = "gunicorn", specifier = "==25.0.3" },
|
||||
{ name = "jsonpatch", specifier = "==1.33" },
|
||||
@@ -398,7 +389,7 @@ requires-dist = [
|
||||
{ name = "urllib3", specifier = "<3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.40.0" },
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
{ name = "webauthn", specifier = "==2.7.0" },
|
||||
{ name = "webauthn", specifier = "==2.7.1" },
|
||||
{ name = "wsproto", specifier = "==1.3.2" },
|
||||
{ name = "xmlsec", specifier = "==1.3.17" },
|
||||
{ name = "zxcvbn", specifier = "==4.5.0" },
|
||||
@@ -710,11 +701,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "7.0.0"
|
||||
version = "7.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -932,55 +923,55 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.4"
|
||||
version = "46.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1655,7 +1646,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.189.0"
|
||||
version = "2.190.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
@@ -1664,9 +1655,9 @@ dependencies = [
|
||||
{ name = "httplib2" },
|
||||
{ name = "uritemplate" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/f8/0783aeca3410ee053d4dd1fccafd85197847b8f84dd038e036634605d083/google_api_python_client-2.189.0.tar.gz", hash = "sha256:45f2d8559b5c895dde6ad3fb33de025f5cb2c197fa5862f18df7f5295a172741", size = 13979470, upload-time = "2026-02-03T19:24:55.432Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/4ab3e3516b93bb50ed7814738ea61d49cba3f72f4e331dc9518ae2731e92/google_api_python_client-2.190.0.tar.gz", hash = "sha256:5357f34552e3724d80d2604c8fa146766e0a9d6bb0afada886fafed9feafeef6", size = 14111143, upload-time = "2026-02-12T00:38:03.37Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl", hash = "sha256:a258c09660a49c6159173f8bbece171278e917e104a11f0640b34751b79c8a1a", size = 14547633, upload-time = "2026-02-03T19:24:52.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ad/223d5f4b0b987669ffeb3eadd7e9f85ece633aa7fd3246f1e2f6238e1e05/google_api_python_client-2.190.0-py3-none-any.whl", hash = "sha256:d9b5266758f96c39b8c21d9bbfeb4e58c14dbfba3c931f7c5a8d7fdcd292dd57", size = 14682070, upload-time = "2026-02-12T00:38:00.974Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4003,17 +3994,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "webauthn"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asn1crypto" },
|
||||
{ name = "cbor2" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "pyopenssl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/f0/e1036df8842782a2947e5f41e76a4accb92e3dba972dba882321ebe15af0/webauthn-2.7.0.tar.gz", hash = "sha256:3c45c25e75a7d7d419220ccd10b8b899984de8012732e10d898f0a8f8c480575", size = 123770, upload-time = "2025-09-04T23:19:21.602Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/f4/9529bcf85ef46c76842b84c66ffa8ec31f18e3aacd1330b62f440077b45b/webauthn-2.7.1.tar.gz", hash = "sha256:2a1ebbfffc4a83e31d3db5d69113944bc49d05fae77770c2d4e388386cb9656e", size = 124256, upload-time = "2026-02-11T23:36:02.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b1/3f380d02552f1d75d3db789f761a1ee0dafd6181ebc07dd4b9ded61225a4/webauthn-2.7.0-py3-none-any.whl", hash = "sha256:2ecfee7959b09ebeaaffee9f8982ecdbbdc369a11766d20d4bc0637b36e235b7", size = 71311, upload-time = "2025-09-04T23:19:20.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/5b/f73513367a9d34b199de916b44306acfa4027b57f7e22200421212b1f763/webauthn-2.7.1-py3-none-any.whl", hash = "sha256:d57e9613c65e0c6a4db7ee715fb49ebdf3c4a6eb3979729eeb497c99105e8181", size = 71684, upload-time = "2026-02-11T23:36:00.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.5.0-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.5.0-rc1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.5.0-rc1",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -243,8 +243,10 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
|
||||
name="minReviewersIsPerGroup"
|
||||
?checked=${this.instance?.minReviewersIsPerGroup ?? false}
|
||||
label=${msg("Min reviewers is per-group")}
|
||||
help=${msg(
|
||||
"If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.",
|
||||
.help=${msg(
|
||||
html`If checked, approving a review will require at least that many users from
|
||||
<em>each</em> of the selected groups. When disabled, the value is a total
|
||||
across all groups.`,
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
|
||||
[EventActions.EmailSent, msg("Email sent")],
|
||||
[EventActions.UpdateAvailable, msg("Update available")],
|
||||
[EventActions.ExportReady, msg("Data export ready")],
|
||||
[EventActions.ReviewInitiated, msg("Review initiated")],
|
||||
[EventActions.ReviewOverdue, msg("Review overdue")],
|
||||
[EventActions.ReviewAttested, msg("Review attested")],
|
||||
[EventActions.ReviewCompleted, msg("Review completed")],
|
||||
]);
|
||||
|
||||
export const actionToLabel = (action?: EventActions): string =>
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AkSwitchInput extends AKElement {
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
help: string | TemplateResult = "";
|
||||
|
||||
/**
|
||||
* For more complex help instructions, provide a template result.
|
||||
@@ -47,11 +47,13 @@ export class AkSwitchInput extends AKElement {
|
||||
#fieldID: string = IDGenerator.randomID();
|
||||
|
||||
protected renderHelp() {
|
||||
const helpText = this.help.trim();
|
||||
const helpContent = typeof this.help === "string" ? this.help.trim() : this.help;
|
||||
|
||||
return [
|
||||
helpText
|
||||
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">${helpText}</p>`
|
||||
helpContent
|
||||
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">
|
||||
${helpContent}
|
||||
</p>`
|
||||
: nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10321,6 +10321,15 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10361,6 +10361,15 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -8202,6 +8202,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10281,6 +10281,15 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10537,6 +10537,15 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10526,6 +10526,15 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10229,6 +10229,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10517,6 +10517,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9861,6 +9861,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9519,6 +9519,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9889,6 +9889,15 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10510,6 +10510,15 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9977,6 +9977,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9956,6 +9956,15 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10817,6 +10817,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9575,6 +9575,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
2
website/api/types/api-plugin.d.ts
vendored
2
website/api/types/api-plugin.d.ts
vendored
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: OAuth2/OpenID Connect front-channel and back-channel logout
|
||||
title: Front-channel and back-channel logout
|
||||
description: Configure front-channel and back-channel logout for OAuth2/OpenID Connect providers
|
||||
authentik_version: "2025.8.0"
|
||||
authentik_preview: true
|
||||
|
||||
@@ -23,7 +23,7 @@ OAuth 2.0 is an authorization protocol that allows an application (the RP) to de
|
||||
1. An authorization request is prepared by the RP and contains parameters for its implementation of OAuth and which data it requires, and then the User's browser is redirected to that URL.
|
||||
2. The RP sends a request to authentik in the background to exchange the access code for an access token (and optionally a refresh token).
|
||||
|
||||
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorised (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
|
||||
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorized (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
|
||||
|
||||
The image below shows a typical authorization code flow.
|
||||
|
||||
@@ -102,7 +102,7 @@ The flows and grant types used in this case are those used for a typical authori
|
||||
|
||||
The authorization code is for environments with both a Client and a application server, where the back and forth happens between the client and an app server (the logic lives on app server). The RP needs to authorise itself to the OP. Client ID (public, identifies which app is talking to it) and client secret (the password) that the RP uses to authenticate.
|
||||
|
||||
If you configure authentik to use "Offline access" then during the initial auth the OP sends two tokens, an access token (short-lived, hours, can be customised) and a refresh token (typically longer validity, days or infinite). The RP (the app) saves both tokens. When the access token is about to expire, the RP sends the saved refresh token back to the OP, and requests a new access token. When the refresh token itself is about to expire, the RP can also ask for a new refresh token. This can all happen without user interaction if you configured the offline access.
|
||||
If you configure authentik to use "Offline access" then during the initial auth the OP sends two tokens, an access token (short-lived, hours, can be customized) and a refresh token (typically longer validity, days or infinite). The RP (the app) saves both tokens. When the access token is about to expire, the RP sends the saved refresh token back to the OP, and requests a new access token. When the refresh token itself is about to expire, the RP can also ask for a new refresh token. This can all happen without user interaction if you configured the offline access.
|
||||
|
||||
:::info
|
||||
Starting with authentik 2024.2, applications only receive an access token. To receive a refresh token, both applications and authentik must be configured to request the `offline_access` scope. In authentik this can be done by selecting the `offline_access` Scope mapping in the provider settings.
|
||||
@@ -183,6 +183,28 @@ This does _not_ apply to special scopes, as those are not configurable in the pr
|
||||
- `user:email`: Allows read-only access to `/user`, including email address
|
||||
- `read:org`: Allows read-only access to `/user/teams`, listing all the user's groups as teams.
|
||||
|
||||
### Email scope verification
|
||||
|
||||
In authentik releases prior to 2025.10, the email scope always set the `email_verified` claim to `True`. Since authentik does not have a single authoritative source to determine whether a user's email is actually verified, asserting this claim could have security implications. As of 2025.10, `email_verified` now defaults to `False`.
|
||||
|
||||
Some applications require this claim to be `True` in order to authenticate users. In those cases, you can create a custom email scope mapping (**Customization** > **Property Mappings**) that always returns `email_verified` as `True`:
|
||||
|
||||
```python
|
||||
return {
|
||||
"email": request.user.email,
|
||||
"email_verified": True
|
||||
}
|
||||
```
|
||||
|
||||
For greater security guarantees, verify users' email addresses and store the verification status as a user attribute (for example, `email_verified` set to `True` or `False`). You can then configure the scope mapping to return this value dynamically:
|
||||
|
||||
```python
|
||||
return {
|
||||
"email": request.user.email,
|
||||
"email_verified": request.user.attributes.get("email_verified", False)
|
||||
}
|
||||
```
|
||||
|
||||
## Signing & Encryption
|
||||
|
||||
[JWTs](https://jwt.io/introduction) created by authentik will always be signed.
|
||||
|
||||
@@ -15,15 +15,7 @@ Scope mappings are used by the OAuth2 provider to map information from authentik
|
||||
:::info Default value for `email_verified`
|
||||
By default, authentik sets the `email_verified` claim to `False`, since it has no way to confirm whether a user's email is verified. Setting this claim to `True` by default could introduce unintended security risks.
|
||||
|
||||
Be aware that some applications might require this claim to be true to successfully authenticate users. In this case you should create a custom email scope mapping that returns `email_verified` as `True`, using the following expression:
|
||||
|
||||
```
|
||||
return {
|
||||
"email": user.email,
|
||||
"email_verified": True,
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that some applications might require this claim to be true to successfully authenticate users. See [Email scope verification](../oauth2/index.mdx#email-scope-verification) for more information.
|
||||
:::
|
||||
|
||||
## Skip objects during synchronization
|
||||
|
||||
@@ -51,7 +51,7 @@ A new connection is created every time an endpoint is selected in the [User Inte
|
||||
|
||||
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
|
||||
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
The RAC provider utilizes [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
|
||||
For a full list of possible connection configurations, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
|
||||
|
||||
|
||||
@@ -66,8 +66,10 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
return {
|
||||
"private-key": "-----BEGIN SSH PRIVATE KEY-----
|
||||
import textwrap
|
||||
|
||||
private_key = textwrap.dedent("""
|
||||
-----BEGIN SSH PRIVATE KEY-----
|
||||
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
||||
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
||||
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
|
||||
@@ -75,7 +77,12 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
|
||||
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
|
||||
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
|
||||
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
|
||||
-----END SSH PRIVATE KEY-----",
|
||||
-----END SSH PRIVATE KEY-----
|
||||
""")
|
||||
|
||||
return {
|
||||
"username": "<your_username>",
|
||||
"private-key": private_key
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ The RADIUS provider supports EAP-TLS and [PAP](https://en.wikipedia.org/wiki/Pas
|
||||
<details>
|
||||
<summary>RADIUS compatibility matrix for password-based authentication:</summary>
|
||||
|
||||
This table represents the password-hash compatibillity with various RADIUS protocols.
|
||||
This table represents the password-hash compatibility with various RADIUS protocols.
|
||||
|
||||
<HashSupport />
|
||||
</details>
|
||||
@@ -61,7 +61,7 @@ For certificates, ensure that you use a client certificate and a server certific
|
||||
|
||||
For EAP-TLS, note that you should NOT use a globally known CA.
|
||||
|
||||
Using private PKI certificates that are trusted by the end-device is best practise. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via EAP-TLS, and in addition you should implement [custom validation](../../flows-stages/flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
|
||||
Using private PKI certificates that are trusted by the end-device is best practice. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via EAP-TLS, and in addition you should implement [custom validation](../../flows-stages/flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
|
||||
:::
|
||||
|
||||
### RADIUS attributes
|
||||
|
||||
@@ -9,7 +9,7 @@ authentik SAML providers can be created either from scratch or by using SAML met
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to solely create the provider by navigating to **Applications** > **Providers** and clicking **Create**.
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application details, and then click **Next**.
|
||||
4. Select **SAML Provider** as the **Provider Type**, and then click **Next**.
|
||||
5. On the **Configure SAML Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
|
||||
@@ -19,7 +19,7 @@ To create a provider along with the corresponding application that uses it for a
|
||||
If you have exported SAML metadata from your SP, you can optionally create the authentik SAML provider by importing this metadata.
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers** and click **Create** to create a provider.
|
||||
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
|
||||
3. Select **SAML Provider from Metadata** as the **Provider Type**, and then click **Next**.
|
||||
4. On the **Create SAML Provider from Metadata** page, provide the configuration settings along with an SP metadata file and then click **Finish** to create the provider.
|
||||
5. (Optional) Edit the created SAML provider and configure any further settings.
|
||||
@@ -33,7 +33,7 @@ After an authentik SAML provider has been created via any of the above methods,
|
||||
To download the metadata of an authentik SAML provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the name of the provider you want metadata from to open its overview tab.
|
||||
4. In the **Related objects** section, under **Metadata** click on **Download**. This will download the metadata XML file for that provider.
|
||||
|
||||
@@ -42,7 +42,7 @@ To download the metadata of an authentik SAML provider, follow these steps:
|
||||
To view and optionally download the metadata of an authentik SAML provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the name of the provider you want metadata from to open its overview tab.
|
||||
4. Navigate to the **Metadata** tab.
|
||||
5. The metadata for the provider will be shown in a codebox. You can optionally use the **Download** button to obtain the metadata as a file.
|
||||
|
||||
@@ -1,56 +1,42 @@
|
||||
---
|
||||
title: Configure an SSF provider
|
||||
authentik_version: "2025.2.0"
|
||||
description: "How to create and configure an SSF provider in authentik"
|
||||
authentik_enterprise: true
|
||||
authentik_preview: true
|
||||
tags:
|
||||
- backchannel
|
||||
- provider
|
||||
tags: [Shared Signals Framework, SSF, Apple Business Manager, backchannel]
|
||||
---
|
||||
|
||||
The workflow to implement an SSF provider as a [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers) for an application/provider pair is as follows:
|
||||
Follow this workflow to create and configure an SSF provider for an application:
|
||||
|
||||
1. Create the SSF provider (which serves as the backchannel provider).
|
||||
1. Create the SSF provider (which serves as the [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers)).
|
||||
2. Create an OIDC provider (which serves as the protocol provider for the application).
|
||||
3. Create the application, and assign both the OIDC provider and the SSF provider.
|
||||
|
||||
## Create the SSF provider
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. In the modal, select the **Provider Type** of **SSF**, and then click **Next**.
|
||||
|
||||
4. On the **New provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
|
||||
|
||||
5. Click **Finish** to create and save the provider.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
|
||||
3. Select **Shared Signals Framework Provider** as the **Provider Type**, and then click **Next**.
|
||||
4. On the **Create SSF Provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
|
||||
5. Click **Finish** to create the provider.
|
||||
|
||||
## Create the OIDC provider
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. In the modal, select the **Provider Type** of **OIDC**, and then click **Next**.
|
||||
|
||||
4. Define the settings for the provider, and then click **Finish** to save the new provider.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
|
||||
3. Select **OAuth2/OpenID Provider** as the **Provider Type**, and then click **Next**.
|
||||
4. On the **Create OAuth2/OpenID Provider** page, provide the configuration settings and then click **Finish** to create the provider.
|
||||
|
||||
## Create the application
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Applications**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. Define the settings for the application:
|
||||
- **Name**: define a descriptive name of the application.
|
||||
- **Slug**: optionally define the internal application name used in URLs.
|
||||
- **Group**: optionally select a group that you want to have access to this application.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create** to create an application.
|
||||
3. Configure the following required settings for the application:
|
||||
- **Name**: provide a descriptive name of the application.
|
||||
- **Slug**: provide the application slug used in URLs.
|
||||
- **Provider**: select the OIDC provider that you created.
|
||||
- **Backchannel Providers**: select the SSF provider you created.
|
||||
- **Policy engine mode**: define policy-based access.
|
||||
- **UI Settings**: optionally define a launch URL, an icon, and other UI elements.
|
||||
|
||||
- **Backchannel Providers**: select the SSF provider that you created.
|
||||
4. Click **Create** to save the new application.
|
||||
|
||||
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your list of Applications.
|
||||
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your application list.
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
---
|
||||
title: Shared Signals Framework (SSF) Provider
|
||||
sidebar_label: SSF Provider
|
||||
description: "Overview of SSF and the authentik SSF provider"
|
||||
authentik_version: "2025.2.0"
|
||||
authentik_enterprise: true
|
||||
authentik_preview: true
|
||||
tags: [Shared Signals Framework, SSF, Apple Business Manager]
|
||||
---
|
||||
|
||||
Shared Signals Framework (SSF) is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected, secure webhooks.
|
||||
The Shared Signals Framework (SSF) provider allows you to integrate applications with the Shared Signals Framework protocol.
|
||||
|
||||
## About Shared Signals Framework
|
||||
SSF is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected and secure webhooks.
|
||||
|
||||
In authentik, an SSF provider allows applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
|
||||
The authentik SSF provider allows OIDC applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
|
||||
|
||||
Events in authentik that are tracked via SSF include when an MFA device is added or removed, logouts, sessions being revoked by Admin or user clicking logout, or credentials changed.
|
||||
|
||||
Refer to our documentation to learn how to [create a SSF provider](./create-ssf-provider.md).
|
||||
|
||||
## Example use cases
|
||||
|
||||
One important use case for SFF is to [integrate Apple Business Manager](https://integrations.goauthentik.io/device-management/apple/) or any of the Apple device management platforms with authentik, so that users can enroll their Apple devices using their authentik credentials. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
|
||||
|
||||
Another use case for SSF is when an Admin wants to know if a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
|
||||
Another use case for SSF is when an administrator wants to know when a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
|
||||
|
||||
Another example use case is when an application uses SSF to subscribe to authorization events because the application needs to know if a user changed their password in authentik. If a user did change their password, then the application receives a POST request to write the fact that the password was changed.
|
||||
|
||||
## About using SSF in authentik
|
||||
## Using the authentik SSF provider
|
||||
|
||||
Let's look at a few details about using SSF in authentik.
|
||||
The SSF provider serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application.
|
||||
|
||||
The SSF provider in authentik serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application. Thus you will still need to [create a typical application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair) (using an OIDC provider), and when creating the application, assign the SSF provider as a backchannel provider.
|
||||
Therefore you still need to [create a typical OIDC application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair), and when creating the application, assign the SSF provider as a [backchannel provider](../../applications/manage_apps#backchannel-providers).
|
||||
|
||||
When an authentik Admin [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
|
||||
When an authentik administrator [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
|
||||
|
||||
### The application (the receiver)
|
||||
|
||||
Within the application, the admin creates an SSF stream (which comprises all the signals that the app wants to subscribe to) and defines the audience, called `aud` in the specification (the URL that identifies the stream). A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can change or delete the stream.
|
||||
Within the application, the administrator creates an SSF stream which lists all the signals that the application wants to subscribe to, and defines the audience (`aud`), which is the URL that identifies the stream. A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can also change or delete the stream.
|
||||
|
||||
Note that authentik doesn't specify which events to subscribe to; instead the application defines which they want to listen for.
|
||||
authentik does not specify which events to subscribe to; instead the application defines which events they want to listen for.
|
||||
|
||||
### authentik (the transmitter)
|
||||
|
||||
To configure authentik as a shared signals transmitter, the authentik Admin [creates a new provider](./create-ssf-provider), selecting the type "SSF", to serve as the backchannelprovider for the application.
|
||||
To configure authentik as a shared signals transmitter, the authentik administrator [creates a new SSF provider](./create-ssf-provider), to serve as the backchannelprovider for the application.
|
||||
|
||||
When creating the SSF provider you will need to select a signing key. This is the key that the Security Event Tokens (SET) is signed with.
|
||||
When creating the SSF provider you will need to select a signing key that is used to sign the Security Event Tokens (SET).
|
||||
|
||||
Optionally, you can specify a event retention time period: this value determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
|
||||
Optionally, you can specify a event retention time period, which determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
|
||||
|
||||
:::info
|
||||
:::note SET events
|
||||
Be aware that the SET events are different events than those displayed in the authentik Admin interface under **Events**.
|
||||
:::
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Create a WS-Federation provider
|
||||
---
|
||||
|
||||
An authentik WS-Federation provider is typically created as part of an application/provider pair, using the steps below. You can also create a standalone provider, and then later assign an application to use it.
|
||||
|
||||
## Create a WS-Federation provider and application pair
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application details, and then click **Next**.
|
||||
4. Select **WS-Federation Provider** as the **Provider Type**, and then click **Next**.
|
||||
5. On the **Configure WS-Federation Provider** page, provide a name for the provider, select an authorization flow, and the two required configuration settings:
|
||||
- **Reply URL**: Enter the application callback URL, where the token should be sent. This is the specific endpoint on an RP (application) where an Identity Provider (STS) sends the security token and authentication response after after a successful log in.
|
||||
- **Realm**: Enter the identifier (string) of the requesting realm; that is, the Relying Party (RP) or application receiving the token. Realm is similar to the SAML 2.0 Entity ID.
|
||||
6. Click **Submit** to create both the application and the provider.
|
||||
|
||||
## Export authentik WS-Federation provider metadata
|
||||
|
||||
After an authentik WS-Federation provider has been created via any of the above methods, you can access its metadata in one of two ways:
|
||||
|
||||
### Download authentik metadata for a WS-Federation provider
|
||||
|
||||
To download the metadata of an authentik WS-Federation provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
3. Click the name of the provider you want metadata for.
|
||||
4. On the **Overview** tab, in the **Related objects** section, click on **Download** under **Metadata**. This will download the metadata XML file for that provider.
|
||||
|
||||
### Access the Metadata tab for a WS-Federation provider
|
||||
|
||||
To view and optionally download the metadata of an authentik WS-Federation provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
3. Click the name of the provider you want metadata for, and then click the the **Metadata** tab.
|
||||
4. The metadata for the provider will be shown in a codebox. You can optionally use the **Download** button to obtain the metadata as a file.
|
||||
53
website/docs/add-secure-apps/providers/wsfed/index.md
Normal file
53
website/docs/add-secure-apps/providers/wsfed/index.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: WS-Federation Provider
|
||||
---
|
||||
|
||||
The WS-Federation provider is used to integrate with applications and service providers that use [WS-Federation protocol](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adfsod/204de335-ea34-4f9b-ae73-8b7d4c8152d1). WS-Fedederation is an XML-based identity federation protocol that uses token exchange for federated Single Sign-On (SSO) and IdP authentication, specifically for Windows applications such as Sharepoint.
|
||||
|
||||
There are similarities between WS-Federation and SAML protocols, but there are several key differences in terminology, most importantly:
|
||||
|
||||
- WS-Federation term: **STS (Security Token Service)**
|
||||
- SAML term: **IdP (Identity Provider)**
|
||||
|
||||
:::info SAML2 token support
|
||||
Note that we only support the SAML2 token type within WS-Federation providers, and that using the WS-Federation provider with Entra ID is not supported because Entra ID requires a SAML 1.0 token.
|
||||
:::
|
||||
|
||||
## Supported URL request parameters
|
||||
|
||||
The following URL request parameters are supported in the authentik WS-Federation provider:
|
||||
|
||||
- **wa**: The is a required parameter that represents the action being requested, typically wsignin1.0 for signing in. The parameter's value tells the Security Token Service (STS) which operation to execute.
|
||||
- **wtrealm**: The unique identifier (realm) of the Relying Party (RP) or application requesting the security token, for example, urn:my-app:rp. It defines the trust relationship between the RP and the Identity Provider (IdP) and indicates which application is initiating the WS-Federation request. This is a required query parameter that tells the Security Token Service (STS) which relying party the token is intended for.
|
||||
- **wreply**: The target URL to which the Identity Provider (IdP) sends the WS-Federation response containing the security token. This URL is supplied by the Service Provider (SP). authentik verifies that the received `wreply` parameter matches the URL configured by the administrator and stored in the database.
|
||||
- **wctx**: A context value that is used to maintain state between the Relying Party (RP) and the Identity Provider (IdP) across redirects. It serves the same purpose as the `RelayState` parameter in SAML. The RP includes this value in the authentication request, and the IdP returns it unchanged in the response, allowing the RP to validate and restore the original session or request context.
|
||||
|
||||
## WS-Federation bindings and endpoints
|
||||
|
||||
_Bindings_ define how an Identity Provider (IdP) and the WS-Federation STS (Security Token Service), or IdP in SAML terms, communicate; how messages are transported over network protocols, specifying transport (like HTTP), encoding, and security detail that allow WS-Federation to facilitate secure identity sharing across systems. Both the IdP and STS define various endpoints in their metadata, each associated with a specific WS-Federation binding.
|
||||
|
||||
| Endpoint | URL |
|
||||
| -------- | --------------------- |
|
||||
| SSO/SLO | `/application/wsfed/` |
|
||||
|
||||
## WS-Federation metadata
|
||||
|
||||
Using metadata ensures that WS-Federation single sign-on works reliably by exchanging and maintaining identity and connection information. WS-Federation metadata is an XML document that defines how IdPs and SPs securely interact for authentication. It includes information such as endpoints, bindings, certificates, and unique identifiers. The metadata is what you provide the application to configure it for authenticating with authentik.
|
||||
|
||||
You can [export WS-Federation metadata](./create-wsfed-provider.md#export-authentik-ws-federation-provider-metadata) from an authentik WS-Federation provider to an STS to automatically provide important endpoint and certificate information to the SP.
|
||||
|
||||
## Certificates
|
||||
|
||||
The certificates used with WS-Federation to sign Request Security Token Response (RSTR), which contains the assertion, are the same certificates that are used by SAML.
|
||||
|
||||
For details, refer to our [SAML certificates documentation](../saml/index.md#certificates).
|
||||
|
||||
## WS-Federation property mappings
|
||||
|
||||
Property mappings are used during the authentication process to align, or "map", user attributes values between the SP and STS (Security Token Service), the latter being the equivalent of SAML's IdP.
|
||||
|
||||
The same property mappings that are used in WS-Federation are used in SAML. For details, refer to our [SAML property mapping documentation](../saml/index.md#certificates).
|
||||
|
||||
## Attributes for WS-Federation
|
||||
|
||||
Ws-Federation and SAML also share the use of the [NameID](../saml/index.md#nameid) and the [AuthnContextClassRef](../saml/index.md#authncontextclassref) attributes.
|
||||
@@ -8,7 +8,7 @@ To migrate existing configurations to blueprints, run `ak export_blueprint` with
|
||||
|
||||
Exported blueprints don't use any of the YAML Tags, they just contain a list of entries as they are in the database.
|
||||
|
||||
Note that fields which are write-only (for example, OAuth Provider's Secret Key) will not be added to the blueprint, as the serialisation logic from the API is used for blueprints.
|
||||
Note that fields which are write-only (for example, OAuth Provider's Secret Key) will not be added to the blueprint, as the serialization logic from the API is used for blueprints.
|
||||
|
||||
Additionally, default values will be skipped and not added to the blueprint.
|
||||
|
||||
|
||||
3
website/docs/enterprise/entfeatures.md
Normal file
3
website/docs/enterprise/entfeatures.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Enterprise Features
|
||||
---
|
||||
@@ -429,7 +429,7 @@ Overrides [`AUTHENTIK_STORAGE__FILE__[...]`](#file-storage-backend-settings) set
|
||||
|
||||
#### `AUTHENTIK_STORAGE__MEDIA__S3__[...]`
|
||||
|
||||
Overrides [`AUTHENTIK_STORAGE__FILE__[...]`](#file-storage-backend-settings) settings.
|
||||
Overrides [`AUTHENTIK_STORAGE__S3__[...]`](#s3-storage-backend-settings) settings.
|
||||
|
||||
These settings affect where media files are stored. Those files include applications and sources icons. By default, they use the same storage settings as the main storage configuration. S3 storage is also supported.
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ Every application that you add to authentik requires a provider, which is used t
|
||||
- **Configure the Application**:
|
||||
- **Name**: provide a descriptive name (such as Grafana).
|
||||
- **Group**: select an optional group for the application; groups are used to visually separate applications. For example, you can choose to group applications that you use for coding from those you use for internal communication.
|
||||
- **Policy engine mode**: select **Any** for this tutorial; the mode determnes how strictly policies are adhered to.
|
||||
- **Policy engine mode**: select **Any** for this tutorial; the mode determines how strictly policies are adhered to.
|
||||
- <strong className="tip">TIP</strong>: in authentik,
|
||||
[policies](../../customize/policies/working_with_policies.md) are used in authentik to
|
||||
fine-tune access to applications, flows, stages and many other authentik components. It is
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user