Merge branch 'main' into celery-2-dramatiq
This commit is contained in:
4
Makefile
4
Makefile
@ -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
|
||||
#########################
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -25,6 +25,7 @@ class AuthenticatorWebAuthnStageSerializer(StageSerializer):
|
||||
"resident_key_requirement",
|
||||
"device_type_restrictions",
|
||||
"device_type_restrictions_obj",
|
||||
"max_attempts",
|
||||
]
|
||||
|
||||
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -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 (
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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 []
|
||||
|
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
@ -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()
|
||||
|
96
packages/eslint-config/package-lock.json
generated
96
packages/eslint-config/package-lock.json
generated
@ -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",
|
||||
|
16
schema.yml
16
schema.yml
@ -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 |
@ -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
124
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
|
@ -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
|
||||
|
||||
|
49
website/package-lock.json
generated
49
website/package-lock.json
generated
@ -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"
|
||||
],
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user