Merge branch 'main' into celery-2-dramatiq

This commit is contained in:
Marc 'risson' Schmitt
2025-06-23 16:54:44 +02:00
51 changed files with 707 additions and 227 deletions

View File

@ -86,6 +86,10 @@ dev-create-db:
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
update-test-mmdb: ## Update test GeoIP and ASN Databases
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
#########################
## API Schema
#########################

View File

@ -13,7 +13,6 @@ class Command(TenantCommand):
parser.add_argument("usernames", nargs="*", type=str)
def handle_per_tenant(self, **options):
print(options)
new_type = UserTypes(options["type"])
qs = (
User.objects.exclude_anonymous()

View File

@ -97,6 +97,7 @@ class SourceStageFinal(StageView):
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context.update(self.executor.plan.context)
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
response = plan.to_redirect(self.request, token.flow)
token.delete()

View File

@ -90,14 +90,17 @@ class TestSourceStage(FlowTestCase):
plan: FlowPlan = session[SESSION_KEY_PLAN]
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
plan.context["foo"] = "bar"
session[SESSION_KEY_PLAN] = plan
session.save()
# Pretend we've just returned from the source
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
with self.assertFlowFinishes() as ff:
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
self.assertEqual(ff().context["foo"], "bar")

View File

@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
self.reader: Reader | None = None
self._last_mtime: float = 0.0
self.logger = get_logger()
self.open()
self.load()
def path(self) -> str | None:
"""Get the path to the MMDB file to load"""
raise NotImplementedError
def open(self):
def load(self):
"""Get GeoIP Reader, if configured, otherwise none"""
path = self.path()
if path == "" or not path:
@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
diff = self._last_mtime < mtime
if diff > 0:
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
self.open()
self.load()
except OSError as exc:
self.logger.warning("Failed to check MMDB age", exc=exc)

View File

@ -2,7 +2,9 @@
from django.test import TestCase
from authentik.events.context_processors.base import get_context_processors
from authentik.events.context_processors.geoip import GeoIPContextProcessor
from authentik.events.models import Event, EventAction
class TestGeoIP(TestCase):
@ -13,8 +15,7 @@ class TestGeoIP(TestCase):
def test_simple(self):
"""Test simple city wrapper"""
# IPs from
# https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
self.assertEqual(
self.reader.city_dict("2.125.160.216"),
{
@ -25,3 +26,12 @@ class TestGeoIP(TestCase):
"long": -1.25,
},
)
def test_special_chars(self):
"""Test city name with special characters"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
event = Event.new(EventAction.LOGIN)
event.client_ip = "89.160.20.112"
for processor in get_context_processors():
processor.enrich_event(event)
event.save()

View File

@ -1,13 +1,49 @@
"""authentik database backend"""
from django.core.checks import Warning
from django.db.backends.base.validation import BaseDatabaseValidation
from django_tenants.postgresql_backend.base import DatabaseWrapper as BaseDatabaseWrapper
from authentik.lib.config import CONFIG
class DatabaseValidation(BaseDatabaseValidation):
def check(self, **kwargs):
return self._check_encoding()
def _check_encoding(self):
"""Throw a warning when the server_encoding is not UTF-8 or
server_encoding and client_encoding are mismatched"""
messages = []
with self.connection.cursor() as cursor:
cursor.execute("SHOW server_encoding;")
server_encoding = cursor.fetchone()[0]
cursor.execute("SHOW client_encoding;")
client_encoding = cursor.fetchone()[0]
if server_encoding != client_encoding:
messages.append(
Warning(
"PostgreSQL Server and Client encoding are mismatched: Server: "
f"{server_encoding}, Client: {client_encoding}",
id="ak.db.W001",
)
)
if server_encoding != "UTF8":
messages.append(
Warning(
f"PostgreSQL Server encoding is not UTF8: {server_encoding}",
id="ak.db.W002",
)
)
return messages
class DatabaseWrapper(BaseDatabaseWrapper):
"""database backend which supports rotating credentials"""
validation_class = DatabaseValidation
def get_connection_params(self):
"""Refresh DB credentials before getting connection params"""
conn_params = super().get_connection_params()

View File

@ -12,6 +12,8 @@ from django.test.runner import DiscoverRunner
from django.test.testcases import apps
from structlog.stdlib import get_logger
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.lib.config import CONFIG
from authentik.lib.sentry import sentry_init
from authentik.root.signals import post_startup, pre_startup, startup
@ -76,6 +78,9 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
for key, value in test_config.items():
CONFIG.set(key, value)
ASN_CONTEXT_PROCESSOR.load()
GEOIP_CONTEXT_PROCESSOR.load()
sentry_init()
self.logger.debug("Test environment configured")

View File

@ -1,6 +1,7 @@
"""Validation stage challenge checking"""
from json import loads
from typing import TYPE_CHECKING
from urllib.parse import urlencode
from django.http import HttpRequest
@ -36,10 +37,12 @@ from authentik.stages.authenticator_email.models import EmailDevice
from authentik.stages.authenticator_sms.models import SMSDevice
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
LOGGER = get_logger()
if TYPE_CHECKING:
from authentik.stages.authenticator_validate.stage import AuthenticatorValidateStageView
class DeviceChallenge(PassiveSerializer):
@ -52,11 +55,11 @@ class DeviceChallenge(PassiveSerializer):
def get_challenge_for_device(
request: HttpRequest, stage: AuthenticatorValidateStage, device: Device
stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage, device: Device
) -> dict:
"""Generate challenge for a single device"""
if isinstance(device, WebAuthnDevice):
return get_webauthn_challenge(request, stage, device)
return get_webauthn_challenge(stage_view, stage, device)
if isinstance(device, EmailDevice):
return {"email": mask_email(device.email)}
# Code-based challenges have no hints
@ -64,26 +67,30 @@ def get_challenge_for_device(
def get_webauthn_challenge_without_user(
request: HttpRequest, stage: AuthenticatorValidateStage
stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage
) -> dict:
"""Same as `get_webauthn_challenge`, but allows any client device. We can then later check
who the device belongs to."""
request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
authentication_options = generate_authentication_options(
rp_id=get_rp_id(request),
rp_id=get_rp_id(stage_view.request),
allow_credentials=[],
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
)
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = (
authentication_options.challenge
)
return loads(options_to_json(authentication_options))
def get_webauthn_challenge(
request: HttpRequest, stage: AuthenticatorValidateStage, device: WebAuthnDevice | None = None
stage_view: "AuthenticatorValidateStageView",
stage: AuthenticatorValidateStage,
device: WebAuthnDevice | None = None,
) -> dict:
"""Send the client a challenge that we'll check later"""
request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
allowed_credentials = []
@ -94,12 +101,14 @@ def get_webauthn_challenge(
allowed_credentials.append(user_device.descriptor)
authentication_options = generate_authentication_options(
rp_id=get_rp_id(request),
rp_id=get_rp_id(stage_view.request),
allow_credentials=allowed_credentials,
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
)
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = (
authentication_options.challenge
)
return loads(options_to_json(authentication_options))
@ -146,7 +155,7 @@ def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Dev
def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -> Device:
"""Validate WebAuthn Challenge"""
request = stage_view.request
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
challenge = stage_view.executor.plan.context.get(PLAN_CONTEXT_WEBAUTHN_CHALLENGE)
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
try:
credential = parse_authentication_credential_json(data)

View File

@ -224,7 +224,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
data={
"device_class": device_class,
"device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, stage, device),
"challenge": get_challenge_for_device(self, stage, device),
"last_used": device.last_used,
}
)
@ -243,7 +243,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"device_class": DeviceClasses.WEBAUTHN,
"device_uid": -1,
"challenge": get_webauthn_challenge_without_user(
self.request,
self,
self.executor.current_stage,
),
"last_used": None,

View File

@ -31,7 +31,7 @@ from authentik.stages.authenticator_webauthn.models import (
WebAuthnDevice,
WebAuthnDeviceType,
)
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.user_login.models import UserLoginStage
@ -103,7 +103,11 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
device_classes=[DeviceClasses.WEBAUTHN],
webauthn_user_verification=UserVerification.PREFERRED,
)
challenge = get_challenge_for_device(request, stage, webauthn_device)
plan = FlowPlan("")
stage_view = AuthenticatorValidateStageView(
FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
)
challenge = get_challenge_for_device(stage_view, stage, webauthn_device)
del challenge["challenge"]
self.assertEqual(
challenge,
@ -122,7 +126,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
with self.assertRaises(ValidationError):
validate_challenge_webauthn(
{}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user
{},
StageView(FlowExecutorView(current_stage=stage, plan=plan), request=request),
self.user,
)
def test_device_challenge_webauthn_restricted(self):
@ -193,22 +199,35 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
sign_count=0,
rp_id=generate_id(),
)
challenge = get_challenge_for_device(request, stage, webauthn_device)
webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
plan = FlowPlan("")
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)
stage_view = AuthenticatorValidateStageView(
FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
)
challenge = get_challenge_for_device(stage_view, stage, webauthn_device)
self.assertEqual(
challenge,
{
"allowCredentials": [
{
"id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
"type": "public-key",
}
],
"challenge": bytes_to_base64url(webauthn_challenge),
"rpId": "testserver",
"timeout": 60000,
"userVerification": "preferred",
},
challenge["allowCredentials"],
[
{
"id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
"type": "public-key",
}
],
)
self.assertIsNotNone(challenge["challenge"])
self.assertEqual(
challenge["rpId"],
"testserver",
)
self.assertEqual(
challenge["timeout"],
60000,
)
self.assertEqual(
challenge["userVerification"],
"preferred",
)
def test_get_challenge_userless(self):
@ -228,18 +247,16 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
sign_count=0,
rp_id=generate_id(),
)
challenge = get_webauthn_challenge_without_user(request, stage)
webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
self.assertEqual(
challenge,
{
"allowCredentials": [],
"challenge": bytes_to_base64url(webauthn_challenge),
"rpId": "testserver",
"timeout": 60000,
"userVerification": "preferred",
},
plan = FlowPlan("")
stage_view = AuthenticatorValidateStageView(
FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
)
challenge = get_webauthn_challenge_without_user(stage_view, stage)
self.assertEqual(challenge["allowCredentials"], [])
self.assertIsNotNone(challenge["challenge"])
self.assertEqual(challenge["rpId"], "testserver")
self.assertEqual(challenge["timeout"], 60000)
self.assertEqual(challenge["userVerification"], "preferred")
def test_validate_challenge_unrestricted(self):
"""Test webauthn authentication (unrestricted webauthn device)"""
@ -275,10 +292,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"last_used": None,
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
)
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
@ -352,10 +369,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"last_used": None,
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
)
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
@ -433,10 +450,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"last_used": None,
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
@ -496,17 +513,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.WEBAUTHN],
)
stage_view = AuthenticatorValidateStageView(
FlowExecutorView(flow=flow, current_stage=stage), request=request
)
request = get_request("/")
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
plan = FlowPlan(flow.pk.hex)
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)
request.session.save()
request = get_request("/")
stage_view = AuthenticatorValidateStageView(
FlowExecutorView(flow=flow, current_stage=stage), request=request
FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request
)
request.META["SERVER_NAME"] = "localhost"
request.META["SERVER_PORT"] = "9000"

View File

@ -25,6 +25,7 @@ class AuthenticatorWebAuthnStageSerializer(StageSerializer):
"resident_key_requirement",
"device_type_restrictions",
"device_type_restrictions_obj",
"max_attempts",
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.1.11 on 2025-06-13 22:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_stages_authenticator_webauthn",
"0012_webauthndevice_created_webauthndevice_last_updated_and_more",
),
]
operations = [
migrations.AddField(
model_name="authenticatorwebauthnstage",
name="max_attempts",
field=models.PositiveIntegerField(default=0),
),
]

View File

@ -84,6 +84,8 @@ class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
device_type_restrictions = models.ManyToManyField("WebAuthnDeviceType", blank=True)
max_attempts = models.PositiveIntegerField(default=0)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.stages.authenticator_webauthn.api.stages import (

View File

@ -5,12 +5,13 @@ from uuid import UUID
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.utils.translation import gettext as __
from django.utils.translation import gettext_lazy as _
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from webauthn import options_to_json
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
from webauthn.helpers.exceptions import InvalidRegistrationResponse
from webauthn.helpers.exceptions import WebAuthnException
from webauthn.helpers.structs import (
AttestationConveyancePreference,
AuthenticatorAttachment,
@ -41,7 +42,8 @@ from authentik.stages.authenticator_webauthn.models import (
)
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
PLAN_CONTEXT_WEBAUTHN_CHALLENGE = "goauthentik.io/stages/authenticator_webauthn/challenge"
PLAN_CONTEXT_WEBAUTHN_ATTEMPT = "goauthentik.io/stages/authenticator_webauthn/attempt"
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
@ -62,7 +64,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
def validate_response(self, response: dict) -> dict:
"""Validate webauthn challenge response"""
challenge = self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
challenge = self.stage.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]
try:
registration: VerifiedRegistration = verify_registration_response(
@ -71,7 +73,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
expected_rp_id=get_rp_id(self.request),
expected_origin=get_origin(self.request),
)
except InvalidRegistrationResponse as exc:
except WebAuthnException as exc:
self.stage.logger.warning("registration failed", exc=exc)
raise ValidationError(f"Registration failed. Error: {exc}") from None
@ -114,9 +116,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
response_class = AuthenticatorWebAuthnChallengeResponse
def get_challenge(self, *args, **kwargs) -> Challenge:
# clear session variables prior to starting a new registration
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
self.executor.plan.context.setdefault(PLAN_CONTEXT_WEBAUTHN_ATTEMPT, 0)
# clear flow variables prior to starting a new registration
self.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
user = self.get_pending_user()
# library accepts none so we store null in the database, but if there is a value
@ -139,8 +142,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
attestation=AttestationConveyancePreference.DIRECT,
)
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge
self.request.session.save()
self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = registration_options.challenge
return AuthenticatorWebAuthnChallenge(
data={
"registration": loads(options_to_json(registration_options)),
@ -153,6 +155,24 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
response.user = self.get_pending_user()
return response
def challenge_invalid(self, response):
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
self.executor.plan.context.setdefault(PLAN_CONTEXT_WEBAUTHN_ATTEMPT, 0)
self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_ATTEMPT] += 1
if (
stage.max_attempts > 0
and self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_ATTEMPT] >= stage.max_attempts
):
return self.executor.stage_invalid(
__(
"Exceeded maximum attempts. "
"Contact your {brand} administrator for help.".format(
brand=self.request.brand.branding_title
)
)
)
return super().challenge_invalid(response)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Webauthn Challenge has already been validated
webauthn_credential: VerifiedRegistration = response.validated_data["response"]
@ -179,6 +199,3 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
else:
return self.executor.stage_invalid("Device with Credential ID already exists.")
return self.executor.stage_ok()
def cleanup(self):
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)

View File

@ -18,7 +18,7 @@ from authentik.stages.authenticator_webauthn.models import (
WebAuthnDevice,
WebAuthnDeviceType,
)
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
@ -57,6 +57,9 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(response.status_code, 200)
session = self.client.session
self.assertStageResponse(
@ -70,7 +73,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
"name": self.user.username,
"displayName": self.user.name,
},
"challenge": bytes_to_base64url(session[SESSION_KEY_WEBAUTHN_CHALLENGE]),
"challenge": bytes_to_base64url(plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]),
"pubKeyCredParams": [
{"type": "public-key", "alg": -7},
{"type": "public-key", "alg": -8},
@ -97,11 +100,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
"""Test registration"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@ -146,11 +149,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@ -209,11 +212,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@ -259,11 +262,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@ -298,3 +301,109 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists())
def test_register_max_retries(self):
"""Test registration (exceeding max retries)"""
self.stage.max_attempts = 2
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
# first failed request
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={
"component": "ak-stage-authenticator-webauthn",
"response": {
"id": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
"rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
"type": "public-key",
"registrationClientExtensions": "{}",
"response": {
"clientDataJSON": (
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd"
"lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV"
"pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU"
"mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw"
"Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmF"
),
"attestationObject": (
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg"
"OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA"
"cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp"
"QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq"
"2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds"
),
},
},
},
SERVER_NAME="localhost",
SERVER_PORT="9000",
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
flow=self.flow,
component="ak-stage-authenticator-webauthn",
response_errors={
"response": [
{
"string": (
"Registration failed. Error: Unable to decode "
"client_data_json bytes as JSON"
),
"code": "invalid",
}
]
},
)
self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists())
# Second failed request
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={
"component": "ak-stage-authenticator-webauthn",
"response": {
"id": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
"rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
"type": "public-key",
"registrationClientExtensions": "{}",
"response": {
"clientDataJSON": (
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd"
"lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV"
"pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU"
"mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw"
"Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmF"
),
"attestationObject": (
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg"
"OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA"
"cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp"
"QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq"
"2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds"
),
},
},
},
SERVER_NAME="localhost",
SERVER_PORT="9000",
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
flow=self.flow,
component="ak-stage-access-denied",
error_message=(
"Exceeded maximum attempts. Contact your authentik administrator for help."
),
)
self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists())

View File

@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
)
if configured_binding_net != NetworkBinding.NO_BINDING:
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
if configured_binding_geo != GeoIPBinding.NO_BINDING:
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
# If we got to this point without any error being raised, we need to
# update the last saved IP to the current one
if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
@ -111,7 +111,8 @@ class BoundSessionMiddleware(SessionMiddleware):
# (== basically requires the user to be logged in)
request.session[request.session.model.Keys.LAST_IP] = new_ip
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
@staticmethod
def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
"""Check network/ASN binding"""
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
@ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware):
new_ip,
)
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
@staticmethod
def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
"""Check GeoIP binding"""
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
@ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.continent != new_geo.continent:
raise SessionBindingBroken(
"geoip.continent",
last_geo.continent,
new_geo.continent,
last_geo.continent.to_dict(),
new_geo.continent.to_dict(),
last_ip,
new_ip,
)
@ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.country != new_geo.country:
raise SessionBindingBroken(
"geoip.country",
last_geo.country,
new_geo.country,
last_geo.country.to_dict(),
new_geo.country.to_dict(),
last_ip,
new_ip,
)
@ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.city != new_geo.city:
raise SessionBindingBroken(
"geoip.city",
last_geo.city,
new_geo.city,
last_geo.city.to_dict(),
new_geo.city.to_dict(),
last_ip,
new_ip,
)

View File

@ -3,6 +3,7 @@
from time import sleep
from unittest.mock import patch
from django.http import HttpRequest
from django.urls import reverse
from django.utils.timezone import now
@ -17,7 +18,12 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.user_login.models import UserLoginStage
from authentik.stages.user_login.middleware import (
BoundSessionMiddleware,
SessionBindingBroken,
logout_extra,
)
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
class TestUserLoginStage(FlowTestCase):
@ -192,3 +198,52 @@ class TestUserLoginStage(FlowTestCase):
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
response = self.client.get(reverse("authentik_api:application-list"))
self.assertEqual(response.status_code, 403)
def test_binding_net_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
for args, expect in [
[[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]],
[[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]],
[
[NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"],
["network.asn_network"],
],
[[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_net(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)
def test_binding_geo_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
for args, expect in [
[[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]],
[[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"],
["geoip.country"],
],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"],
["geoip.city"],
],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_geo(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)

View File

@ -16,6 +16,7 @@ def check_embedded_outpost_disabled(app_configs, **kwargs):
"Embedded outpost must be disabled when tenants API is enabled.",
hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to "
"True, or disable the tenants API by setting tenants.enabled to False",
id="ak.tenants.E001",
)
]
return []

View File

@ -13359,6 +13359,12 @@
"format": "uuid"
},
"title": "Device type restrictions"
},
"max_attempts": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"title": "Max attempts"
}
},
"required": []

2
go.mod
View File

@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025062.3
goauthentik.io/api/v3 v3.2025062.4
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.15.0

4
go.sum
View File

@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025062.3 h1:syBOKigaHyX/8Rwmh9kOSF+TzsxOzmP5i7rsFwbemzA=
goauthentik.io/api/v3 v3.2025062.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025062.4 h1:HuyL12kKserXT2w+wCDUYNRSeyCCGX81wU9SRCPuxDo=
goauthentik.io/api/v3 v3.2025062.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -10,7 +10,7 @@ from typing import Any
from psycopg import Connection, Cursor, connect
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
from authentik.lib.config import CONFIG, django_db_config
LOGGER = get_logger()
ADV_LOCK_UID = 1000
@ -115,9 +115,13 @@ def run_migrations():
execute_from_command_line(["", "migrate_schemas"])
if CONFIG.get_bool("tenants.enabled", False):
execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"])
execute_from_command_line(
["", "check"] + ([] if CONFIG.get_bool("debug") else ["--deploy"])
)
# Run django system checks for all databases
check_args = ["", "check"]
for label in django_db_config(CONFIG).keys():
check_args.append(f"--database={label}")
if not CONFIG.get_bool("DEBUG"):
check_args.append("--deploy")
execute_from_command_line(check_args)
finally:
release_lock(curr)
curr.close()

View File

@ -920,17 +920,19 @@
}
},
"node_modules/array-includes": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
"integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
"integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.2",
"es-object-atoms": "^1.0.0",
"get-intrinsic": "^1.2.4",
"is-string": "^1.0.7"
"es-abstract": "^1.24.0",
"es-object-atoms": "^1.1.1",
"get-intrinsic": "^1.3.0",
"is-string": "^1.1.1",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@ -1376,27 +1378,27 @@
}
},
"node_modules/es-abstract": {
"version": "1.23.9",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
"integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
"integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
"license": "MIT",
"dependencies": {
"array-buffer-byte-length": "^1.0.2",
"arraybuffer.prototype.slice": "^1.0.4",
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"call-bound": "^1.0.4",
"data-view-buffer": "^1.0.2",
"data-view-byte-length": "^1.0.2",
"data-view-byte-offset": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"es-set-tostringtag": "^2.1.0",
"es-to-primitive": "^1.3.0",
"function.prototype.name": "^1.1.8",
"get-intrinsic": "^1.2.7",
"get-proto": "^1.0.0",
"get-intrinsic": "^1.3.0",
"get-proto": "^1.0.1",
"get-symbol-description": "^1.1.0",
"globalthis": "^1.0.4",
"gopd": "^1.2.0",
@ -1408,21 +1410,24 @@
"is-array-buffer": "^3.0.5",
"is-callable": "^1.2.7",
"is-data-view": "^1.0.2",
"is-negative-zero": "^2.0.3",
"is-regex": "^1.2.1",
"is-set": "^2.0.3",
"is-shared-array-buffer": "^1.0.4",
"is-string": "^1.1.1",
"is-typed-array": "^1.1.15",
"is-weakref": "^1.1.0",
"is-weakref": "^1.1.1",
"math-intrinsics": "^1.1.0",
"object-inspect": "^1.13.3",
"object-inspect": "^1.13.4",
"object-keys": "^1.1.1",
"object.assign": "^4.1.7",
"own-keys": "^1.0.1",
"regexp.prototype.flags": "^1.5.3",
"regexp.prototype.flags": "^1.5.4",
"safe-array-concat": "^1.1.3",
"safe-push-apply": "^1.0.0",
"safe-regex-test": "^1.1.0",
"set-proto": "^1.0.0",
"stop-iteration-iterator": "^1.1.0",
"string.prototype.trim": "^1.2.10",
"string.prototype.trimend": "^1.0.9",
"string.prototype.trimstart": "^1.0.8",
@ -1431,7 +1436,7 @@
"typed-array-byte-offset": "^1.0.4",
"typed-array-length": "^1.0.7",
"unbox-primitive": "^1.1.0",
"which-typed-array": "^1.1.18"
"which-typed-array": "^1.1.19"
},
"engines": {
"node": ">= 0.4"
@ -1634,9 +1639,9 @@
}
},
"node_modules/eslint-module-utils": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
"license": "MIT",
"dependencies": {
"debug": "^3.2.7"
@ -1660,29 +1665,29 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"version": "2.32.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"license": "MIT",
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
"array.prototype.findlastindex": "^1.2.5",
"array.prototype.flat": "^1.3.2",
"array.prototype.flatmap": "^1.3.2",
"array-includes": "^3.1.9",
"array.prototype.findlastindex": "^1.2.6",
"array.prototype.flat": "^1.3.3",
"array.prototype.flatmap": "^1.3.3",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.12.0",
"eslint-module-utils": "^2.12.1",
"hasown": "^2.0.2",
"is-core-module": "^2.15.1",
"is-core-module": "^2.16.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"object.groupby": "^1.0.3",
"object.values": "^1.2.0",
"object.values": "^1.2.1",
"semver": "^6.3.1",
"string.prototype.trimend": "^1.0.8",
"string.prototype.trimend": "^1.0.9",
"tsconfig-paths": "^3.15.0"
},
"engines": {
@ -2501,6 +2506,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
"integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -3693,6 +3710,19 @@
"node": ">=10"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
"integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"internal-slot": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",

View File

@ -34791,6 +34791,10 @@ paths:
name: friendly_name
schema:
type: string
- in: query
name: max_attempts
schema:
type: integer
- in: query
name: name
schema:
@ -42831,6 +42835,10 @@ components:
items:
$ref: '#/components/schemas/WebAuthnDeviceType'
readOnly: true
max_attempts:
type: integer
maximum: 2147483647
minimum: 0
required:
- component
- device_type_restrictions_obj
@ -42873,6 +42881,10 @@ components:
items:
type: string
format: uuid
max_attempts:
type: integer
maximum: 2147483647
minimum: 0
required:
- name
AuthorizationCodeAuthMethodEnum:
@ -52833,6 +52845,10 @@ components:
items:
type: string
format: uuid
max_attempts:
type: integer
maximum: 2147483647
minimum: 0
PatchedBlueprintInstanceRequest:
type: object
description: Info about a single blueprint instance file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -7,7 +7,7 @@ services:
network_mode: host
restart: always
mailpit:
image: docker.io/axllent/mailpit:v1.26.1
image: docker.io/axllent/mailpit:v1.26.2
ports:
- 1025:1025
- 8025:8025

124
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.7.2",
"@goauthentik/api": "^2025.6.2-1750246811",
"@goauthentik/api": "^2025.6.2-1750636159",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -1731,9 +1731,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2025.6.2-1750246811",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750246811.tgz",
"integrity": "sha512-ENHEi3kGAodf5tKQb5kziUrT1EcJw3z8tp2mU7LWqNlXr4eoAI15BjDfH5DW56l4jy3xKqTd+R2Ntnj4hiVhHw=="
"version": "2025.6.2-1750636159",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750636159.tgz",
"integrity": "sha512-LPseyRzzi5Wk/cP8suRYUhwe/sGdIsGIcaXUkl13jprkJCUXEfuLcfAgdJka2MnIPaMyBDv7oYxJ0IhV/sidEg=="
},
"node_modules/@goauthentik/core": {
"resolved": "packages/core",
@ -10380,18 +10380,20 @@
}
},
"node_modules/array-includes": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
"integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
"integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.2",
"es-object-atoms": "^1.0.0",
"get-intrinsic": "^1.2.4",
"is-string": "^1.0.7"
"es-abstract": "^1.24.0",
"es-object-atoms": "^1.1.1",
"get-intrinsic": "^1.3.0",
"is-string": "^1.1.1",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@ -13642,9 +13644,9 @@
"license": "MIT"
},
"node_modules/es-abstract": {
"version": "1.23.9",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
"integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
"integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -13652,18 +13654,18 @@
"arraybuffer.prototype.slice": "^1.0.4",
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"call-bound": "^1.0.4",
"data-view-buffer": "^1.0.2",
"data-view-byte-length": "^1.0.2",
"data-view-byte-offset": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"es-set-tostringtag": "^2.1.0",
"es-to-primitive": "^1.3.0",
"function.prototype.name": "^1.1.8",
"get-intrinsic": "^1.2.7",
"get-proto": "^1.0.0",
"get-intrinsic": "^1.3.0",
"get-proto": "^1.0.1",
"get-symbol-description": "^1.1.0",
"globalthis": "^1.0.4",
"gopd": "^1.2.0",
@ -13675,21 +13677,24 @@
"is-array-buffer": "^3.0.5",
"is-callable": "^1.2.7",
"is-data-view": "^1.0.2",
"is-negative-zero": "^2.0.3",
"is-regex": "^1.2.1",
"is-set": "^2.0.3",
"is-shared-array-buffer": "^1.0.4",
"is-string": "^1.1.1",
"is-typed-array": "^1.1.15",
"is-weakref": "^1.1.0",
"is-weakref": "^1.1.1",
"math-intrinsics": "^1.1.0",
"object-inspect": "^1.13.3",
"object-inspect": "^1.13.4",
"object-keys": "^1.1.1",
"object.assign": "^4.1.7",
"own-keys": "^1.0.1",
"regexp.prototype.flags": "^1.5.3",
"regexp.prototype.flags": "^1.5.4",
"safe-array-concat": "^1.1.3",
"safe-push-apply": "^1.0.0",
"safe-regex-test": "^1.1.0",
"set-proto": "^1.0.0",
"stop-iteration-iterator": "^1.1.0",
"string.prototype.trim": "^1.2.10",
"string.prototype.trimend": "^1.0.9",
"string.prototype.trimstart": "^1.0.8",
@ -13698,7 +13703,7 @@
"typed-array-byte-offset": "^1.0.4",
"typed-array-length": "^1.0.7",
"unbox-primitive": "^1.1.0",
"which-typed-array": "^1.1.18"
"which-typed-array": "^1.1.19"
},
"engines": {
"node": ">= 0.4"
@ -14623,9 +14628,9 @@
}
},
"node_modules/eslint-module-utils": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -14651,30 +14656,30 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"version": "2.32.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
"array.prototype.findlastindex": "^1.2.5",
"array.prototype.flat": "^1.3.2",
"array.prototype.flatmap": "^1.3.2",
"array-includes": "^3.1.9",
"array.prototype.findlastindex": "^1.2.6",
"array.prototype.flat": "^1.3.3",
"array.prototype.flatmap": "^1.3.3",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.12.0",
"eslint-module-utils": "^2.12.1",
"hasown": "^2.0.2",
"is-core-module": "^2.15.1",
"is-core-module": "^2.16.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"object.groupby": "^1.0.3",
"object.values": "^1.2.0",
"object.values": "^1.2.1",
"semver": "^6.3.1",
"string.prototype.trimend": "^1.0.8",
"string.prototype.trimend": "^1.0.9",
"tsconfig-paths": "^3.15.0"
},
"engines": {
@ -17382,9 +17387,10 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
@ -17563,6 +17569,19 @@
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
"node_modules/is-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
"integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -24021,14 +24040,17 @@
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
"integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
"es-errors": "^1.3.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"set-function-name": "^2.0.2"
},
"engines": {
@ -25530,6 +25552,20 @@
"dev": true,
"optional": true
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
"integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"internal-slot": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/storybook": {
"version": "8.6.14",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.14.tgz",

View File

@ -93,7 +93,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.7.2",
"@goauthentik/api": "^2025.6.2-1750246811",
"@goauthentik/api": "^2025.6.2-1750636159",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",

View File

@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -165,6 +166,15 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
>
</ak-radio>
</ak-form-element-horizontal>
<ak-number-input
label=${msg("Maximum registration attempts")}
required
name="maxAttempts"
value="${this.instance?.maxAttempts || 0}"
help=${msg(
"Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.",
)}
></ak-number-input>
<ak-form-element-horizontal
label=${msg("Device type restrictions")}
name="deviceTypeRestrictions"

View File

@ -93,7 +93,7 @@ export class UserSettingsFlowExecutor
}
updated(): void {
if (!this.flowSlug && this.brand) {
if (!this.flowSlug && this.brand?.flowUserSettings) {
this.flowSlug = this.brand.flowUserSettings;
this.nextChallenge();
}

View File

@ -9250,6 +9250,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -7760,6 +7760,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9311,6 +9311,12 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9879,6 +9879,12 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9862,6 +9862,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9218,6 +9218,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9122,6 +9122,12 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9545,6 +9545,12 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9554,4 +9554,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body></file></xliff>

View File

@ -9637,6 +9637,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9609,6 +9609,12 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -6377,6 +6377,12 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -9892,6 +9892,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -7461,6 +7461,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -9197,6 +9197,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
</trans-unit>
<trans-unit id="s8495753cb15e8d8e">
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
</trans-unit>
</body>
</file>

View File

@ -8,9 +8,7 @@ To prevent infinite loops (events created by policies which are attached to a No
## Filtering Events
Starting with authentik 0.15, you can create notification rules, which can alert you based on the creation of certain events.
Filtering is done by using the Policy Engine. You can do simple filtering using the "Event Matcher Policy" type.
An authentik administrator can create notification rules based on the creation of specified events. Filtering is done by using the Policy Engine. You can do simple filtering using the "Event Matcher Policy" type.
![](./event_matcher.png)

View File

@ -128,9 +128,9 @@ To support the integration of Bitwarden with authentik, you need to create an ap
### Download certificate file
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers** and click on the name of the provider that you created in the previous section (e.g. `Provider for Bitwarden`).
3. Navigate to **Applications** > **Providers** and click on the name of the provider that you created in the previous section (e.g. `Provider for bitwarden`).
4. Under **Related objects** > **Download signing certificate**, click on **Download**. This downloaded file is your certificate file and it will be required in the next section.
3. Under **Related objects** > **Download signing certificate**, click on **Download**. This downloaded file is your certificate file and it will be required in the next section.
## Bitwarden configuration

View File

@ -19,6 +19,7 @@
"@goauthentik/docusaurus-config": "^1.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@mdx-js/react": "^3.1.0",
"@swc/html-linux-x64-gnu": "1.12.5",
"clsx": "^2.1.1",
"docusaurus-plugin-openapi-docs": "^4.4.0",
"docusaurus-theme-openapi-docs": "^4.4.0",
@ -64,12 +65,12 @@
"@rspack/binding-darwin-arm64": "1.3.15",
"@rspack/binding-linux-arm64-gnu": "1.3.15",
"@rspack/binding-linux-x64-gnu": "1.3.15",
"@swc/core-darwin-arm64": "1.12.1",
"@swc/core-linux-arm64-gnu": "1.12.1",
"@swc/core-linux-x64-gnu": "1.12.1",
"@swc/html-darwin-arm64": "1.12.1",
"@swc/html-linux-arm64-gnu": "1.12.1",
"@swc/html-linux-x64-gnu": "1.12.1",
"@swc/core-darwin-arm64": "1.12.5",
"@swc/core-linux-arm64-gnu": "1.12.5",
"@swc/core-linux-x64-gnu": "1.12.5",
"@swc/html-darwin-arm64": "1.12.5",
"@swc/html-linux-arm64-gnu": "1.12.5",
"@swc/html-linux-x64-gnu": "1.12.5",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"
@ -5592,9 +5593,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.1.tgz",
"integrity": "sha512-nUjWVcJ3YS2N40ZbKwYO2RJ4+o2tWYRzNOcIQp05FqW0+aoUCVMdAUUzQinPDynfgwVshDAXCKemY8X7nN5MaA==",
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.5.tgz",
"integrity": "sha512-3WF+naP/qkt5flrTfJr+p07b522JcixKvIivM7FgvllA6LjJxf+pheoILrTS8IwrNAK/XtHfKWYcGY+3eaA4mA==",
"cpu": [
"arm64"
],
@ -5640,9 +5641,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.1.tgz",
"integrity": "sha512-BxJDIJPq1+aCh9UsaSAN6wo3tuln8UhNXruOrzTI8/ElIig/3sAueDM6Eq7GvZSGGSA7ljhNATMJ0elD7lFatQ==",
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.5.tgz",
"integrity": "sha512-GkzgIUz+2r6J6Tn3hb7/4ByaWHRrRZt4vuN9BLAd+y65m2Bt0vlEpPtWhrB/TVe4hEkFR+W5PDETLEbUT4i0tQ==",
"cpu": [
"arm64"
],
@ -5672,9 +5673,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.1.tgz",
"integrity": "sha512-CrYnV8SZIgArQ9LKH0xEF95PKXzX9WkRSc5j55arOSBeDCeDUQk1Bg/iKdnDiuj5HC1hZpvzwMzSBJjv+Z70jA==",
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.5.tgz",
"integrity": "sha512-PeYoSziNy+iNiBHPtAsO84bzBne/mbCsG5ijYkAhS1GVsDgohClorUvRXXhcUZoX2gr8TfSI9WLHo30K+DKiHg==",
"cpu": [
"x64"
],
@ -5830,9 +5831,9 @@
}
},
"node_modules/@swc/html-darwin-arm64": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.1.tgz",
"integrity": "sha512-vbCqYgBBdoxlsnUe/G6irBJ69LUOrlLVXgdxWxDSZ3YcbnpVmwi5YEeaRvqf4vNzZ/nzBMd4DYl6KK2Qsi0prw==",
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.5.tgz",
"integrity": "sha512-PE9cCiQUxxC15VrN9D+nDXS9R1z6Fxg04wRoEVg2imRa+I5uUSWjJwzfT3vHmNbvXdR0poybL9ro1Zr74EWliw==",
"cpu": [
"arm64"
],
@ -5878,9 +5879,9 @@
}
},
"node_modules/@swc/html-linux-arm64-gnu": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.1.tgz",
"integrity": "sha512-KbqPLtsPVt0/kjp7sUT1APfEtNQUqMam3S0RzJkvuMz9jB2F9DREvj5EG+DPnx2s/kxnDm4sh9vM2sG2xNHErQ==",
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.5.tgz",
"integrity": "sha512-qNukrD+Nm4OjH6S+aiHHj7q5Bufhx/uPR3G3do6+xJLX97LsAONG0yQfgw1mRVoOtbVZ7JF2+y7VliFi+jcjow==",
"cpu": [
"arm64"
],
@ -5910,9 +5911,9 @@
}
},
"node_modules/@swc/html-linux-x64-gnu": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.1.tgz",
"integrity": "sha512-9QNCTgCZtyQVifLXqDTW7v4lgaC11v0/iL9OhsSZ19ycJrBmnxBmZtDIbuQrXAIzE1GD8mMOK/GLey2IeceoDQ==",
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.5.tgz",
"integrity": "sha512-RmGL1PFzFQ4IVABNEQQMnKH4kucbnYlh7Lro9GcfGyEHD+QowZF4JBCkhIYK6eL7eahGr8gVjyB7rApuj9+GbA==",
"cpu": [
"x64"
],

View File

@ -78,12 +78,12 @@
"@rspack/binding-darwin-arm64": "1.3.15",
"@rspack/binding-linux-arm64-gnu": "1.3.15",
"@rspack/binding-linux-x64-gnu": "1.3.15",
"@swc/core-darwin-arm64": "1.12.1",
"@swc/core-linux-arm64-gnu": "1.12.1",
"@swc/core-linux-x64-gnu": "1.12.1",
"@swc/html-darwin-arm64": "1.12.1",
"@swc/html-linux-arm64-gnu": "1.12.1",
"@swc/html-linux-x64-gnu": "1.12.1",
"@swc/core-darwin-arm64": "1.12.5",
"@swc/core-linux-arm64-gnu": "1.12.5",
"@swc/core-linux-x64-gnu": "1.12.5",
"@swc/html-darwin-arm64": "1.12.5",
"@swc/html-linux-arm64-gnu": "1.12.5",
"@swc/html-linux-x64-gnu": "1.12.5",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"