mirror of
https://github.com/goauthentik/authentik
synced 2026-05-15 11:26:31 +02:00
Compare commits
40 Commits
version/20
...
website/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15ad260333 | ||
|
|
b76539e73f | ||
|
|
c205a41cb5 | ||
|
|
32f2d3ad30 | ||
|
|
a9e382d6c5 | ||
|
|
a35005416b | ||
|
|
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 |
@@ -80,7 +80,7 @@ jobs:
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
|
||||
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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
id: push
|
||||
with:
|
||||
push: true
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,10 @@ class EmailTemplates(models.TextChoices):
|
||||
"email/event_notification.html",
|
||||
_("Event Notification"),
|
||||
)
|
||||
INVITATION = (
|
||||
"email/invitation.html",
|
||||
_("Invitation"),
|
||||
)
|
||||
|
||||
|
||||
def get_template_choices():
|
||||
|
||||
55
authentik/stages/email/templates/email/invitation.html
Normal file
55
authentik/stages/email/templates/email/invitation.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "email/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td align="center">
|
||||
<h1>
|
||||
{% blocktrans %}
|
||||
You're Invited!
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td align="center" style="max-width: 300px; padding: 20px 0; color: #212124;">
|
||||
{% blocktrans %}
|
||||
You have been invited to join {{ host }}. Click the button below to get started.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if expires %}
|
||||
<tr>
|
||||
<td align="center" style="max-width: 300px; padding: 10px 0; color: #212124; font-size: 12px;">
|
||||
{% blocktrans with expires=expires|naturaltime %}
|
||||
This invitation expires {{ expires }}.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td align="center" class="btn btn-primary">
|
||||
<a id="confirm" href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Accept Invitation' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
||||
|
||||
{% block sub_content %}
|
||||
<tr>
|
||||
<td style="padding: 20px; font-size: 12px; color: #212124;" align="center">
|
||||
{% blocktrans %}
|
||||
If you cannot click the button above, please copy and paste the following URL into your browser:
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
<a href="{{ url }}" rel="noopener noreferrer" target="_blank">{{ url }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
||||
16
authentik/stages/email/templates/email/invitation.txt
Normal file
16
authentik/stages/email/templates/email/invitation.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% blocktrans %}You're Invited!{% endblocktrans %}
|
||||
|
||||
{% blocktrans %}You have been invited to join {{ host }}. Use the link below to get started.{% endblocktrans %}
|
||||
|
||||
{% trans 'Accept Invitation' %}: {{ url }}
|
||||
|
||||
{% if expires %}
|
||||
{% blocktrans with expires=expires|naturaltime %}This invitation expires {{ expires }}.{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% blocktrans %}If you cannot click the link above, please copy and paste the following URL into your browser:{% endblocktrans %}
|
||||
|
||||
{{ url }}
|
||||
@@ -54,7 +54,7 @@ class TestEmailStageTemplates(FlowTestCase):
|
||||
chmod(file2, 0o000) # Remove all permissions so we can't read the file
|
||||
choices = get_template_choices()
|
||||
self.assertEqual(choices[-1][0], Path(file).name)
|
||||
self.assertEqual(len(choices), 5)
|
||||
self.assertEqual(len(choices), 6)
|
||||
unlink(file)
|
||||
unlink(file2)
|
||||
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
"""Invitation Stage API Views"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django_filters.filters import BooleanFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.serializers import PrimaryKeyRelatedField
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
CharField,
|
||||
ListField,
|
||||
PrimaryKeyRelatedField,
|
||||
Serializer,
|
||||
)
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.groups import PartialUserSerializer
|
||||
@@ -13,8 +24,11 @@ from authentik.core.api.utils import JSONDictField, ModelSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.api.flows import FlowSerializer
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class InvitationStageSerializer(StageSerializer):
|
||||
"""InvitationStage Serializer"""
|
||||
@@ -77,6 +91,15 @@ class InvitationSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class InvitationSendEmailSerializer(Serializer):
|
||||
"""Serializer for sending invitation emails"""
|
||||
|
||||
email_addresses = ListField(required=True)
|
||||
cc_addresses = ListField(required=False)
|
||||
bcc_addresses = ListField(required=False)
|
||||
template = CharField(required=False, default="invitation")
|
||||
|
||||
|
||||
class InvitationViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Invitation Viewset"""
|
||||
|
||||
@@ -91,3 +114,61 @@ class InvitationViewSet(UsedByMixin, ModelViewSet):
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT not in serializer.context:
|
||||
kwargs["created_by"] = self.request.user
|
||||
serializer.save(**kwargs)
|
||||
|
||||
@extend_schema(
|
||||
request=InvitationSendEmailSerializer,
|
||||
responses={204: None},
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
serializer_class=InvitationSendEmailSerializer,
|
||||
)
|
||||
def send_email(self, request: Request, pk: str) -> Response:
|
||||
"""Send invitation link via email to one or more addresses"""
|
||||
invitation = self.get_object()
|
||||
email_addresses = request.data.get("email_addresses", [])
|
||||
cc_addresses = request.data.get("cc_addresses", [])
|
||||
bcc_addresses = request.data.get("bcc_addresses", [])
|
||||
template = request.data.get("template", "email/invitation.html")
|
||||
|
||||
if not email_addresses:
|
||||
return Response({"error": "No email addresses provided"}, status=400)
|
||||
|
||||
# Build the invitation link
|
||||
http_request: HttpRequest = request._request
|
||||
protocol = "https" if http_request.is_secure() else "http"
|
||||
host = http_request.get_host()
|
||||
|
||||
# Determine the flow slug
|
||||
flow_slug = invitation.flow.slug if invitation.flow else None
|
||||
if not flow_slug:
|
||||
return Response({"error": "Invitation has no associated flow"}, status=400)
|
||||
|
||||
invitation_link = f"{protocol}://{host}/if/flow/{flow_slug}/?itoken={invitation.pk}"
|
||||
|
||||
# Prepare template context
|
||||
context = {
|
||||
"url": invitation_link,
|
||||
"expires": invitation.expires,
|
||||
"host": host,
|
||||
}
|
||||
|
||||
# Prepare email content
|
||||
subject = f"You have been invited to {host}"
|
||||
|
||||
# Queue emails for sending via async ak_send_email
|
||||
evaluator = BaseEvaluator()
|
||||
|
||||
for email in email_addresses:
|
||||
evaluator.expr_send_email(
|
||||
address=email,
|
||||
subject=subject,
|
||||
template=template,
|
||||
context=context,
|
||||
stage=None,
|
||||
cc=cc_addresses if cc_addresses else None,
|
||||
bcc=bcc_addresses if bcc_addresses else None,
|
||||
)
|
||||
|
||||
return Response(status=204)
|
||||
|
||||
@@ -217,3 +217,105 @@ class TestInvitationsAPI(APITestCase):
|
||||
self.assertEqual(invitation.created_by, get_anonymous_user())
|
||||
self.assertEqual(invitation.name, "test-blueprint-invitation")
|
||||
self.assertEqual(invitation.fixed_data, {"email": "test@example.com"})
|
||||
|
||||
def test_send_email_no_addresses(self):
|
||||
"""Test send_email endpoint with no email addresses"""
|
||||
flow = create_test_flow(FlowDesignation.ENROLLMENT)
|
||||
invite = Invitation.objects.create(
|
||||
name="test-invite",
|
||||
created_by=self.user,
|
||||
flow=flow,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
|
||||
{"email_addresses": []},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("error", response.data)
|
||||
|
||||
def test_send_email_no_flow(self):
|
||||
"""Test send_email endpoint with invitation without flow"""
|
||||
invite = Invitation.objects.create(
|
||||
name="test-invite-no-flow",
|
||||
created_by=self.user,
|
||||
flow=None,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
|
||||
{"email_addresses": ["test@example.com"]},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("error", response.data)
|
||||
|
||||
@patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email")
|
||||
def test_send_email_success(self, mock_send_email: MagicMock):
|
||||
"""Test send_email endpoint successfully queues emails"""
|
||||
flow = create_test_flow(FlowDesignation.ENROLLMENT)
|
||||
invite = Invitation.objects.create(
|
||||
name="test-invite",
|
||||
created_by=self.user,
|
||||
flow=flow,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
|
||||
{
|
||||
"email_addresses": ["user1@example.com", "user2@example.com"],
|
||||
"template": "email/invitation.html",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(mock_send_email.call_count, 2)
|
||||
|
||||
@patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email")
|
||||
def test_send_email_with_cc_bcc(self, mock_send_email: MagicMock):
|
||||
"""Test send_email endpoint with CC and BCC addresses"""
|
||||
flow = create_test_flow(FlowDesignation.ENROLLMENT)
|
||||
invite = Invitation.objects.create(
|
||||
name="test-invite",
|
||||
created_by=self.user,
|
||||
flow=flow,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
|
||||
{
|
||||
"email_addresses": ["user@example.com"],
|
||||
"cc_addresses": ["cc@example.com"],
|
||||
"bcc_addresses": ["bcc@example.com"],
|
||||
"template": "email/invitation.html",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
mock_send_email.assert_called_once()
|
||||
call_kwargs = mock_send_email.call_args.kwargs
|
||||
self.assertEqual(call_kwargs["cc"], ["cc@example.com"])
|
||||
self.assertEqual(call_kwargs["bcc"], ["bcc@example.com"])
|
||||
|
||||
@patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email")
|
||||
def test_send_email_context(self, mock_send_email: MagicMock):
|
||||
"""Test send_email endpoint passes correct context to email"""
|
||||
flow = create_test_flow(FlowDesignation.ENROLLMENT)
|
||||
invite = Invitation.objects.create(
|
||||
name="test-invite",
|
||||
created_by=self.user,
|
||||
flow=flow,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}),
|
||||
{"email_addresses": ["user@example.com"]},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
mock_send_email.assert_called_once()
|
||||
call_kwargs = mock_send_email.call_args.kwargs
|
||||
self.assertIn("url", call_kwargs["context"])
|
||||
self.assertIn(str(invite.pk), call_kwargs["context"]["url"])
|
||||
self.assertIn(flow.slug, call_kwargs["context"]["url"])
|
||||
|
||||
@@ -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.20260213141435-0db2228fbd47
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/sync v0.19.0
|
||||
|
||||
6
go.sum
6
go.sum
@@ -218,6 +218,12 @@ goauthentik.io/api/v3 v3.2026020.17-0.20260205232234-280022b0a8de h1:X1ELA34R1N+
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260205232234-280022b0a8de/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535 h1:DPk8z6SGesp0gbmaD2zTAKVSd/NQ++Nu+lu3UrCkNvE=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260210174940-ae049de99535/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211005401-cdd71ec2f62f h1:KK5lBHSvZSlMbUViB7KStlkP9kC1t9JeiMawa7wyI6Q=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211005401-cdd71ec2f62f/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393 h1:eLRd2GC+pxvwd3m2msJRNB9upH7pcIZH5V4L9/WhRcw=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260211204352-035cbbe57393/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260213141435-0db2228fbd47 h1:quNPFsxsMNKICxrJP3dFehxgCvt3Qi9UeV8HzcEk17c=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260213141435-0db2228fbd47/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -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.
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,21 +1,20 @@
|
||||
[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",
|
||||
"django-channels-postgres",
|
||||
"django-countries==7.6.1",
|
||||
"django-cte==3.0.0",
|
||||
"django-dramatiq-postgres",
|
||||
"django-filter==25.2",
|
||||
"django-model-utils==5.0.0",
|
||||
@@ -37,7 +36,7 @@ dependencies = [
|
||||
"fido2==2.1.1",
|
||||
"geoip2==5.2.0",
|
||||
"geopy==2.4.1",
|
||||
"google-api-python-client==2.189.0",
|
||||
"google-api-python-client==2.190.0",
|
||||
"gssapi==1.11.1",
|
||||
"gunicorn==25.0.3",
|
||||
"jsonpatch==1.33",
|
||||
@@ -69,7 +68,7 @@ dependencies = [
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.40.0",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.7.0",
|
||||
"webauthn==2.7.1",
|
||||
"wsproto==1.3.2",
|
||||
"xmlsec==1.3.17",
|
||||
"zxcvbn==4.5.0",
|
||||
@@ -103,7 +102,7 @@ dev = [
|
||||
"pytest-timeout==2.4.0",
|
||||
"pytest==9.0.2",
|
||||
"requests-mock==1.12.1",
|
||||
"ruff==0.15.0",
|
||||
"ruff==0.15.1",
|
||||
"selenium==4.40.0",
|
||||
"types-channels==4.3.0.20250822",
|
||||
"types-docker==7.1.0.20260109",
|
||||
|
||||
112
schema.yml
112
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:
|
||||
@@ -30597,6 +30605,35 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/invitation/invitations/{invite_uuid}/send_email/:
|
||||
post:
|
||||
operationId: stages_invitation_invitations_send_email_create
|
||||
description: Send invitation link via email to one or more addresses
|
||||
parameters:
|
||||
- in: path
|
||||
name: invite_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Invitation.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InvitationSendEmailRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/invitation/invitations/{invite_uuid}/used_by/:
|
||||
get:
|
||||
operationId: stages_invitation_invitations_used_by_list
|
||||
@@ -40696,8 +40733,7 @@ components:
|
||||
logout_urls:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
$ref: '#/components/schemas/LogoutURL'
|
||||
IframeLogoutChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response for iframe logout
|
||||
@@ -40845,6 +40881,25 @@ components:
|
||||
description: When set, only the configured flow can use this invitation.
|
||||
required:
|
||||
- name
|
||||
InvitationSendEmailRequest:
|
||||
type: object
|
||||
description: Serializer for sending invitation emails
|
||||
properties:
|
||||
email_addresses:
|
||||
type: array
|
||||
items: {}
|
||||
cc_addresses:
|
||||
type: array
|
||||
items: {}
|
||||
bcc_addresses:
|
||||
type: array
|
||||
items: {}
|
||||
template:
|
||||
type: string
|
||||
minLength: 1
|
||||
default: invitation
|
||||
required:
|
||||
- email_addresses
|
||||
InvitationStage:
|
||||
type: object
|
||||
description: InvitationStage Serializer
|
||||
@@ -42393,6 +42448,29 @@ components:
|
||||
required:
|
||||
- challenge
|
||||
- name
|
||||
LogoutURL:
|
||||
type: object
|
||||
description: Data for a single logout URL
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
nullable: true
|
||||
binding:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_request:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_response:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_relay_state:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- url
|
||||
MDMConfigRequest:
|
||||
type: object
|
||||
description: Base serializer class which doesn't implement create/update methods
|
||||
@@ -42956,21 +43034,23 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
post_url:
|
||||
type: string
|
||||
saml_request:
|
||||
type: string
|
||||
relay_state:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
binding:
|
||||
type: string
|
||||
redirect_url:
|
||||
type: string
|
||||
is_complete:
|
||||
type: boolean
|
||||
default: false
|
||||
post_url:
|
||||
type: string
|
||||
redirect_url:
|
||||
type: string
|
||||
saml_binding:
|
||||
$ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
saml_request:
|
||||
type: string
|
||||
saml_response:
|
||||
type: string
|
||||
saml_relay_state:
|
||||
type: string
|
||||
NativeLogoutChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response for native browser logout
|
||||
@@ -49905,6 +49985,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
@@ -53397,6 +53479,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
@@ -53591,6 +53675,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
|
||||
@@ -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
|
||||
|
||||
181
uv.lock
generated
181
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" },
|
||||
@@ -235,7 +226,6 @@ dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-channels-postgres" },
|
||||
{ name = "django-countries" },
|
||||
{ name = "django-cte" },
|
||||
{ name = "django-dramatiq-postgres" },
|
||||
{ name = "django-filter" },
|
||||
{ name = "django-model-utils" },
|
||||
@@ -336,16 +326,15 @@ 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" },
|
||||
{ name = "django", specifier = "==5.2.11" },
|
||||
{ name = "django-channels-postgres", editable = "packages/django-channels-postgres" },
|
||||
{ name = "django-countries", specifier = "==7.6.1" },
|
||||
{ name = "django-cte", specifier = "==3.0.0" },
|
||||
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
|
||||
{ name = "django-filter", specifier = "==25.2" },
|
||||
{ name = "django-model-utils", specifier = "==5.0.0" },
|
||||
@@ -366,7 +355,7 @@ requires-dist = [
|
||||
{ name = "fido2", specifier = "==2.1.1" },
|
||||
{ name = "geoip2", specifier = "==5.2.0" },
|
||||
{ name = "geopy", specifier = "==2.4.1" },
|
||||
{ name = "google-api-python-client", specifier = "==2.189.0" },
|
||||
{ name = "google-api-python-client", specifier = "==2.190.0" },
|
||||
{ name = "gssapi", specifier = "==1.11.1" },
|
||||
{ name = "gunicorn", specifier = "==25.0.3" },
|
||||
{ name = "jsonpatch", specifier = "==1.33" },
|
||||
@@ -398,7 +387,7 @@ requires-dist = [
|
||||
{ name = "urllib3", specifier = "<3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.40.0" },
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
{ name = "webauthn", specifier = "==2.7.0" },
|
||||
{ name = "webauthn", specifier = "==2.7.1" },
|
||||
{ name = "wsproto", specifier = "==1.3.2" },
|
||||
{ name = "xmlsec", specifier = "==1.3.17" },
|
||||
{ name = "zxcvbn", specifier = "==4.5.0" },
|
||||
@@ -432,7 +421,7 @@ dev = [
|
||||
{ name = "pytest-randomly", specifier = "==4.0.1" },
|
||||
{ name = "pytest-timeout", specifier = "==2.4.0" },
|
||||
{ name = "requests-mock", specifier = "==1.12.1" },
|
||||
{ name = "ruff", specifier = "==0.15.0" },
|
||||
{ name = "ruff", specifier = "==0.15.1" },
|
||||
{ name = "selenium", specifier = "==4.40.0" },
|
||||
{ name = "types-channels", specifier = "==4.3.0.20250822" },
|
||||
{ name = "types-docker", specifier = "==7.1.0.20260109" },
|
||||
@@ -710,11 +699,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "7.0.0"
|
||||
version = "7.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -932,55 +921,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]]
|
||||
@@ -1138,18 +1127,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/46/b6931858e5161e5d9166bfcfde3af0b7d60ba89e4f7dd8f033e591c68794/django_countries-7.6.1-py3-none-any.whl", hash = "sha256:1ed20842fe0f6194f91faca21076649513846a8787c9eb5aeec3cbe1656b8acc", size = 864507, upload-time = "2024-04-01T21:01:05.702Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-cte"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/c0/64cda7c7b3e5641160a4c9dd1030b3a567592ba2b6c64f5303678a780084/django_cte-3.0.0.tar.gz", hash = "sha256:888710bb7109559621a34ab890f0f87d54188c9678f874e61e82112b59bbccb4", size = 11422, upload-time = "2026-02-05T13:08:53.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8a/5fdf282b8496e485b007d48ac211a0e2b203b8623e67c32ed2cf65faedc4/django_cte-3.0.0-py3-none-any.whl", hash = "sha256:3eabb89b68d328a6a97c695a21f45ed6ee7a6602193670db74e70c2e17bf2cd5", size = 13211, upload-time = "2026-02-05T13:08:51.908Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-dramatiq-postgres"
|
||||
version = "0.1.0"
|
||||
@@ -1655,7 +1632,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.189.0"
|
||||
version = "2.190.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
@@ -1664,9 +1641,9 @@ dependencies = [
|
||||
{ name = "httplib2" },
|
||||
{ name = "uritemplate" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/f8/0783aeca3410ee053d4dd1fccafd85197847b8f84dd038e036634605d083/google_api_python_client-2.189.0.tar.gz", hash = "sha256:45f2d8559b5c895dde6ad3fb33de025f5cb2c197fa5862f18df7f5295a172741", size = 13979470, upload-time = "2026-02-03T19:24:55.432Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/4ab3e3516b93bb50ed7814738ea61d49cba3f72f4e331dc9518ae2731e92/google_api_python_client-2.190.0.tar.gz", hash = "sha256:5357f34552e3724d80d2604c8fa146766e0a9d6bb0afada886fafed9feafeef6", size = 14111143, upload-time = "2026-02-12T00:38:03.37Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl", hash = "sha256:a258c09660a49c6159173f8bbece171278e917e104a11f0640b34751b79c8a1a", size = 14547633, upload-time = "2026-02-03T19:24:52.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ad/223d5f4b0b987669ffeb3eadd7e9f85ece633aa7fd3246f1e2f6238e1e05/google_api_python_client-2.190.0-py3-none-any.whl", hash = "sha256:d9b5266758f96c39b8c21d9bbfeb4e58c14dbfba3c931f7c5a8d7fdcd292dd57", size = 14682070, upload-time = "2026-02-12T00:38:00.974Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3308,27 +3285,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4003,17 +3980,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "webauthn"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asn1crypto" },
|
||||
{ name = "cbor2" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "pyopenssl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/f0/e1036df8842782a2947e5f41e76a4accb92e3dba972dba882321ebe15af0/webauthn-2.7.0.tar.gz", hash = "sha256:3c45c25e75a7d7d419220ccd10b8b899984de8012732e10d898f0a8f8c480575", size = 123770, upload-time = "2025-09-04T23:19:21.602Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/f4/9529bcf85ef46c76842b84c66ffa8ec31f18e3aacd1330b62f440077b45b/webauthn-2.7.1.tar.gz", hash = "sha256:2a1ebbfffc4a83e31d3db5d69113944bc49d05fae77770c2d4e388386cb9656e", size = 124256, upload-time = "2026-02-11T23:36:02.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b1/3f380d02552f1d75d3db789f761a1ee0dafd6181ebc07dd4b9ded61225a4/webauthn-2.7.0-py3-none-any.whl", hash = "sha256:2ecfee7959b09ebeaaffee9f8982ecdbbdc369a11766d20d4bc0637b36e235b7", size = 71311, upload-time = "2025-09-04T23:19:20.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/5b/f73513367a9d34b199de916b44306acfa4027b57f7e22200421212b1f763/webauthn-2.7.1-py3-none-any.whl", hash = "sha256:d57e9613c65e0c6a4db7ee715fb49ebdf3c4a6eb3979729eeb497c99105e8181", size = 71684, upload-time = "2026-02-11T23:36:00.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import "#admin/stages/invitation/InvitationSendEmailForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { writeToClipboard } from "#common/clipboard";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
@@ -9,6 +14,7 @@ import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
@@ -21,7 +27,7 @@ export class InvitationListLink extends AKElement {
|
||||
@property()
|
||||
selectedFlow?: string;
|
||||
|
||||
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList];
|
||||
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList, PFButton];
|
||||
|
||||
renderLink(): string {
|
||||
if (this.invitation?.flowObj) {
|
||||
@@ -102,6 +108,35 @@ export class InvitationListLink extends AKElement {
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Actions")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
@click=${() => {
|
||||
writeToClipboard(this.renderLink());
|
||||
}}
|
||||
>
|
||||
${msg("Copy Link")}
|
||||
</button>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Send")}</span>
|
||||
<span slot="header">${msg("Send Invitation via Email")}</span>
|
||||
<ak-invitation-send-email-form
|
||||
slot="form"
|
||||
.invitation=${this.invitation}
|
||||
>
|
||||
</ak-invitation-send-email-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Send via Email")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
</div>
|
||||
<small>${item.createdBy.name}</small>`,
|
||||
html`${item.expires?.toLocaleString() || msg("-")}`,
|
||||
html` <ak-forms-modal>
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update Invitation")}</span>
|
||||
<ak-invitation-form slot="form" .instancePk=${item.pk}> </ak-invitation-form>
|
||||
|
||||
167
web/src/admin/stages/invitation/InvitationSendEmailForm.ts
Normal file
167
web/src/admin/stages/invitation/InvitationSendEmailForm.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#components/ak-textarea-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
|
||||
import { type DescriptionPair, renderDescriptionList } from "#components/DescriptionList";
|
||||
|
||||
import { Invitation, StagesApi, TypeCreate } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
interface InvitationSendEmailRequestWithTemplate {
|
||||
emailAddresses: string;
|
||||
ccAddresses?: string;
|
||||
bccAddresses?: string;
|
||||
template?: TypeCreate;
|
||||
}
|
||||
|
||||
@customElement("ak-invitation-send-email-form")
|
||||
export class InvitationSendEmailForm extends Form<InvitationSendEmailRequestWithTemplate> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [...super.styles, PFDescriptionList];
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
invitation?: Invitation;
|
||||
|
||||
@state()
|
||||
availableTemplates: TypeCreate[] = [];
|
||||
|
||||
@state()
|
||||
selectedTemplate = "email/invitation.html";
|
||||
|
||||
fetchAvailableTemplates = async (): Promise<void> => {
|
||||
try {
|
||||
this.availableTemplates = await new StagesApi(
|
||||
DEFAULT_CONFIG,
|
||||
).stagesEmailTemplatesList();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch email templates:", error);
|
||||
}
|
||||
};
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("ak-modal-show", this.fetchAvailableTemplates);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("ak-modal-show", this.fetchAvailableTemplates);
|
||||
}
|
||||
|
||||
parseEmailAddresses(addresses: string): string[] {
|
||||
return addresses
|
||||
.split(/[\n,;]/)
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => email.length > 0);
|
||||
}
|
||||
|
||||
async send(data: InvitationSendEmailRequestWithTemplate): Promise<void> {
|
||||
const addresses = this.parseEmailAddresses(data.emailAddresses);
|
||||
const ccAddresses = this.parseEmailAddresses(data.ccAddresses ?? "");
|
||||
const bccAddresses = this.parseEmailAddresses(data.bccAddresses ?? "");
|
||||
|
||||
if (addresses.length === 0) {
|
||||
showMessage({
|
||||
message: msg("Please enter at least one email address"),
|
||||
level: MessageLevel.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsSendEmailCreate({
|
||||
inviteUuid: this.invitation?.pk || "",
|
||||
invitationSendEmailRequest: {
|
||||
emailAddresses: addresses,
|
||||
ccAddresses: ccAddresses.length > 0 ? ccAddresses : undefined,
|
||||
bccAddresses: bccAddresses.length > 0 ? bccAddresses : undefined,
|
||||
template: data.template?.name,
|
||||
},
|
||||
});
|
||||
|
||||
showMessage({
|
||||
message: msg(
|
||||
str`Invitation emails queued for sending to ${addresses.length} recipient(s). Check the System Tasks for more information.`,
|
||||
),
|
||||
level: MessageLevel.success,
|
||||
});
|
||||
} catch (error) {
|
||||
showMessage({
|
||||
message: msg(str`Failed to queue invitation emails: ${error}`),
|
||||
level: MessageLevel.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
const expiresDisplay = this.invitation?.expires
|
||||
? this.invitation.expires.toLocaleString()
|
||||
: msg("Never");
|
||||
|
||||
const invitationInfo: DescriptionPair[] = [
|
||||
[msg("Name"), this.invitation?.name ?? "-"],
|
||||
[msg("Expires"), expiresDisplay],
|
||||
[msg("Flow"), this.invitation?.flowObj?.slug ?? msg("No flow set")],
|
||||
[msg("Single use"), this.invitation?.singleUse ? msg("Yes") : msg("No")],
|
||||
];
|
||||
|
||||
return html`${renderDescriptionList(invitationInfo, { horizontal: true, twocolumn: true })}
|
||||
<ak-textarea-input
|
||||
label=${msg("To")}
|
||||
name="emailAddresses"
|
||||
required
|
||||
help=${msg(
|
||||
"One email address per line, or comma/semicolon separated. Each recipient will receive a separate email with an invitation link.",
|
||||
)}
|
||||
>
|
||||
</ak-textarea-input>
|
||||
<ak-textarea-input
|
||||
label=${msg("CC")}
|
||||
name="ccAddresses"
|
||||
help=${msg(
|
||||
"A comma-separated list of addresses to receive copies of the invitation. Recipients will receive the full list of other addresses in this list.",
|
||||
)}
|
||||
>
|
||||
</ak-textarea-input>
|
||||
<ak-textarea-input
|
||||
label=${msg("BCC")}
|
||||
name="bccAddresses"
|
||||
help=${msg(
|
||||
"A comma-separated list of addresses to receive copies of the invitation. Recipients will not receive the addresses of other recipients.",
|
||||
)}
|
||||
>
|
||||
</ak-textarea-input>
|
||||
<ak-form-element-horizontal label=${msg("Template")} required name="template">
|
||||
<select class="pf-c-form-control">
|
||||
${this.availableTemplates?.map((template) => {
|
||||
return html`<option
|
||||
value=${template.name}
|
||||
?selected=${template.name === this.selectedTemplate}
|
||||
>
|
||||
${template.description}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Select the email template to use for sending invitations.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-invitation-send-email-form": InvitationSendEmailForm;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -10189,9 +10189,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10321,6 +10318,32 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10229,9 +10229,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10361,6 +10358,32 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -8070,9 +8070,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -8202,6 +8199,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10149,9 +10149,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10281,6 +10278,32 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10405,9 +10405,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10537,6 +10534,32 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10394,9 +10394,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10526,6 +10523,32 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10097,9 +10097,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10229,6 +10226,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10385,9 +10385,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10517,6 +10514,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9729,9 +9729,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -9861,6 +9858,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9387,9 +9387,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -9519,6 +9516,32 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9757,9 +9757,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -9889,6 +9886,32 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10378,9 +10378,6 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10510,6 +10507,32 @@ por exemplo: <x id="0" equiv-text="<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>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9845,9 +9845,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -9977,6 +9974,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9824,9 +9824,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -9956,6 +9953,32 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10685,9 +10685,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -10817,6 +10814,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -9443,9 +9443,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s20c950d20c8aec84">
|
||||
<source>Min reviewers is per-group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se4f63fe867f1714a">
|
||||
<source>If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s74906f78877111cb">
|
||||
<source>Reviewers</source>
|
||||
</trans-unit>
|
||||
@@ -9575,6 +9572,32 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s804ab712b1add3d3">
|
||||
<source>An unknown error occurred while submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78adb00480380ad9">
|
||||
<source>Sign logout response</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7faa5610b9493a92">
|
||||
<source>When enabled, SAML logout responses will be signed.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa088a2d0f90d5a9">
|
||||
<source>Posting logout response to SAML provider: <x id="0" equiv-text="${providerName}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h903c2b1c21bb9fc7">
|
||||
<source>If checked, approving a review will require at least that many users from
|
||||
<x id="0" equiv-text="<em>"/>each<x id="1" equiv-text="</em>"/> of the selected groups. When disabled, the value is a total
|
||||
across all groups.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s776d93092ad9ce90">
|
||||
<source>Review initiated</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad46bcad1a343b51">
|
||||
<source>Review overdue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s59d168d966cecbaf">
|
||||
<source>Review attested</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5158b7f014cecefc">
|
||||
<source>Review completed</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user