mirror of
https://github.com/goauthentik/authentik
synced 2026-05-13 10:26:43 +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 |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -58,7 +58,7 @@ runs:
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/compose.yml up -d
|
||||
cd web && npm ci
|
||||
cd web && npm i
|
||||
- name: Generate config
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: uv run python {0}
|
||||
|
||||
@@ -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
|
||||
|
||||
4
.github/workflows/release-publish.yml
vendored
4
.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
|
||||
|
||||
8
Makefile
8
Makefile
@@ -148,11 +148,11 @@ bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||
ifndef version
|
||||
$(error Usage: make bump version=20xx.xx.xx )
|
||||
endif
|
||||
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
$(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
|
||||
npm version --no-git-tag-version --allow-same-version $(version)
|
||||
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
|
||||
echo -n $(version) > ${PWD}/internal/constants/VERSION
|
||||
|
||||
#########################
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.2.0-rc4"
|
||||
VERSION = "2026.5.0-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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-rc4 Blueprint schema",
|
||||
"title": "authentik 2026.5.0-rc1 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@@ -10759,6 +10759,10 @@
|
||||
"type": "boolean",
|
||||
"title": "Sign logout request"
|
||||
},
|
||||
"sign_logout_response": {
|
||||
"type": "boolean",
|
||||
"title": "Sign logout response"
|
||||
},
|
||||
"sp_binding": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
2
go.mod
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-rc4
|
||||
2026.5.0-rc1
|
||||
@@ -18,7 +18,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2026.2.0-rc4
|
||||
Default: 2026.5.0-rc1
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.0-rc1}
|
||||
ports:
|
||||
- ${COMPOSE_PORT_HTTP:-9000}:9000
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.0-rc1}
|
||||
restart: unless-stopped
|
||||
shm_size: 512mb
|
||||
user: root
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc4",
|
||||
"version": "2026.5.0-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc4",
|
||||
"version": "2026.5.0-rc1",
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@goauthentik/eslint-config": "./packages/eslint-config",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc4",
|
||||
"version": "2026.5.0-rc1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2026.2.0-rc4"
|
||||
version = "2026.5.0-rc1"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.14.*"
|
||||
dependencies = [
|
||||
"ak-guardian==3.2.0",
|
||||
"argon2-cffi==25.1.0",
|
||||
"cachetools==7.0.0",
|
||||
"cachetools==7.0.1",
|
||||
"channels==4.3.2",
|
||||
"cryptography==46.0.5",
|
||||
"dacite==1.9.2",
|
||||
@@ -15,7 +15,6 @@ dependencies = [
|
||||
"defusedxml==0.7.1",
|
||||
"django-channels-postgres",
|
||||
"django-countries==7.6.1",
|
||||
"django-cte==3.0.0",
|
||||
"django-dramatiq-postgres",
|
||||
"django-filter==25.2",
|
||||
"django-model-utils==5.0.0",
|
||||
@@ -37,7 +36,7 @@ dependencies = [
|
||||
"fido2==2.1.1",
|
||||
"geoip2==5.2.0",
|
||||
"geopy==2.4.1",
|
||||
"google-api-python-client==2.189.0",
|
||||
"google-api-python-client==2.190.0",
|
||||
"gssapi==1.11.1",
|
||||
"gunicorn==25.0.3",
|
||||
"jsonpatch==1.33",
|
||||
@@ -69,7 +68,7 @@ dependencies = [
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.40.0",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.7.0",
|
||||
"webauthn==2.7.1",
|
||||
"wsproto==1.3.2",
|
||||
"xmlsec==1.3.17",
|
||||
"zxcvbn==4.5.0",
|
||||
@@ -103,7 +102,7 @@ dev = [
|
||||
"pytest-timeout==2.4.0",
|
||||
"pytest==9.0.2",
|
||||
"requests-mock==1.12.1",
|
||||
"ruff==0.15.0",
|
||||
"ruff==0.15.1",
|
||||
"selenium==4.40.0",
|
||||
"types-channels==4.3.0.20250822",
|
||||
"types-docker==7.1.0.20260109",
|
||||
|
||||
112
schema.yml
112
schema.yml
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2026.2.0-rc4
|
||||
version: 2026.5.0-rc1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@@ -18677,6 +18677,10 @@ paths:
|
||||
name: sign_logout_request
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_logout_response
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_response
|
||||
schema:
|
||||
@@ -19863,6 +19867,10 @@ paths:
|
||||
name: sign_logout_request
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_logout_response
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_response
|
||||
schema:
|
||||
@@ -30597,6 +30605,35 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/invitation/invitations/{invite_uuid}/send_email/:
|
||||
post:
|
||||
operationId: stages_invitation_invitations_send_email_create
|
||||
description: Send invitation link via email to one or more addresses
|
||||
parameters:
|
||||
- in: path
|
||||
name: invite_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Invitation.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InvitationSendEmailRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/invitation/invitations/{invite_uuid}/used_by/:
|
||||
get:
|
||||
operationId: stages_invitation_invitations_used_by_list
|
||||
@@ -40696,8 +40733,7 @@ components:
|
||||
logout_urls:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
$ref: '#/components/schemas/LogoutURL'
|
||||
IframeLogoutChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response for iframe logout
|
||||
@@ -40845,6 +40881,25 @@ components:
|
||||
description: When set, only the configured flow can use this invitation.
|
||||
required:
|
||||
- name
|
||||
InvitationSendEmailRequest:
|
||||
type: object
|
||||
description: Serializer for sending invitation emails
|
||||
properties:
|
||||
email_addresses:
|
||||
type: array
|
||||
items: {}
|
||||
cc_addresses:
|
||||
type: array
|
||||
items: {}
|
||||
bcc_addresses:
|
||||
type: array
|
||||
items: {}
|
||||
template:
|
||||
type: string
|
||||
minLength: 1
|
||||
default: invitation
|
||||
required:
|
||||
- email_addresses
|
||||
InvitationStage:
|
||||
type: object
|
||||
description: InvitationStage Serializer
|
||||
@@ -42393,6 +42448,29 @@ components:
|
||||
required:
|
||||
- challenge
|
||||
- name
|
||||
LogoutURL:
|
||||
type: object
|
||||
description: Data for a single logout URL
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
nullable: true
|
||||
binding:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_request:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_response:
|
||||
type: string
|
||||
nullable: true
|
||||
saml_relay_state:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- url
|
||||
MDMConfigRequest:
|
||||
type: object
|
||||
description: Base serializer class which doesn't implement create/update methods
|
||||
@@ -42956,21 +43034,23 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
post_url:
|
||||
type: string
|
||||
saml_request:
|
||||
type: string
|
||||
relay_state:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
binding:
|
||||
type: string
|
||||
redirect_url:
|
||||
type: string
|
||||
is_complete:
|
||||
type: boolean
|
||||
default: false
|
||||
post_url:
|
||||
type: string
|
||||
redirect_url:
|
||||
type: string
|
||||
saml_binding:
|
||||
$ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
saml_request:
|
||||
type: string
|
||||
saml_response:
|
||||
type: string
|
||||
saml_relay_state:
|
||||
type: string
|
||||
NativeLogoutChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response for native browser logout
|
||||
@@ -49905,6 +49985,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
@@ -53397,6 +53479,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
@@ -53591,6 +53675,8 @@ components:
|
||||
type: boolean
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
sign_logout_response:
|
||||
type: boolean
|
||||
sp_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SAMLBindingsEnum'
|
||||
|
||||
91
uv.lock
generated
91
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.0rc4"
|
||||
version = "2026.5.0rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "ak-guardian" },
|
||||
@@ -235,7 +226,6 @@ dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-channels-postgres" },
|
||||
{ name = "django-countries" },
|
||||
{ name = "django-cte" },
|
||||
{ name = "django-dramatiq-postgres" },
|
||||
{ name = "django-filter" },
|
||||
{ name = "django-model-utils" },
|
||||
@@ -336,7 +326,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "ak-guardian", editable = "packages/ak-guardian" },
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.1" },
|
||||
{ name = "channels", specifier = "==4.3.2" },
|
||||
{ name = "cryptography", specifier = "==46.0.5" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
@@ -345,7 +335,6 @@ requires-dist = [
|
||||
{ name = "django", specifier = "==5.2.11" },
|
||||
{ name = "django-channels-postgres", editable = "packages/django-channels-postgres" },
|
||||
{ name = "django-countries", specifier = "==7.6.1" },
|
||||
{ name = "django-cte", specifier = "==3.0.0" },
|
||||
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
|
||||
{ name = "django-filter", specifier = "==25.2" },
|
||||
{ name = "django-model-utils", specifier = "==5.0.0" },
|
||||
@@ -366,7 +355,7 @@ requires-dist = [
|
||||
{ name = "fido2", specifier = "==2.1.1" },
|
||||
{ name = "geoip2", specifier = "==5.2.0" },
|
||||
{ name = "geopy", specifier = "==2.4.1" },
|
||||
{ name = "google-api-python-client", specifier = "==2.189.0" },
|
||||
{ name = "google-api-python-client", specifier = "==2.190.0" },
|
||||
{ name = "gssapi", specifier = "==1.11.1" },
|
||||
{ name = "gunicorn", specifier = "==25.0.3" },
|
||||
{ name = "jsonpatch", specifier = "==1.33" },
|
||||
@@ -398,7 +387,7 @@ requires-dist = [
|
||||
{ name = "urllib3", specifier = "<3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.40.0" },
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
{ name = "webauthn", specifier = "==2.7.0" },
|
||||
{ name = "webauthn", specifier = "==2.7.1" },
|
||||
{ name = "wsproto", specifier = "==1.3.2" },
|
||||
{ name = "xmlsec", specifier = "==1.3.17" },
|
||||
{ name = "zxcvbn", specifier = "==4.5.0" },
|
||||
@@ -432,7 +421,7 @@ dev = [
|
||||
{ name = "pytest-randomly", specifier = "==4.0.1" },
|
||||
{ name = "pytest-timeout", specifier = "==2.4.0" },
|
||||
{ name = "requests-mock", specifier = "==1.12.1" },
|
||||
{ name = "ruff", specifier = "==0.15.0" },
|
||||
{ name = "ruff", specifier = "==0.15.1" },
|
||||
{ name = "selenium", specifier = "==4.40.0" },
|
||||
{ name = "types-channels", specifier = "==4.3.0.20250822" },
|
||||
{ name = "types-docker", specifier = "==7.1.0.20260109" },
|
||||
@@ -710,11 +699,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "7.0.0"
|
||||
version = "7.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1138,18 +1127,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/46/b6931858e5161e5d9166bfcfde3af0b7d60ba89e4f7dd8f033e591c68794/django_countries-7.6.1-py3-none-any.whl", hash = "sha256:1ed20842fe0f6194f91faca21076649513846a8787c9eb5aeec3cbe1656b8acc", size = 864507, upload-time = "2024-04-01T21:01:05.702Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-cte"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/c0/64cda7c7b3e5641160a4c9dd1030b3a567592ba2b6c64f5303678a780084/django_cte-3.0.0.tar.gz", hash = "sha256:888710bb7109559621a34ab890f0f87d54188c9678f874e61e82112b59bbccb4", size = 11422, upload-time = "2026-02-05T13:08:53.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8a/5fdf282b8496e485b007d48ac211a0e2b203b8623e67c32ed2cf65faedc4/django_cte-3.0.0-py3-none-any.whl", hash = "sha256:3eabb89b68d328a6a97c695a21f45ed6ee7a6602193670db74e70c2e17bf2cd5", size = 13211, upload-time = "2026-02-05T13:08:51.908Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-dramatiq-postgres"
|
||||
version = "0.1.0"
|
||||
@@ -1655,7 +1632,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.189.0"
|
||||
version = "2.190.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
@@ -1664,9 +1641,9 @@ dependencies = [
|
||||
{ name = "httplib2" },
|
||||
{ name = "uritemplate" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/f8/0783aeca3410ee053d4dd1fccafd85197847b8f84dd038e036634605d083/google_api_python_client-2.189.0.tar.gz", hash = "sha256:45f2d8559b5c895dde6ad3fb33de025f5cb2c197fa5862f18df7f5295a172741", size = 13979470, upload-time = "2026-02-03T19:24:55.432Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/4ab3e3516b93bb50ed7814738ea61d49cba3f72f4e331dc9518ae2731e92/google_api_python_client-2.190.0.tar.gz", hash = "sha256:5357f34552e3724d80d2604c8fa146766e0a9d6bb0afada886fafed9feafeef6", size = 14111143, upload-time = "2026-02-12T00:38:03.37Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl", hash = "sha256:a258c09660a49c6159173f8bbece171278e917e104a11f0640b34751b79c8a1a", size = 14547633, upload-time = "2026-02-03T19:24:52.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ad/223d5f4b0b987669ffeb3eadd7e9f85ece633aa7fd3246f1e2f6238e1e05/google_api_python_client-2.190.0-py3-none-any.whl", hash = "sha256:d9b5266758f96c39b8c21d9bbfeb4e58c14dbfba3c931f7c5a8d7fdcd292dd57", size = 14682070, upload-time = "2026-02-12T00:38:00.974Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3308,27 +3285,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4003,17 +3980,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "webauthn"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asn1crypto" },
|
||||
{ name = "cbor2" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "pyopenssl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/f0/e1036df8842782a2947e5f41e76a4accb92e3dba972dba882321ebe15af0/webauthn-2.7.0.tar.gz", hash = "sha256:3c45c25e75a7d7d419220ccd10b8b899984de8012732e10d898f0a8f8c480575", size = 123770, upload-time = "2025-09-04T23:19:21.602Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/f4/9529bcf85ef46c76842b84c66ffa8ec31f18e3aacd1330b62f440077b45b/webauthn-2.7.1.tar.gz", hash = "sha256:2a1ebbfffc4a83e31d3db5d69113944bc49d05fae77770c2d4e388386cb9656e", size = 124256, upload-time = "2026-02-11T23:36:02.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b1/3f380d02552f1d75d3db789f761a1ee0dafd6181ebc07dd4b9ded61225a4/webauthn-2.7.0-py3-none-any.whl", hash = "sha256:2ecfee7959b09ebeaaffee9f8982ecdbbdc369a11766d20d4bc0637b36e235b7", size = 71311, upload-time = "2025-09-04T23:19:20.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/5b/f73513367a9d34b199de916b44306acfa4027b57f7e22200421212b1f763/webauthn-2.7.1-py3-none-any.whl", hash = "sha256:d57e9613c65e0c6a4db7ee715fb49ebdf3c4a6eb3979729eeb497c99105e8181", size = 71684, upload-time = "2026-02-11T23:36:00.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
77
web/package-lock.json
generated
77
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc4",
|
||||
"version": "2026.5.0-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc4",
|
||||
"version": "2026.5.0-rc1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
@@ -188,6 +188,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2609,7 +2610,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
@@ -3879,18 +3879,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": {
|
||||
"version": "0.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
|
||||
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-reference": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.1.tgz",
|
||||
@@ -4029,6 +4017,7 @@
|
||||
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.25"
|
||||
@@ -4358,8 +4347,7 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
@@ -4734,6 +4722,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz",
|
||||
"integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -4752,6 +4741,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
|
||||
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -4841,6 +4831,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -5081,6 +5072,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz",
|
||||
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -5546,6 +5538,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6088,6 +6081,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -6376,6 +6370,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -6407,6 +6402,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
@@ -6677,6 +6673,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -7071,6 +7068,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -7231,6 +7229,7 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@@ -7498,7 +7497,6 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz",
|
||||
"integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
@@ -7511,7 +7509,6 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
|
||||
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
@@ -7562,8 +7559,7 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
@@ -7858,6 +7854,7 @@
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -7951,6 +7948,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9242,7 +9240,6 @@
|
||||
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz",
|
||||
"integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
|
||||
}
|
||||
@@ -10852,6 +10849,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
|
||||
"integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-element": "^4.2.0",
|
||||
@@ -11114,7 +11112,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -13558,6 +13555,7 @@
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
@@ -13659,6 +13657,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -13693,7 +13692,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -13708,7 +13706,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -13720,8 +13717,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
@@ -13929,6 +13925,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
|
||||
"integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ramda"
|
||||
@@ -14008,6 +14005,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14017,6 +14015,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -14553,6 +14552,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -15153,15 +15153,13 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.0.1.tgz",
|
||||
"integrity": "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sort-package-json": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.5.0.tgz",
|
||||
"integrity": "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-indent": "^7.0.1",
|
||||
"detect-newline": "^4.0.1",
|
||||
@@ -15296,6 +15294,7 @@
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.7.tgz",
|
||||
"integrity": "sha512-LFKSuZyF6EW2/Kkl5d7CvqgwhXXfuWv+aLBuoc616boLKJ3mxXuea+GxIgfk02NEyTKctJ0QsnSh5pAomf6Qkg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^2.0.1",
|
||||
@@ -15696,7 +15695,6 @@
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
@@ -15902,18 +15900,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
|
||||
"integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-json": {
|
||||
"version": "0.24.8",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz",
|
||||
@@ -16156,6 +16142,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16169,6 +16156,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
|
||||
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
@@ -16587,6 +16575,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16675,6 +16664,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -17367,6 +17357,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc4",
|
||||
"version": "2026.5.0-rc1",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -421,7 +421,7 @@ export class IdentificationStage extends BaseStage<
|
||||
? html`
|
||||
<p>
|
||||
${msg(
|
||||
"Enter the email address or username associated with your account.",
|
||||
"Enter the email associated with your account, and we'll send you a link to reset your password.",
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -31,7 +31,7 @@ Keys prefixed with `goauthentik.io` are used internally by authentik and are sub
|
||||
|
||||
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../../stages/identification/index.mdx).
|
||||
|
||||
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx), and others will use this value if it is set, and fall back to the request's user when possible.
|
||||
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx) and others will use this value if it is set, and fallback to the request's users when possible.
|
||||
|
||||
#### `prompt_data` (Dictionary)
|
||||
|
||||
@@ -55,6 +55,8 @@ Stores the final redirect URL that the user's browser will be sent to after the
|
||||
|
||||
If _Show matched user_ is disabled, this key will hold the user identifier entered by the user in the identification stage.
|
||||
|
||||
Stores the final redirect URL that the user's browser will be sent to after the flow is finished executing successfully. This is set when an un-authenticated user attempts to access a secured application, and when a user authenticates/enrolls with an external source.
|
||||
|
||||
#### `application` (Application object)
|
||||
|
||||
When an unauthenticated user attempts to access a secured resource, they are redirected to an authentication flow. The application they attempted to access will be stored in the key attached to this object. For example: `application.github`, with `application` being the key and `github` the value.
|
||||
@@ -149,7 +151,7 @@ Type the `pending_user` will be created as. Must be one of `internal`, `external
|
||||
|
||||
##### `user_backend` (string)
|
||||
|
||||
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
|
||||
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating in the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
|
||||
|
||||
##### `auth_method` (string)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
title: Default flows
|
||||
---
|
||||
|
||||
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that overrides the defaults configured on the brand.
|
||||
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that override the defaults configured on the brand.
|
||||
|
||||
If no default flow is selected when the provider is created, authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands/index.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug`, evaluate policies bound directly to the flows, and pick the first flow whose policies allow access.
|
||||
If no default flow is selected when the provider is created, to determine which flow should be used authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug` and evaluate policies bound directly to the flows, and the first flow whose policies allow access will be picked.
|
||||
|
||||
import DefaultFlowList from "../../flow/flow_list/\_defaultflowlist.mdx";
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ The user interface (/if/user/) uses a specialized flow executor to allow individ
|
||||
|
||||
Because the stages in a flow can change during its execution, be aware that configuring this executor to use any stage type other than Prompt or User Write will automatically trigger a redirect to the standard executor.
|
||||
|
||||
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands/index.md) on the same authentik instance.
|
||||
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands.md) on the same authentik instance.
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Flows
|
||||
|
||||
Flows are a major component in authentik. In conjunction with stages and [policies](../../../customize/policies/index.md), flows are at the heart of our system of building blocks, used to define and execute the workflows of authentication, authorization, enrollment, and user settings.
|
||||
|
||||
There are over a dozen default, out-of-the-box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
|
||||
There are over a dozen default, out-of-the box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
|
||||
|
||||
A flow is a method of describing a sequence of stages. A stage represents a single verification or logic step. By connecting a series of stages within a flow (and optionally attaching policies as needed) you can build a highly flexible process for authenticating users, enrolling them, and more.
|
||||
|
||||
@@ -54,7 +54,7 @@ To create a flow, follow these steps:
|
||||
|
||||
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
|
||||
|
||||
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands/index.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
|
||||
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
|
||||
|
||||
## Flow configuration options
|
||||
|
||||
@@ -78,9 +78,9 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
|
||||
|
||||
**Behavior settings**:
|
||||
|
||||
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled when accessing the flow from a desktop browser. However, accessing the flow from a mobile device might necessitate this setting to be enabled.
|
||||
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled, when accessing the flow from a desktop browser. However accessing the flow from a mobile device might necessitate this setting to be enabled.
|
||||
|
||||
The technical reason for this setting's existence is the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
|
||||
The technical reasons for this settings' existence is due to the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
|
||||
|
||||
When the compatibility mode is enabled, authentik uses a polyfill which emulates the Shadow DOM APIs without actually using the feature, and instead a traditional DOM is rendered. This increases support for password managers, especially on mobile devices.
|
||||
|
||||
@@ -95,7 +95,7 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
|
||||
|
||||
- **Layout**: select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.
|
||||
|
||||
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands/index.md#branding-settings).
|
||||
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands.md#branding-settings).
|
||||
|
||||
## Edit or delete a flow
|
||||
|
||||
|
||||
@@ -80,4 +80,4 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
4. Click **Finish**.
|
||||
|
||||
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the user's browser.
|
||||
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the users' browser.
|
||||
|
||||
@@ -66,7 +66,7 @@ return {
|
||||
|
||||
## Verify only
|
||||
|
||||
To only verify the validity of a user's phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
|
||||
To only verify the validity of a users' phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
|
||||
|
||||
## Limiting phone numbers
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Keep in mind that when using Code-based devices (TOTP, Static and SMS), values l
|
||||
|
||||
#### Less-frequent validation
|
||||
|
||||
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the user's devices within the selected classes are checked.
|
||||
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the users devices within the selected classes are checked.
|
||||
|
||||
#### Passwordless authentication
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ The Mutual TLS stage enables authentik to use client certificates to enroll and
|
||||
|
||||
For mTLS, note that you should NOT use a globally known CA.
|
||||
|
||||
Using private PKI certificates that are trusted by the end-device is best practise. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via mTLS, and in addition you should implement [custom validation](../../flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
|
||||
Using private PKI certificates that are trusted by the end-device is best practice. For example, using a Verisign certificate as a "known CA" means that ANYONE who has a certificate signed by them can authenticate via mTLS, and in addition you should implement [custom validation](../../flow/context/index.mdx#auth_method-string) to prevent unauthorized access.
|
||||
:::
|
||||
|
||||
## Reverse-proxy configuration
|
||||
@@ -97,7 +97,7 @@ See the [Envoy mTLS documentation](https://www.envoyproxy.io/docs/envoy/latest/s
|
||||
|
||||
#### No reverse proxy
|
||||
|
||||
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands/index.md#client-certificates) for the domain, under **Other global settings**.
|
||||
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands.md#client-certificates) for the domain, under **Other global settings**.
|
||||
|
||||
## Stage configuration
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is a generic password prompt which authenticates the current `pending_user`
|
||||
|
||||
## Passwordless login
|
||||
|
||||
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the user's device, which is documented here.
|
||||
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the users device, which is documented here.
|
||||
|
||||
If you want users to be able to pick a passkey from the browser's passkey/autofill UI without entering a username first, configure **Passkey autofill (WebAuthn conditional UI)** in the [Identification stage](../identification/index.mdx#passkey-autofill-webauthn-conditional-ui). This is separate from configuring a dedicated passwordless flow, and can be used alongside normal identification flows.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ The device code flow is also known as _device flow_ or _device authorization gra
|
||||
|
||||
### Requirements
|
||||
|
||||
This device flow is only possible if the active [brand](../../../sys-mgmt/brands/index.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
|
||||
This device flow is only possible if the active [brand](../../../sys-mgmt/brands.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
|
||||
|
||||
authentik does not include a default flow for this use case, so it is necessary to create a new one with a **Designation** of `Stage Configuration`.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ OAuth 2.0 is an authorization protocol that allows an application (the RP) to de
|
||||
1. An authorization request is prepared by the RP and contains parameters for its implementation of OAuth and which data it requires, and then the User's browser is redirected to that URL.
|
||||
2. The RP sends a request to authentik in the background to exchange the access code for an access token (and optionally a refresh token).
|
||||
|
||||
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the user's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorized (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
|
||||
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorized (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
|
||||
|
||||
The image below shows a typical authorization code flow.
|
||||
|
||||
@@ -102,7 +102,7 @@ The flows and grant types used in this case are those used for a typical authori
|
||||
|
||||
The authorization code is for environments with both a Client and a application server, where the back and forth happens between the client and an app server (the logic lives on app server). The RP needs to authorise itself to the OP. Client ID (public, identifies which app is talking to it) and client secret (the password) that the RP uses to authenticate.
|
||||
|
||||
If you configure authentik to use "Offline access" then during the initial auth the OP sends two tokens, an access token (short-lived, hours, can be customised) and a refresh token (typically longer validity, days or infinite). The RP (the app) saves both tokens. When the access token is about to expire, the RP sends the saved refresh token back to the OP, and requests a new access token. When the refresh token itself is about to expire, the RP can also ask for a new refresh token. This can all happen without user interaction if you configured the offline access.
|
||||
If you configure authentik to use "Offline access" then during the initial auth the OP sends two tokens, an access token (short-lived, hours, can be customized) and a refresh token (typically longer validity, days or infinite). The RP (the app) saves both tokens. When the access token is about to expire, the RP sends the saved refresh token back to the OP, and requests a new access token. When the refresh token itself is about to expire, the RP can also ask for a new refresh token. This can all happen without user interaction if you configured the offline access.
|
||||
|
||||
:::info
|
||||
Starting with authentik 2024.2, applications only receive an access token. To receive a refresh token, both applications and authentik must be configured to request the `offline_access` scope. In authentik this can be done by selecting the `offline_access` Scope mapping in the provider settings.
|
||||
|
||||
@@ -8,6 +8,6 @@ The [WebFinger protocol](https://webfinger.net/) allows for the discovery of inf
|
||||
|
||||
## authentik WebFinger support
|
||||
|
||||
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands/index.md#external-user-settings).
|
||||
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands.md#external-user-settings).
|
||||
|
||||
The WebFinger endpoint is available at: `https://authentik.company/.well-known/webfinger` (where authentik.company is the FQDN of your authentik instance)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
title: Create a Remote Access Control (RAC) provider
|
||||
---
|
||||
|
||||
For an overview of Remote Access Control (RAC), see the [RAC provider](./index.md) documentation.
|
||||
|
||||
You can also watch our video on YouTube for setting up RAC:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
|
||||
|
||||
## Workflow to create an RAC provider
|
||||
|
||||
Follow this workflow to create and configure an RAC provider:
|
||||
|
||||
1. Create a RAC provider and application pair.
|
||||
2. Create RAC property mappings (that define the access credentials to each remote machine).
|
||||
3. Create endpoints for each remote machine you want to connect to.
|
||||
4. Create an RAC outpost to service the provider.
|
||||
|
||||
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
|
||||
|
||||
### Create a RAC provider and application pair
|
||||
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with Provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to create only the provider by navigating to **Applications** > **Providers** and clicking **Create**.
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application details, and then click **Next**.
|
||||
4. Select the **RAC** provider type, and then click **Next**.
|
||||
5. On the **Configure Remote Access Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
|
||||
|
||||
### Create RAC property mappings
|
||||
|
||||
Next, you need to add property mappings for each remote machine you want to access. RAC property mappings can be used to pass the access credentials and connection settings of the remote machine.
|
||||
|
||||
Refer to the [RAC Credentials Prompt](./rac_credentials_prompt.md) and [RAC SSH Public Key Authentication](./rac-public-key.md) documentation for alternative methods of handling RAC authentication.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings**, and click **Create**.
|
||||
3. Select **RAC Provider Property Mapping** as the property mapping type, and then click **Next**.
|
||||
4. On the **Create RAC Provider Property Mapping** page, provide the following configuration settings:
|
||||
- **Name**: provide a name for the property mapping
|
||||
- Under **General settings**:
|
||||
- **Username**: the username for the remote machine
|
||||
- **Password**: the password for the remote machine
|
||||
- Under **Advanced settings**:
|
||||
- **Expression _(optional)_**: define other connection settings to be used, such as an SSH key. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation.
|
||||
|
||||
5. Click **Finish**.
|
||||
|
||||
### Create endpoints for the provider
|
||||
|
||||
Then, you need to create an endpoint corresponding to each remote machine you want to connect to. Endpoints define the IP address, port, protocol, and other settings used for connecting to a remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the **Edit** button on the RAC provider that you previously created.
|
||||
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
|
||||
- **Provider Name** (endpoint name): define a name for the endpoint
|
||||
- **Protocol**: select the appropriate protocol
|
||||
- **Host**: enter the host name or IP address of the remote machine. Optionally include the port.
|
||||
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation
|
||||
- **Property mappings**: select either the property mapping that you previously created, or use one of the default RAC property mappings
|
||||
- **Advanced settings _(optional)_**: define other connection settings to be used. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation
|
||||
|
||||
5. Click **Create**.
|
||||
|
||||
### Create an RAC outpost
|
||||
|
||||
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Outposts**.
|
||||
3. Click **Create** and set the following values:
|
||||
- **Name**: define a name for the outpost.
|
||||
- **Type**: `RAC`
|
||||
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
|
||||
- **Applications**: select the RAC application that you previously created.
|
||||
- **Advanced settings _(optional)_**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
|
||||
|
||||
4. Click **Create** to save your new outpost.
|
||||
|
||||
## Access the remote machine
|
||||
|
||||
To verify your configuration and access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page, click the **Remote Access** application to start a secure session on the remote machine in your web browser.
|
||||
|
||||
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.
|
||||
88
website/docs/add-secure-apps/providers/rac/how-to-rac.md
Normal file
88
website/docs/add-secure-apps/providers/rac/how-to-rac.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Create a Remote Access Control (RAC) provider
|
||||
---
|
||||
|
||||
The Remote Access Control (RAC) provider is a highly flexible feature for accessing remote machines.
|
||||
|
||||
For overview information, see the [RAC provider](./index.md) documentation. You can also view our video on YouTube for setting up RAC.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
|
||||
|
||||
## Overview workflow to create an RAC provider
|
||||
|
||||
The typical workflow to create and configure a RAC provider is:
|
||||
|
||||
1. Create an application and provider.
|
||||
2. Create property mappings (that define the access credentials to each remote machine).
|
||||
3. Create an endpoint for each remote machine you want to connect to.
|
||||
4. Create an RAC outpost to service the provider.
|
||||
|
||||
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
|
||||
|
||||
### Create an application and RAC provider
|
||||
|
||||
The first step is to create the RAC application and provider pair.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with provider**.
|
||||
3. Follow these [instructions](../../applications/manage_apps.mdx#create-an-application-and-provider-pair) to create your RAC application and provider.
|
||||
|
||||
### Create RAC property mappings
|
||||
|
||||
Next, you need to add property mappings for each remote machine you want to access. Property mappings allow you to pass information to external applications, and with RAC they are used to pass the host name, IP address, and access credentials of the remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
- **Select Type**: `RAC Provider Property Mapping`
|
||||
- **Create RAC Property Mapping**:
|
||||
- **Name**s: define a name for the property mapping, perhaps include the type of connection (RDP, SSH, VNC)
|
||||
- **General settings**:
|
||||
- **Username**: the username for the remote machine
|
||||
- **Password**: the password for the remote machine
|
||||
- **RDP settings**:
|
||||
- **Ignore server certificate**: select **Enabled** (Depending on the setup of your RDP Server, it might be required to enable this setting.)
|
||||
- **Enable wallpaper**: optional
|
||||
- **Enable font smoothing**: optional
|
||||
- **Enable full window dragging**: optional
|
||||
- Advanced settings:
|
||||
- **Expressions**: optional, using Python you can define custom [expressions](../property-mappings/expression.mdx).
|
||||
|
||||
3. Click **Finish**.
|
||||
|
||||
### Create endpoints for the provider
|
||||
|
||||
Then, you need to create an endpoint for each remote machine. Endpoints are defined within providers; connections between the remote machine and authentik are enabled through communication between the provider's endpoint and the remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the **Edit** button on the RAC provider that you previously created.
|
||||
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
|
||||
- **Name**: define a name for the endpoint, perhaps include the type of connection (RDP, SSH, VNC).
|
||||
- **Protocol**: select the appropriate protocol.
|
||||
- **Host**: enter the host name or IP address of the remote machine.
|
||||
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation.
|
||||
- **Property mapping**: select either the property mapping that you previously created, or use one of the default settings.
|
||||
- **Advance settings**: (_optional_)
|
||||
|
||||
5. Click **Create**.
|
||||
|
||||
### Create an RAC outpost
|
||||
|
||||
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Outposts**.
|
||||
3. Click **Create** and set the following values:
|
||||
- **Name**: define a name for the outpost.
|
||||
- **Type**: `RAC`
|
||||
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
|
||||
- **Applications**: select the RAC application that you previously created.
|
||||
- **Advanced settings (optional)**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
|
||||
|
||||
4. Click Create to save your new outpost.
|
||||
|
||||
## Access the remote machine
|
||||
|
||||
To verify your configuration and then access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page click the **Remote Access** application and authentik then connects you to a secure session on the remote machine, in your web browser.
|
||||
|
||||
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.
|
||||
@@ -2,85 +2,67 @@
|
||||
title: Remote Access Control (RAC) Provider
|
||||
---
|
||||
|
||||
:::info
|
||||
This provider requires the deployment of the [RAC Outpost](../../outposts/index.mdx).
|
||||
:::
|
||||
|
||||
## About the Remote Access Control (RAC) Provider
|
||||
|
||||
The RAC provider allows users to access remote Windows, macOS, and Linux machines via [RDP](https://en.wikipedia.org/wiki/Remote_Desktop_Protocol)/[SSH](https://en.wikipedia.org/wiki/Secure_Shell)/[VNC](https://en.wikipedia.org/wiki/Virtual_Network_Computing). Just like other providers in authentik, the RAC provider is associated with an application that appears on a user's **My applications** page.
|
||||
|
||||
For instructions on creating a RAC provider, refer to the [Create a Remote Access Control (RAC) provider](./create-rac-provider.md) documentation. Alternatively, watch our ["Remote Access Control (RAC) in authentik" video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts).
|
||||
:::info
|
||||
Note that with RAC, you create a single application and associated provider that serves to connect with _all remote machines_ that you want to configure for access via RAC.
|
||||
:::
|
||||
|
||||
## RAC components
|
||||
For instructions on creating a RAC provider, refer to the [Managing RAC providers](./how-to-rac.md) documentation. You can also view our [video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts) for setting up a RAC.
|
||||
|
||||
A RAC provider uses several components:
|
||||
For an example of how to configure RAC connections settings, refer to the [RAC SSH Public Key Authentication](./rac-public-key.md) documentation.
|
||||
|
||||
```mermaid
|
||||
architecture-beta
|
||||
service application(mdi:application-outline)[Application]
|
||||
service provider(mdi:application-cog-outline)[Provider]
|
||||
service endpoint(mdi:network-pos)[Endpoint Settings]
|
||||
service server(mdi:server)[authentik Server]
|
||||
service outpost(mdi:server-plus)[RAC Outpost]
|
||||
There are several components used with a RAC provider; let's take a closer look at the high-level configuration layout of these components and how they are managed using endpoints and connections.
|
||||
|
||||
service machine(mdi:desktop-classic)[Remote Machine]
|
||||

|
||||
|
||||
application:R --> L:provider
|
||||
provider:B -- T:endpoint
|
||||
provider:R --> L:server
|
||||
server:R <--> L:outpost
|
||||
outpost:B <--> T:machine
|
||||
```
|
||||
The provider-application pair, the authentik server, and the authentik API are typical to all configurations. With RAC, there are some new components, namely the endpoints, the outpost, and of course the target remote machines.
|
||||
|
||||
When a user starts the RAC application, it communicates with the authentik server, which then connects to the RAC outpost and sends instructions (based on the endpoint data you defined) on how to connect to the remote machine.
|
||||
When a user starts the RAC application, the app communicates with the authentik server, which then connects to an instance of the outpost (the exact instance is selected dynamically based on connection load). After the outpost is selected, then the authentik server sends the outpost the instructions (based on the data you defined in the endpoint) required to connect to the remote machine.
|
||||
|
||||
After connecting to the remote machine, the outpost sends a message back to the authentik server (via WebSockets), and the web browser opens the WebSocket connection to the remote machine.
|
||||
After the connection to the remote machine is made, the outpost sends a message back to the authentik server (via websockets), and the web browser opens the websocket connection to the remote machine.
|
||||
|
||||
## Endpoints
|
||||
### Endpoints
|
||||
|
||||
Unlike other providers, where an application-provider pair is created for each resource you wish to access, RAC works differently. RAC uses a single application connected to one RAC provider. The RAC provider then has an _Endpoint_ object for each remote machine (computer/server) you want to connect to.
|
||||
Unlike other providers, where one provider-application pair must be created for each resource you wish to access, the RAC provider handles this slightly differently. For each remote machine (computer/server) that should be accessible, you create an _Endpoint_ object within a single RAC provider. (And as mentioned above, a single provider-application pair is used for all remote connections.)
|
||||
|
||||
The _Endpoint_ object specifies:
|
||||
The _Endpoint_ object specifies the hostname/IP of the machine to connect to, as well as the protocol to use. Additionally it is possible to bind policies to _endpoint_ objects to restrict access. Users must have access to both the application that the RAC Provider is using as well as the individual endpoint.
|
||||
|
||||
- Hostname, IP address, and port of the remote machine
|
||||
- Protocol to use: SSH, RDP, or VNC
|
||||
- RDP connection settings
|
||||
- [RAC Property mappings](#rac-property-mappings) to apply
|
||||
- [Connection settings](#connection-settings) to apply
|
||||
|
||||
Additionally, it is possible to bind policies to _Endpoint_ objects to restrict user access. To connect to a remote machine, users must have access to both the application that the RAC provider is using and the corresponding endpoint.
|
||||
|
||||
## Connection management
|
||||
|
||||
A new connection is created every time an RAC application/endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, you can configure connection expiry in the RAC provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
|
||||
|
||||
## RAC Property Mappings
|
||||
|
||||
You can create RAC property mappings via **Customization** > **Property Mappings**.
|
||||
|
||||
RAC property mappings allow you to configure the following settings:
|
||||
|
||||
- **Username**: the username for the remote machine
|
||||
- **Password**: the password for the remote machine
|
||||
- **Ignore Server certificate**: set whether the validity of the returned RDP server certificate will be ignored
|
||||
- **Enable wallpaper**: enable/disable the desktop wallpaper of the RDP server
|
||||
- **Enable font-smoothing**: enable/disable font-smoothing (anti-aliasing) on the RDP server
|
||||
- **Enable full window dragging**: enable/disable whether the full content of a window is visible while moving it on the RDP server
|
||||
- **Advanced settings**: set [connection settings](#connection-settings) via a Python expression
|
||||
|
||||
## Connection settings
|
||||
|
||||
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
|
||||
Connection settings can include `username`, `password`, `domain`, `private-key`, `security`, `enable-audio`, and more.
|
||||
|
||||
For a full list of possible connection settings, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
|
||||
|
||||
RAC connection settings can be set via several methods and are all merged together when connecting:
|
||||
Configuration details such as credentials can be specified through _settings_, which can be specified on different levels and are all merged together when connecting:
|
||||
|
||||
1. Default settings
|
||||
2. RAC Provider settings
|
||||
3. RAC Endpoint settings
|
||||
4. RAC Provider property mapping settings
|
||||
5. RAC Endpoint property mapping settings
|
||||
6. The `connection_settings` object in the flow plan
|
||||
2. Provider settings
|
||||
3. Endpoint settings
|
||||
4. Provider property mapping settings
|
||||
5. Endpoint property mapping settings
|
||||
6. Connection settings
|
||||
|
||||
For examples of how to configure connection settings, see the [RAC SSH public key authentication](./rac-public-key.md) and [RAC Credentials Prompt](./rac_credentials_prompt.md) documentation.
|
||||
### Connection settings
|
||||
|
||||
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
|
||||
|
||||
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
|
||||
|
||||
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
|
||||
The RAC provider utilizes [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
|
||||
For a full list of possible connection configurations, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
|
||||
|
||||
RAC connection settings can be set via several methods:
|
||||
|
||||
1. The settings of the RAC provider
|
||||
2. RAC endpoint settings
|
||||
3. RAC property mappings
|
||||
4. Retrieved from user or group attributes via RAC property mappings
|
||||
|
||||
For an example of how to set a connection setting see the [RAC SSH public key authentication](./rac-public-key.md) page.
|
||||
|
||||
## Capabilities
|
||||
|
||||
|
||||
BIN
website/docs/add-secure-apps/providers/rac/rac-v3.png
Normal file
BIN
website/docs/add-secure-apps/providers/rac/rac-v3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user