diff --git a/authentik/flows/tests/test_inspector.py b/authentik/flows/tests/test_inspector.py
index 2a01ea370c..81a04a797b 100644
--- a/authentik/flows/tests/test_inspector.py
+++ b/authentik/flows/tests/test_inspector.py
@@ -46,6 +46,7 @@ class TestFlowInspector(APITestCase):
res.content,
{
"allow_show_password": False,
+ "captcha_stage": None,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
diff --git a/authentik/providers/scim/clients/groups.py b/authentik/providers/scim/clients/groups.py
index 44b3405dff..8221d7d131 100644
--- a/authentik/providers/scim/clients/groups.py
+++ b/authentik/providers/scim/clients/groups.py
@@ -197,6 +197,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
chunk_size = self._config.bulk.maxOperations
if chunk_size < 1:
chunk_size = len(ops)
+ if len(ops) < 1:
+ return
for chunk in batched(ops, chunk_size):
req = PatchRequest(Operations=list(chunk))
self._request(
@@ -237,13 +239,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
users_to_add = []
users_to_remove = []
# Check users currently in group and if they shouldn't be in the group and remove them
- for user in current_group.members:
+ for user in current_group.members or []:
if user.value not in users_should:
users_to_remove.append(user.value)
# Check users that should be in the group and add them
for user in users_should:
if len([x for x in current_group.members if x.value == user]) < 1:
users_to_add.append(user)
+ # Only send request if we need to make changes
+ if len(users_to_add) < 1 and len(users_to_remove) < 1:
+ return
return self._patch_chunked(
scim_group.scim_id,
*[
diff --git a/authentik/stages/authenticator_validate/challenge.py b/authentik/stages/authenticator_validate/challenge.py
index c11439684f..1f9a656a38 100644
--- a/authentik/stages/authenticator_validate/challenge.py
+++ b/authentik/stages/authenticator_validate/challenge.py
@@ -8,7 +8,7 @@ from django.http.response import Http404
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as __
from django.utils.translation import gettext_lazy as _
-from rest_framework.fields import CharField
+from rest_framework.fields import CharField, DateTimeField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from webauthn import options_to_json
@@ -45,6 +45,7 @@ class DeviceChallenge(PassiveSerializer):
device_class = CharField()
device_uid = CharField()
challenge = JSONDictField()
+ last_used = DateTimeField(allow_null=True)
def get_challenge_for_device(
diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py
index 96ae7e6215..bde76d37db 100644
--- a/authentik/stages/authenticator_validate/stage.py
+++ b/authentik/stages/authenticator_validate/stage.py
@@ -217,6 +217,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"device_class": device_class,
"device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, stage, device),
+ "last_used": device.last_used,
}
)
challenge.is_valid()
@@ -237,6 +238,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
self.request,
self.executor.current_stage,
),
+ "last_used": None,
}
)
challenge.is_valid()
diff --git a/authentik/stages/authenticator_validate/tests/test_sms.py b/authentik/stages/authenticator_validate/tests/test_sms.py
index 5cce796207..850854a892 100644
--- a/authentik/stages/authenticator_validate/tests/test_sms.py
+++ b/authentik/stages/authenticator_validate/tests/test_sms.py
@@ -107,6 +107,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
"device_class": "sms",
"device_uid": str(device.pk),
"challenge": {},
+ "last_used": None,
},
},
)
diff --git a/authentik/stages/authenticator_validate/tests/test_stage.py b/authentik/stages/authenticator_validate/tests/test_stage.py
index 98fe5d2fe4..82a2f51322 100644
--- a/authentik/stages/authenticator_validate/tests/test_stage.py
+++ b/authentik/stages/authenticator_validate/tests/test_stage.py
@@ -169,6 +169,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_class": "baz",
"device_uid": "quox",
"challenge": {},
+ "last_used": None,
}
},
)
@@ -188,6 +189,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_class": "static",
"device_uid": "1",
"challenge": {},
+ "last_used": None,
},
},
)
diff --git a/authentik/stages/authenticator_validate/tests/test_webauthn.py b/authentik/stages/authenticator_validate/tests/test_webauthn.py
index e4247b221a..05fe216081 100644
--- a/authentik/stages/authenticator_validate/tests/test_webauthn.py
+++ b/authentik/stages/authenticator_validate/tests/test_webauthn.py
@@ -274,6 +274,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
+ "last_used": None,
}
]
session[SESSION_KEY_PLAN] = plan
@@ -352,6 +353,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
+ "last_used": None,
}
]
session[SESSION_KEY_PLAN] = plan
@@ -432,6 +434,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
+ "last_used": None,
}
]
session[SESSION_KEY_PLAN] = plan
diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py
index 3967e6d3d3..73bcff5dec 100644
--- a/authentik/stages/captcha/stage.py
+++ b/authentik/stages/captcha/stage.py
@@ -1,10 +1,11 @@
"""authentik captcha stage"""
from django.http.response import HttpResponse
-from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext as _
from requests import RequestException
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
+from structlog.stdlib import get_logger
from authentik.flows.challenge import (
Challenge,
@@ -16,6 +17,7 @@ from authentik.lib.utils.http import get_http_session
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.captcha.models import CaptchaStage
+LOGGER = get_logger()
PLAN_CONTEXT_CAPTCHA = "captcha"
@@ -27,6 +29,56 @@ class CaptchaChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-captcha")
+def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str):
+ """Validate captcha token"""
+ try:
+ response = get_http_session().post(
+ stage.api_url,
+ headers={
+ "Content-type": "application/x-www-form-urlencoded",
+ },
+ data={
+ "secret": stage.private_key,
+ "response": token,
+ "remoteip": remote_ip,
+ },
+ )
+ response.raise_for_status()
+ data = response.json()
+ if stage.error_on_invalid_score:
+ if not data.get("success", False):
+ error_codes = data.get("error-codes", ["unknown-error"])
+ LOGGER.warning("Failed to verify captcha token", error_codes=error_codes)
+
+ # These cases can usually be fixed by simply requesting a new token and retrying.
+ # [reCAPTCHA](https://developers.google.com/recaptcha/docs/verify#error_code_reference)
+ # [hCaptcha](https://docs.hcaptcha.com/#siteverify-error-codes-table)
+ # [Turnstile](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes)
+ retriable_error_codes = [
+ "missing-input-response",
+ "invalid-input-response",
+ "timeout-or-duplicate",
+ "expired-input-response",
+ "already-seen-response",
+ ]
+
+ if set(error_codes).issubset(set(retriable_error_codes)):
+ error_message = _("Invalid captcha response. Retrying may solve this issue.")
+ else:
+ error_message = _("Invalid captcha response")
+ raise ValidationError(error_message)
+ if "score" in data:
+ score = float(data.get("score"))
+ if stage.score_max_threshold > -1 and score > stage.score_max_threshold:
+ raise ValidationError(_("Invalid captcha response"))
+ if stage.score_min_threshold > -1 and score < stage.score_min_threshold:
+ raise ValidationError(_("Invalid captcha response"))
+ except (RequestException, TypeError) as exc:
+ raise ValidationError(_("Failed to validate token")) from exc
+
+ return data
+
+
class CaptchaChallengeResponse(ChallengeResponse):
"""Validate captcha token"""
@@ -36,38 +88,9 @@ class CaptchaChallengeResponse(ChallengeResponse):
def validate_token(self, token: str) -> str:
"""Validate captcha token"""
stage: CaptchaStage = self.stage.executor.current_stage
- try:
- response = get_http_session().post(
- stage.api_url,
- headers={
- "Content-type": "application/x-www-form-urlencoded",
- },
- data={
- "secret": stage.private_key,
- "response": token,
- "remoteip": ClientIPMiddleware.get_client_ip(self.stage.request),
- },
- )
- response.raise_for_status()
- data = response.json()
- if stage.error_on_invalid_score:
- if not data.get("success", False):
- raise ValidationError(
- _(
- "Failed to validate token: {error}".format(
- error=data.get("error-codes", _("Unknown error"))
- )
- )
- )
- if "score" in data:
- score = float(data.get("score"))
- if stage.score_max_threshold > -1 and score > stage.score_max_threshold:
- raise ValidationError(_("Invalid captcha response"))
- if stage.score_min_threshold > -1 and score < stage.score_min_threshold:
- raise ValidationError(_("Invalid captcha response"))
- except (RequestException, TypeError) as exc:
- raise ValidationError(_("Failed to validate token")) from exc
- return data
+ client_ip = ClientIPMiddleware.get_client_ip(self.stage.request)
+
+ return verify_captcha_token(stage, token, client_ip)
class CaptchaStageView(ChallengeStageView):
diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py
index 9ad97320e8..c8de2d7436 100644
--- a/authentik/stages/identification/api.py
+++ b/authentik/stages/identification/api.py
@@ -27,6 +27,7 @@ class IdentificationStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + [
"user_fields",
"password_stage",
+ "captcha_stage",
"case_insensitive_matching",
"show_matched_user",
"enrollment_flow",
@@ -46,6 +47,7 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet):
filterset_fields = [
"name",
"password_stage",
+ "captcha_stage",
"case_insensitive_matching",
"show_matched_user",
"enrollment_flow",
diff --git a/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py
new file mode 100644
index 0000000000..734dc7631c
--- /dev/null
+++ b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.0.8 on 2024-08-29 11:31
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"),
+ ("authentik_stages_identification", "0014_identificationstage_pretend"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="identificationstage",
+ name="captcha_stage",
+ field=models.ForeignKey(
+ default=None,
+ help_text="When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="authentik_stages_captcha.captchastage",
+ ),
+ ),
+ ]
diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py
index 27cfcb92f1..ed6728c932 100644
--- a/authentik/stages/identification/models.py
+++ b/authentik/stages/identification/models.py
@@ -8,6 +8,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source
from authentik.flows.models import Flow, Stage
+from authentik.stages.captcha.models import CaptchaStage
from authentik.stages.password.models import PasswordStage
@@ -43,6 +44,19 @@ class IdentificationStage(Stage):
),
)
+ captcha_stage = models.ForeignKey(
+ CaptchaStage,
+ null=True,
+ default=None,
+ on_delete=models.SET_NULL,
+ help_text=_(
+ (
+ "When set, adds functionality exactly like a Captcha stage, but baked into the "
+ "Identification stage."
+ ),
+ ),
+ )
+
case_insensitive_matching = models.BooleanField(
default=True,
help_text=_("When enabled, user fields are matched regardless of their casing."),
diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py
index dffd119da9..1d2dfe8cab 100644
--- a/authentik/stages/identification/stage.py
+++ b/authentik/stages/identification/stage.py
@@ -29,6 +29,7 @@ from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_
from authentik.lib.utils.reflection import all_subclasses
from authentik.lib.utils.urls import reverse_with_qs
from authentik.root.middleware import ClientIPMiddleware
+from authentik.stages.captcha.stage import CaptchaChallenge, verify_captcha_token
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate
@@ -75,6 +76,7 @@ class IdentificationChallenge(Challenge):
allow_show_password = BooleanField(default=False)
application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices)
+ captcha_stage = CaptchaChallenge(required=False)
enroll_url = CharField(required=False)
recovery_url = CharField(required=False)
@@ -91,14 +93,16 @@ class IdentificationChallengeResponse(ChallengeResponse):
uid_field = CharField()
password = CharField(required=False, allow_blank=True, allow_null=True)
+ captcha_token = CharField(required=False, allow_blank=True, allow_null=True)
component = CharField(default="ak-stage-identification")
pre_user: User | None = None
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
- """Validate that user exists, and optionally their password"""
+ """Validate that user exists, and optionally their password and captcha token"""
uid_field = attrs["uid_field"]
current_stage: IdentificationStage = self.stage.executor.current_stage
+ client_ip = ClientIPMiddleware.get_client_ip(self.stage.request)
pre_user = self.stage.get_user(uid_field)
if not pre_user:
@@ -113,7 +117,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
self.stage.logger.info(
"invalid_login",
identifier=uid_field,
- client_ip=ClientIPMiddleware.get_client_ip(self.stage.request),
+ client_ip=client_ip,
action="invalid_identifier",
context={
"stage": sanitize_item(self.stage),
@@ -136,6 +140,15 @@ class IdentificationChallengeResponse(ChallengeResponse):
return attrs
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
+
+ # Captcha check
+ if captcha_stage := current_stage.captcha_stage:
+ captcha_token = attrs.get("captcha_token", None)
+ if not captcha_token:
+ self.stage.logger.warning("Token not set for captcha attempt")
+ verify_captcha_token(captcha_stage, captcha_token, client_ip)
+
+ # Password check
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
@@ -206,6 +219,14 @@ class IdentificationStageView(ChallengeStageView):
"primary_action": self.get_primary_action(),
"user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
+ "captcha_stage": (
+ {
+ "js_url": current_stage.captcha_stage.js_url,
+ "site_key": current_stage.captcha_stage.public_key,
+ }
+ if current_stage.captcha_stage
+ else None
+ ),
"allow_show_password": bool(current_stage.password_stage)
and current_stage.password_stage.allow_show_password,
"show_source_labels": current_stage.show_source_labels,
diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py
index 57ffed1283..c39434e24a 100644
--- a/authentik/stages/identification/tests.py
+++ b/authentik/stages/identification/tests.py
@@ -1,6 +1,7 @@
"""identification tests"""
from django.urls import reverse
+from requests_mock import Mocker
from rest_framework.exceptions import ValidationError
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
@@ -8,6 +9,8 @@ from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.sources.oauth.models import OAuthSource
+from authentik.stages.captcha.models import CaptchaStage
+from authentik.stages.captcha.tests import RECAPTCHA_PRIVATE_KEY, RECAPTCHA_PUBLIC_KEY
from authentik.stages.identification.api import IdentificationStageSerializer
from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password import BACKEND_INBUILT
@@ -133,6 +136,135 @@ class TestIdentificationStage(FlowTestCase):
user_fields=["email"],
)
+ @Mocker()
+ def test_valid_with_captcha(self, mock: Mocker):
+ """Test with valid email and captcha token in single step"""
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": True,
+ "score": 0.5,
+ },
+ )
+
+ captcha_stage = CaptchaStage.objects.create(
+ name="captcha",
+ public_key=RECAPTCHA_PUBLIC_KEY,
+ private_key=RECAPTCHA_PRIVATE_KEY,
+ )
+ self.stage.captcha_stage = captcha_stage
+ self.stage.save()
+
+ form_data = {"uid_field": self.user.email, "captcha_token": "PASSED"}
+ url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+ response = self.client.post(url, form_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
+
+ @Mocker()
+ def test_invalid_with_captcha(self, mock: Mocker):
+ """Test with valid email and invalid captcha token in single step"""
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": False,
+ "score": 0.5,
+ },
+ )
+
+ captcha_stage = CaptchaStage.objects.create(
+ name="captcha",
+ public_key=RECAPTCHA_PUBLIC_KEY,
+ private_key=RECAPTCHA_PRIVATE_KEY,
+ )
+
+ self.stage.captcha_stage = captcha_stage
+ self.stage.save()
+
+ form_data = {
+ "uid_field": self.user.email,
+ "captcha_token": "FAILED",
+ }
+ url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+ response = self.client.post(url, form_data)
+ self.assertStageResponse(
+ response,
+ self.flow,
+ component="ak-stage-identification",
+ password_fields=False,
+ primary_action="Log in",
+ response_errors={
+ "non_field_errors": [{"code": "invalid", "string": "Invalid captcha response"}]
+ },
+ sources=[
+ {
+ "challenge": {
+ "component": "xak-flow-redirect",
+ "to": "/source/oauth/login/test/",
+ },
+ "icon_url": "/static/authentik/sources/default.svg",
+ "name": "test",
+ }
+ ],
+ show_source_labels=False,
+ user_fields=["email"],
+ )
+
+ @Mocker()
+ def test_invalid_with_captcha_retriable(self, mock: Mocker):
+ """Test with valid email and invalid captcha token in single step"""
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": False,
+ "score": 0.5,
+ "error-codes": ["timeout-or-duplicate"],
+ },
+ )
+
+ captcha_stage = CaptchaStage.objects.create(
+ name="captcha",
+ public_key=RECAPTCHA_PUBLIC_KEY,
+ private_key=RECAPTCHA_PRIVATE_KEY,
+ )
+
+ self.stage.captcha_stage = captcha_stage
+ self.stage.save()
+
+ form_data = {
+ "uid_field": self.user.email,
+ "captcha_token": "FAILED",
+ }
+ url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+ response = self.client.post(url, form_data)
+ self.assertStageResponse(
+ response,
+ self.flow,
+ component="ak-stage-identification",
+ password_fields=False,
+ primary_action="Log in",
+ response_errors={
+ "non_field_errors": [
+ {
+ "code": "invalid",
+ "string": "Invalid captcha response. Retrying may solve this issue.",
+ }
+ ]
+ },
+ sources=[
+ {
+ "challenge": {
+ "component": "xak-flow-redirect",
+ "to": "/source/oauth/login/test/",
+ },
+ "icon_url": "/static/authentik/sources/default.svg",
+ "name": "test",
+ }
+ ],
+ show_source_labels=False,
+ user_fields=["email"],
+ )
+
def test_invalid_with_username(self):
"""Test invalid with username (user exists but stage only allows email)"""
form_data = {"uid_field": self.user.username}
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 9b3b91eb74..0604c8d76d 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -10679,6 +10679,11 @@
"title": "Password stage",
"description": "When set, shows a password field, instead of showing the password field as separate step."
},
+ "captcha_stage": {
+ "type": "integer",
+ "title": "Captcha stage",
+ "description": "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage."
+ },
"case_insensitive_matching": {
"type": "boolean",
"title": "Case insensitive matching",
diff --git a/blueprints/system/sources-kerberos.yaml b/blueprints/system/sources-kerberos.yaml
index d97e8eda53..8664183b7e 100644
--- a/blueprints/system/sources-kerberos.yaml
+++ b/blueprints/system/sources-kerberos.yaml
@@ -38,7 +38,7 @@ entries:
name: "authentik default Kerberos User Mapping: Ignore system principals"
expression: |
localpart, realm = principal.rsplit("@", 1)
- denied_prefixes = ["kadmin/", "krbtgt/", "K/M", "WELLKNOWN/"]
+ denied_prefixes = ["kadmin/", "krbtgt/", "K/M", "WELLKNOWN/", "kiprop/", "changepw/"]
for prefix in denied_prefixes:
if localpart.lower().startswith(prefix.lower()):
raise SkipObject
diff --git a/go.mod b/go.mod
index 804d9d0dc5..3b36858dd0 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
- goauthentik.io/api/v3 v3.2024083.11
+ goauthentik.io/api/v3 v3.2024083.14
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.8.0
diff --git a/go.sum b/go.sum
index 7c008d78ad..fb4733c1dd 100644
--- a/go.sum
+++ b/go.sum
@@ -299,8 +299,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.2024083.11 h1:kF5WAnS0dB2cq9Uldqel8e8PDepJg/824JC3YFsQVHU=
-goauthentik.io/api/v3 v3.2024083.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
+goauthentik.io/api/v3 v3.2024083.14 h1:8iLXkNpVS275S4DLMBr6WIeaMkkaIJbzlNRLCFe+k3A=
+goauthentik.io/api/v3 v3.2024083.14/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=
diff --git a/internal/outpost/proxyv2/application/endpoint.go b/internal/outpost/proxyv2/application/endpoint.go
index 9a91459182..c9cc50d40c 100644
--- a/internal/outpost/proxyv2/application/endpoint.go
+++ b/internal/outpost/proxyv2/application/endpoint.go
@@ -82,6 +82,9 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
if embedded {
ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host)
ep.JwksUri = updateURL(jwksUri, newHost.Scheme, newHost.Host)
+ } else {
+ // Fixes: https://github.com/goauthentik/authentik/issues/9622 / ep.Issuer must be the HostBrowser URL
+ ep.Issuer = updateURL(ep.Issuer, newBrowserHost.Scheme, newBrowserHost.Host)
}
return ep
}
diff --git a/internal/outpost/proxyv2/application/endpoint_test.go b/internal/outpost/proxyv2/application/endpoint_test.go
index d3d0f74262..bd2be424d6 100644
--- a/internal/outpost/proxyv2/application/endpoint_test.go
+++ b/internal/outpost/proxyv2/application/endpoint_test.go
@@ -55,7 +55,7 @@ func TestEndpointAuthentikHostBrowser(t *testing.T) {
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL)
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
- assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer)
+ assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/", ep.Issuer)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
}
diff --git a/lifecycle/ak b/lifecycle/ak
index 2033951557..2d52023bb9 100755
--- a/lifecycle/ak
+++ b/lifecycle/ak
@@ -54,7 +54,9 @@ function cleanup {
}
function prepare_debug {
- apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get update
+ apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
touch /unittest.xml
chown authentik:authentik /unittest.xml
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 9477afafd1..9e941ce67e 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-23 16:39+0000\n"
+"POT-Creation-Date: 2024-10-28 00:09+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -2614,12 +2614,7 @@ msgid "Captcha Stages"
msgstr ""
#: authentik/stages/captcha/stage.py
-msgid "Unknown error"
-msgstr ""
-
-#: authentik/stages/captcha/stage.py
-#, python-brace-format
-msgid "Failed to validate token: {error}"
+msgid "Invalid captcha response. Retrying may solve this issue."
msgstr ""
#: authentik/stages/captcha/stage.py
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 243070be90..7662522a26 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-18 00:09+0000\n"
+"POT-Creation-Date: 2024-10-23 16:39+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2024\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@@ -587,6 +587,30 @@ msgstr "Limite maximum de connection atteinte."
msgid "(You are already connected in another tab/window)"
msgstr "(Vous êtes déjà connecté dans un autre onglet/une autre fenêtre)"
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stage"
+msgstr ""
+"Étape d'authentificateur d'appareil du connecteur de confiance des appareils"
+" Google"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stages"
+msgstr ""
+"Étapes d'authentificateur d'appareil du connecteur de confiance des "
+"appareils Google"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Device"
+msgstr "Appareil point de terminaison"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Devices"
+msgstr "Appareils point de terminaison"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py
+msgid "Verifying your browser..."
+msgstr "Vérification de votre navigateur..."
+
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@@ -2029,6 +2053,125 @@ msgstr ""
msgid "Used recovery-link to authenticate."
msgstr "Utiliser un lien de récupération pour se connecter."
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos realm"
+msgstr "Realm Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Custom krb5.conf to use. Uses the system one by default"
+msgstr ""
+"krb5.conf personnalisé à utiliser. Utilise celui du système par défault"
+
+#: authentik/sources/kerberos/models.py
+msgid "Sync users from Kerberos into authentik"
+msgstr "Synchroniser les utilisateurs Kerberos dans authentik"
+
+#: authentik/sources/kerberos/models.py
+msgid "When a user changes their password, sync it back to Kerberos"
+msgstr ""
+"Lorsqu'un utilisateur change son mot de passe, le synchroniser à nouveau "
+"vers Kerberos."
+
+#: authentik/sources/kerberos/models.py
+msgid "Principal to authenticate to kadmin for sync."
+msgstr "Principal pour s'authentifier à kadmin pour la synchronisation."
+
+#: authentik/sources/kerberos/models.py
+msgid "Password to authenticate to kadmin for sync"
+msgstr "Mot de passe pour s'authentifier à kadmin pour la synchronisation."
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the "
+"form TYPE:residual"
+msgstr ""
+"Keytab pour s'authentifier à kadmin pour la synchronisation. Doit être "
+"encodé en base64 ou de la forme TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Credentials cache to authenticate to kadmin for sync. Must be in the form "
+"TYPE:residual"
+msgstr ""
+"Credentials cache pour s'authentifier à kadmin pour la synchronisation. Doit"
+" être de la forme TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Force the use of a specific server name for SPNEGO. Must be in the form "
+"HTTP@hostname"
+msgstr ""
+"Force l'utilisation d'un nom de serveur spécifique pour SPNEGO. Doit être de"
+" la forme HTTP@hostname"
+
+#: authentik/sources/kerberos/models.py
+msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
+msgstr ""
+"Keytab SPNEGO encodée en base64 ou chemin vers la keytab de la forme "
+"FILE:path"
+
+#: authentik/sources/kerberos/models.py
+msgid "Credential cache to use for SPNEGO in form type:residual"
+msgstr "Credentials cache pour SPNEGO de la forme TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"If enabled, the authentik-stored password will be updated upon login with "
+"the Kerberos password backend"
+msgstr ""
+"Si activé, le mot de passe stocké par authentik sera mis à jour à la "
+"connexion avec le backend de mot de passe Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source"
+msgstr "Source Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Sources"
+msgstr "Sources Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mapping"
+msgstr "Mappage de propriété source Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mappings"
+msgstr "Mappages de propriété source Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connection"
+msgstr "Connexion de l'utilisateur à la source Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connections"
+msgstr "Connexions de l'utilisateur à la source Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connection"
+msgstr "Connexion du groupe à la source Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connections"
+msgstr "Connexions du groupe à la source Kerberos"
+
+#: authentik/sources/kerberos/views.py
+msgid "SPNEGO authentication required"
+msgstr "Authentification SPNEGO requise"
+
+#: authentik/sources/kerberos/views.py
+msgid ""
+"\n"
+" Make sure you have valid tickets (obtainable via kinit)\n"
+" and configured the browser correctly.\n"
+" Please contact your administrator.\n"
+" "
+msgstr ""
+"\n"
+" Vérifiez que vous avez des tickets valides (qu'on peut obtenir via kinit)\n"
+" et que le navigateur est configuré correctement.\n"
+" Veuillez contacter votre administrateur.\n"
+" "
+
#: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr ""
@@ -3121,6 +3264,10 @@ msgstr "Base de données utilisateurs + mots de passes applicatifs"
msgid "User database + LDAP password"
msgstr "Base de données utilisateurs + mot de passe LDAP"
+#: authentik/stages/password/models.py
+msgid "User database + Kerberos password"
+msgstr "Base de données utilisateurs + mot de passe Kerberos"
+
#: authentik/stages/password/models.py
msgid "Selection of backends to test the password against."
msgstr "Sélection de backends pour tester le mot de passe."
diff --git a/locale/it/LC_MESSAGES/django.po b/locale/it/LC_MESSAGES/django.po
index 7d0ad0097d..000198dbae 100644
--- a/locale/it/LC_MESSAGES/django.po
+++ b/locale/it/LC_MESSAGES/django.po
@@ -11,15 +11,17 @@
# Marco Vitale, 2024
# Kowalski Dragon (kowalski7cc) , 2024
# albanobattistella , 2024
+# Nicola Mersi, 2024
+# tom max, 2024
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-18 00:09+0000\n"
+"POT-Creation-Date: 2024-10-28 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
-"Last-Translator: albanobattistella , 2024\n"
+"Last-Translator: tom max, 2024\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -583,6 +585,28 @@ msgstr "Limite massimo di connessioni raggiunto."
msgid "(You are already connected in another tab/window)"
msgstr "(Sei già connesso in un'altra scheda/finestra)"
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stage"
+msgstr ""
+"Fase di autenticazione per la verifica dispositivo Google tramite endpoint"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stages"
+msgstr ""
+"Fasi di autenticazione per la verifica dispositivo Google tramite endpoint"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Device"
+msgstr "Dispositivo di Accesso"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Devices"
+msgstr "Dispositivi di Accesso"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py
+msgid "Verifying your browser..."
+msgstr "Verifica del tuo browser..."
+
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@@ -2017,6 +2041,124 @@ msgstr ""
msgid "Used recovery-link to authenticate."
msgstr "Utilizzato il link di recupero per autenticarsi."
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos realm"
+msgstr "Dominio Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Custom krb5.conf to use. Uses the system one by default"
+msgstr ""
+"krb5.conf personalizzato da usare. Usa la configurazione di sistema per "
+"default"
+
+#: authentik/sources/kerberos/models.py
+msgid "Sync users from Kerberos into authentik"
+msgstr "Sincronizza utenti da Kerberos a authentik"
+
+#: authentik/sources/kerberos/models.py
+msgid "When a user changes their password, sync it back to Kerberos"
+msgstr "Quando un utente cambia la sua password, sincronizzala in Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Principal to authenticate to kadmin for sync."
+msgstr "Entità da autenticare su kadmin per la sincronizzazione."
+
+#: authentik/sources/kerberos/models.py
+msgid "Password to authenticate to kadmin for sync"
+msgstr "Password per autenticarsi in kadmin per sincronizzare"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the "
+"form TYPE:residual"
+msgstr ""
+"Keytab per autenticarsi su kadmin per la sincronizzazione. Deve essere con "
+"codifica base64 o nel formato TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Credentials cache to authenticate to kadmin for sync. Must be in the form "
+"TYPE:residual"
+msgstr ""
+"Credenziali memorizzate nella cache per autenticarsi su kadmin per la "
+"sincronizzazione. Devono essere nel formato TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Force the use of a specific server name for SPNEGO. Must be in the form "
+"HTTP@hostname"
+msgstr ""
+"Forza l'uso di un nome server specifico per SPNEGO. Deve essere nel formato "
+"HTTP@nomehost"
+
+#: authentik/sources/kerberos/models.py
+msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
+msgstr ""
+"keytab SPNEGO con codifica base64 o percorso del keytab nel formato "
+"FILE:percorso"
+
+#: authentik/sources/kerberos/models.py
+msgid "Credential cache to use for SPNEGO in form type:residual"
+msgstr ""
+"Cache delle credenziali da utilizzare per SPNEGO nella forma type:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"If enabled, the authentik-stored password will be updated upon login with "
+"the Kerberos password backend"
+msgstr ""
+"Se abilitato, la password memorizzata in authentik verrà aggiornata al login"
+" nel backend Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source"
+msgstr "Sorgente Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Sources"
+msgstr "Sorgenti Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mapping"
+msgstr "Mappa delle proprietà della sorgente kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mappings"
+msgstr "Mappe delle proprietà della sorgente kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connection"
+msgstr "Connessione sorgente dell'utente kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connections"
+msgstr " Connessioni alle sorgente dell'utente kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connection"
+msgstr " Connessione sorgente del gruppo kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connections"
+msgstr "Connessioni alle sorgenti del gruppo kerberos"
+
+#: authentik/sources/kerberos/views.py
+msgid "SPNEGO authentication required"
+msgstr "autenticazione SPNEGO necessaria"
+
+#: authentik/sources/kerberos/views.py
+msgid ""
+"\n"
+" Make sure you have valid tickets (obtainable via kinit)\n"
+" and configured the browser correctly.\n"
+" Please contact your administrator.\n"
+" "
+msgstr ""
+"\n"
+"Assicurati di avere un ticket valido (ottenibile tramite kinit)\n"
+" e di aver configurato correttamente il browser. \n"
+"Contatta il tuo amministratore."
+
#: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr ""
@@ -2735,13 +2877,10 @@ msgid "Captcha Stages"
msgstr "Fasi Captcha"
#: authentik/stages/captcha/stage.py
-msgid "Unknown error"
-msgstr "Errore sconosciuto"
-
-#: authentik/stages/captcha/stage.py
-#, python-brace-format
-msgid "Failed to validate token: {error}"
-msgstr "Impossibile convalidare il token: {error}"
+msgid "Invalid captcha response. Retrying may solve this issue."
+msgstr ""
+"Risposta captcha non valida. Un nuovo tentativo potrebbe risolvere il "
+"problema."
#: authentik/stages/captcha/stage.py
msgid "Invalid captcha response"
@@ -3114,6 +3253,10 @@ msgstr "Database utente + password app"
msgid "User database + LDAP password"
msgstr "Database utenti + password LDAP"
+#: authentik/stages/password/models.py
+msgid "User database + Kerberos password"
+msgstr "Database utenti + password Kerberos"
+
#: authentik/stages/password/models.py
msgid "Selection of backends to test the password against."
msgstr "Selezione di backend su cui testare la password."
diff --git a/locale/zh-Hans/LC_MESSAGES/django.mo b/locale/zh-Hans/LC_MESSAGES/django.mo
index 3377f5d239..05a66744b3 100644
Binary files a/locale/zh-Hans/LC_MESSAGES/django.mo and b/locale/zh-Hans/LC_MESSAGES/django.mo differ
diff --git a/locale/zh-Hans/LC_MESSAGES/django.po b/locale/zh-Hans/LC_MESSAGES/django.po
index d2b79c466b..947dab8ebd 100644
--- a/locale/zh-Hans/LC_MESSAGES/django.po
+++ b/locale/zh-Hans/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-18 00:09+0000\n"
+"POT-Creation-Date: 2024-10-28 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@@ -540,6 +540,26 @@ msgstr "已达到最大连接数。"
msgid "(You are already connected in another tab/window)"
msgstr "(您已经在另一个标签页/窗口连接了)"
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stage"
+msgstr "端点身份验证器 Google 设备信任连接器阶段"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stages"
+msgstr "端点身份验证器 Google 设备信任连接器阶段"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Device"
+msgstr "端点设备"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Devices"
+msgstr "端点设备"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py
+msgid "Verifying your browser..."
+msgstr "正在验证您的浏览器…"
+
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@@ -1848,6 +1868,112 @@ msgstr "创建一个密钥,可用于恢复对 authentik 的访问权限。"
msgid "Used recovery-link to authenticate."
msgstr "已使用恢复链接进行身份验证。"
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos realm"
+msgstr "Kerberos 领域"
+
+#: authentik/sources/kerberos/models.py
+msgid "Custom krb5.conf to use. Uses the system one by default"
+msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
+
+#: authentik/sources/kerberos/models.py
+msgid "Sync users from Kerberos into authentik"
+msgstr "从 Kerberos 同步用户到 authentik"
+
+#: authentik/sources/kerberos/models.py
+msgid "When a user changes their password, sync it back to Kerberos"
+msgstr "当用户修改密码时,将其同步回 Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Principal to authenticate to kadmin for sync."
+msgstr "向 kadmin 进行身份验证以进行同步的主体。"
+
+#: authentik/sources/kerberos/models.py
+msgid "Password to authenticate to kadmin for sync"
+msgstr "向 kadmin 进行身份验证以进行同步的密码"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the "
+"form TYPE:residual"
+msgstr "向 kadmin 进行身份验证以进行同步的 Keytab。必须以 Base64 编码,或者形式为 TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Credentials cache to authenticate to kadmin for sync. Must be in the form "
+"TYPE:residual"
+msgstr "向 kadmin 进行身份验证以进行同步的凭据缓存。形式必须为 TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Force the use of a specific server name for SPNEGO. Must be in the form "
+"HTTP@hostname"
+msgstr "强制为 SPNEGO 使用特定服务器名称。形式必须为 HTTP@主机名"
+
+#: authentik/sources/kerberos/models.py
+msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
+msgstr "以 Base64 编码的 SPNEGO Keytab 或 FILE:path 形式的 Keytab 路径"
+
+#: authentik/sources/kerberos/models.py
+msgid "Credential cache to use for SPNEGO in form type:residual"
+msgstr "SPNEGO 使用的凭据缓存,形式为 type:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"If enabled, the authentik-stored password will be updated upon login with "
+"the Kerberos password backend"
+msgstr "启用时,authentik 存储的密码将会在使用 Kerberos 密码后端登录时更新"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source"
+msgstr "Kerberos 源"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Sources"
+msgstr "Kerberos 源"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mapping"
+msgstr "Kerberos 源属性映射"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mappings"
+msgstr "Kerberos 源属性映射"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connection"
+msgstr "用户 Kerberos 源连接"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connections"
+msgstr "用户 Kerberos 源连接"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connection"
+msgstr "组 Kerberos 源连接"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connections"
+msgstr "组 Kerberos 源连接"
+
+#: authentik/sources/kerberos/views.py
+msgid "SPNEGO authentication required"
+msgstr "需要 SPNEGO 身份验证"
+
+#: authentik/sources/kerberos/views.py
+msgid ""
+"\n"
+" Make sure you have valid tickets (obtainable via kinit)\n"
+" and configured the browser correctly.\n"
+" Please contact your administrator.\n"
+" "
+msgstr ""
+"\n"
+" 请确认您拥有有效票据(通过 kinit 获得)\n"
+" 并且已正确配置浏览器。\n"
+" 请联系您的管理员。\n"
+" "
+
#: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr "仅允许使用密码同步的单个 LDAP 源"
@@ -2523,13 +2649,8 @@ msgid "Captcha Stages"
msgstr "验证码阶段"
#: authentik/stages/captcha/stage.py
-msgid "Unknown error"
-msgstr "未知错误"
-
-#: authentik/stages/captcha/stage.py
-#, python-brace-format
-msgid "Failed to validate token: {error}"
-msgstr "验证令牌失败:{error}"
+msgid "Invalid captcha response. Retrying may solve this issue."
+msgstr "无效的验证码响应。重试可能会解决此问题。"
#: authentik/stages/captcha/stage.py
msgid "Invalid captcha response"
@@ -2876,6 +2997,10 @@ msgstr "用户数据库 + 应用程序密码"
msgid "User database + LDAP password"
msgstr "用户数据库 + LDAP 密码"
+#: authentik/stages/password/models.py
+msgid "User database + Kerberos password"
+msgstr "用户数据库 + Kerberos 密码"
+
#: authentik/stages/password/models.py
msgid "Selection of backends to test the password against."
msgstr "选择用于测试密码的后端。"
diff --git a/locale/zh_CN/LC_MESSAGES/django.po b/locale/zh_CN/LC_MESSAGES/django.po
index f3a6d452c4..edd40cbf85 100644
--- a/locale/zh_CN/LC_MESSAGES/django.po
+++ b/locale/zh_CN/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-18 00:09+0000\n"
+"POT-Creation-Date: 2024-10-28 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@@ -539,6 +539,26 @@ msgstr "已达到最大连接数。"
msgid "(You are already connected in another tab/window)"
msgstr "(您已经在另一个标签页/窗口连接了)"
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stage"
+msgstr "端点身份验证器 Google 设备信任连接器阶段"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Authenticator Google Device Trust Connector Stages"
+msgstr "端点身份验证器 Google 设备信任连接器阶段"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Device"
+msgstr "端点设备"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
+msgid "Endpoint Devices"
+msgstr "端点设备"
+
+#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py
+msgid "Verifying your browser..."
+msgstr "正在验证您的浏览器…"
+
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@@ -1847,6 +1867,112 @@ msgstr "创建一个密钥,可用于恢复对 authentik 的访问权限。"
msgid "Used recovery-link to authenticate."
msgstr "已使用恢复链接进行身份验证。"
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos realm"
+msgstr "Kerberos 领域"
+
+#: authentik/sources/kerberos/models.py
+msgid "Custom krb5.conf to use. Uses the system one by default"
+msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
+
+#: authentik/sources/kerberos/models.py
+msgid "Sync users from Kerberos into authentik"
+msgstr "从 Kerberos 同步用户到 authentik"
+
+#: authentik/sources/kerberos/models.py
+msgid "When a user changes their password, sync it back to Kerberos"
+msgstr "当用户修改密码时,将其同步回 Kerberos"
+
+#: authentik/sources/kerberos/models.py
+msgid "Principal to authenticate to kadmin for sync."
+msgstr "向 kadmin 进行身份验证以进行同步的主体。"
+
+#: authentik/sources/kerberos/models.py
+msgid "Password to authenticate to kadmin for sync"
+msgstr "向 kadmin 进行身份验证以进行同步的密码"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the "
+"form TYPE:residual"
+msgstr "向 kadmin 进行身份验证以进行同步的 Keytab。必须以 Base64 编码,或者形式为 TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Credentials cache to authenticate to kadmin for sync. Must be in the form "
+"TYPE:residual"
+msgstr "向 kadmin 进行身份验证以进行同步的凭据缓存。形式必须为 TYPE:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"Force the use of a specific server name for SPNEGO. Must be in the form "
+"HTTP@hostname"
+msgstr "强制为 SPNEGO 使用特定服务器名称。形式必须为 HTTP@主机名"
+
+#: authentik/sources/kerberos/models.py
+msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
+msgstr "以 Base64 编码的 SPNEGO Keytab 或 FILE:path 形式的 Keytab 路径"
+
+#: authentik/sources/kerberos/models.py
+msgid "Credential cache to use for SPNEGO in form type:residual"
+msgstr "SPNEGO 使用的凭据缓存,形式为 type:residual"
+
+#: authentik/sources/kerberos/models.py
+msgid ""
+"If enabled, the authentik-stored password will be updated upon login with "
+"the Kerberos password backend"
+msgstr "启用时,authentik 存储的密码将会在使用 Kerberos 密码后端登录时更新"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source"
+msgstr "Kerberos 源"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Sources"
+msgstr "Kerberos 源"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mapping"
+msgstr "Kerberos 源属性映射"
+
+#: authentik/sources/kerberos/models.py
+msgid "Kerberos Source Property Mappings"
+msgstr "Kerberos 源属性映射"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connection"
+msgstr "用户 Kerberos 源连接"
+
+#: authentik/sources/kerberos/models.py
+msgid "User Kerberos Source Connections"
+msgstr "用户 Kerberos 源连接"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connection"
+msgstr "组 Kerberos 源连接"
+
+#: authentik/sources/kerberos/models.py
+msgid "Group Kerberos Source Connections"
+msgstr "组 Kerberos 源连接"
+
+#: authentik/sources/kerberos/views.py
+msgid "SPNEGO authentication required"
+msgstr "需要 SPNEGO 身份验证"
+
+#: authentik/sources/kerberos/views.py
+msgid ""
+"\n"
+" Make sure you have valid tickets (obtainable via kinit)\n"
+" and configured the browser correctly.\n"
+" Please contact your administrator.\n"
+" "
+msgstr ""
+"\n"
+" 请确认您拥有有效票据(通过 kinit 获得)\n"
+" 并且已正确配置浏览器。\n"
+" 请联系您的管理员。\n"
+" "
+
#: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr "仅允许使用密码同步的单个 LDAP 源"
@@ -2522,13 +2648,8 @@ msgid "Captcha Stages"
msgstr "验证码阶段"
#: authentik/stages/captcha/stage.py
-msgid "Unknown error"
-msgstr "未知错误"
-
-#: authentik/stages/captcha/stage.py
-#, python-brace-format
-msgid "Failed to validate token: {error}"
-msgstr "验证令牌失败:{error}"
+msgid "Invalid captcha response. Retrying may solve this issue."
+msgstr "无效的验证码响应。重试可能会解决此问题。"
#: authentik/stages/captcha/stage.py
msgid "Invalid captcha response"
@@ -2875,6 +2996,10 @@ msgstr "用户数据库 + 应用程序密码"
msgid "User database + LDAP password"
msgstr "用户数据库 + LDAP 密码"
+#: authentik/stages/password/models.py
+msgid "User database + Kerberos password"
+msgstr "用户数据库 + Kerberos 密码"
+
#: authentik/stages/password/models.py
msgid "Selection of backends to test the password against."
msgstr "选择用于测试密码的后端。"
diff --git a/poetry.lock b/poetry.lock
index 477f4034d8..9fde46832f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1849,35 +1849,36 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
[[package]]
name = "gssapi"
-version = "1.8.3"
+version = "1.9.0"
description = "Python GSSAPI Wrapper"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "gssapi-1.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e4a83e9b275fe69b5d40be6d5479889866b80333a12c51a9243f2712d4f0554"},
- {file = "gssapi-1.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d57d67547e18f4e44a688bfb20abbf176d1b8df547da2b31c3f2df03cfdc269"},
- {file = "gssapi-1.8.3-cp310-cp310-win32.whl", hash = "sha256:3a3f63105f39c4af29ffc8f7b6542053d87fe9d63010c689dd9a9f5571facb8e"},
- {file = "gssapi-1.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:b031c0f186ab4275186da385b2c7470dd47c9b27522cb3b753757c9ac4bebf11"},
- {file = "gssapi-1.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b03d6b30f1fcd66d9a688b45a97e302e4dd3f1386d5c333442731aec73cdb409"},
- {file = "gssapi-1.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca6ceb17fc15eda2a69f2e8c6cf10d11e2edb32832255e5d4c65b21b6db4680a"},
- {file = "gssapi-1.8.3-cp311-cp311-win32.whl", hash = "sha256:edc8ef3a9e397dbe18bb6016f8e2209969677b534316d20bb139da2865a38efe"},
- {file = "gssapi-1.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8fdb1ff130cee49bc865ec1624dee8cf445cd6c6e93b04bffef2c6f363a60cb9"},
- {file = "gssapi-1.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:19c373b3ba63ce19cd3163aa1495635e3d01b0de6cc4ff1126095eded1df6e01"},
- {file = "gssapi-1.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f1a8046d695f2c9b8d640a6e385780d3945c0741571ed6fee6f94c31e431dc"},
- {file = "gssapi-1.8.3-cp312-cp312-win32.whl", hash = "sha256:338db18612e3e6ed64e92b6d849242a535fdc98b365f21122992fb8cae737617"},
- {file = "gssapi-1.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5731c5b40ecc3116cfe7fb7e1d1e128583ec8b3df1e68bf8cd12073160793acd"},
- {file = "gssapi-1.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e556878da197ad115a566d36e46a8082d0079731d9c24d1ace795132d725ff2a"},
- {file = "gssapi-1.8.3-cp37-cp37m-win32.whl", hash = "sha256:e2bb081f2db2111377effe7d40ba23f9a87359b9d2f4881552b731e9da88b36b"},
- {file = "gssapi-1.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4d9ed83f2064cda60aad90e6840ae282096801b2c814b8cbd390bf0df4635aab"},
- {file = "gssapi-1.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d91fe6e2a5c89b32102ea8e374b8ae13b9031d43d7b55f3abc1f194ddce820d"},
- {file = "gssapi-1.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5b28237afc0668046934792756dd4b6b7e957b0d95a608d02f296734a2819ad"},
- {file = "gssapi-1.8.3-cp38-cp38-win32.whl", hash = "sha256:791e44f7bea602b8e3da1ec56fbdb383b8ee3326fdeb736f904c2aa9af13a67d"},
- {file = "gssapi-1.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:5b4bf84d0a6d7779a4bf11dacfd3db57ae02dd53562e2aeadac4219a68eaee07"},
- {file = "gssapi-1.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e40efc88ccefefd6142f8c47b8af498731938958b808bad49990442a91f45160"},
- {file = "gssapi-1.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee74b9211c977b9181ff4652d886d7712c9a221560752a35393b58e5ea07887a"},
- {file = "gssapi-1.8.3-cp39-cp39-win32.whl", hash = "sha256:465c6788f2ac6ef7c738394ba8fde1ede6004e5721766f386add63891d8c90af"},
- {file = "gssapi-1.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:8fb8ee70458f47b51ed881a6881f30b187c987c02af16cc0fff0079255d4d465"},
- {file = "gssapi-1.8.3.tar.gz", hash = "sha256:aa3c8d0b1526f52559552bb2c9d2d6be013d76a8e5db00b39a1db5727e93b0b0"},
+ {file = "gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1"},
+ {file = "gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6"},
+ {file = "gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e"},
+ {file = "gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962"},
+ {file = "gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560"},
+ {file = "gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd"},
+ {file = "gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe"},
+ {file = "gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f"},
+ {file = "gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f"},
+ {file = "gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f"},
+ {file = "gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec"},
+ {file = "gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76"},
+ {file = "gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c"},
+ {file = "gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e"},
+ {file = "gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28"},
+ {file = "gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9"},
+ {file = "gssapi-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0e378d62b2fc352ca0046030cda5911d808a965200f612fdd1d74501b83e98f"},
+ {file = "gssapi-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b74031c70864d04864b7406c818f41be0c1637906fb9654b06823bcc79f151dc"},
+ {file = "gssapi-1.9.0-cp38-cp38-win32.whl", hash = "sha256:f2f3a46784d8127cc7ef10d3367dedcbe82899ea296710378ccc9b7cefe96f4c"},
+ {file = "gssapi-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:a81f30cde21031e7b1f8194a3eea7285e39e551265e7744edafd06eadc1c95bc"},
+ {file = "gssapi-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbc93fdadd5aab9bae594538b2128044b8c5cdd1424fe015a465d8a8a587411a"},
+ {file = "gssapi-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b2a3c0a9beb895942d4b8e31f515e52c17026e55aeaa81ee0df9bbfdac76098"},
+ {file = "gssapi-1.9.0-cp39-cp39-win32.whl", hash = "sha256:060b58b455d29ab8aca74770e667dca746264bee660ac5b6a7a17476edc2c0b8"},
+ {file = "gssapi-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:11c9fe066edb0fa0785697eb0cecf2719c7ad1d9f2bf27be57b647a617bcfaa5"},
+ {file = "gssapi-1.9.0.tar.gz", hash = "sha256:f468fac8f3f5fca8f4d1ca19e3cd4d2e10bd91074e7285464b22715d13548afe"},
]
[package.dependencies]
@@ -3895,13 +3896,13 @@ pytest = ">=4.0.0"
[[package]]
name = "pytest-randomly"
-version = "3.15.0"
+version = "3.16.0"
description = "Pytest plugin to randomly order tests and control random.seed."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"},
- {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"},
+ {file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"},
+ {file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"},
]
[package.dependencies]
@@ -4292,29 +4293,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
-version = "0.7.0"
+version = "0.7.1"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"},
- {file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"},
- {file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"},
- {file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"},
- {file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"},
- {file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"},
- {file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"},
- {file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"},
- {file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"},
- {file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"},
- {file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"},
- {file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"},
+ {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
+ {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
+ {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
+ {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
+ {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
+ {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
+ {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
+ {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
+ {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
+ {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
+ {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
+ {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
]
[[package]]
@@ -4424,13 +4425,13 @@ tornado = ["tornado (>=6)"]
[[package]]
name = "service-identity"
-version = "24.1.0"
+version = "24.2.0"
description = "Service identity verification for pyOpenSSL & cryptography."
optional = false
python-versions = ">=3.8"
files = [
- {file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"},
- {file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"},
+ {file = "service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85"},
+ {file = "service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09"},
]
[package.dependencies]
@@ -4440,7 +4441,7 @@ pyasn1 = "*"
pyasn1-modules = "*"
[package.extras]
-dev = ["pyopenssl", "service-identity[idna,mypy,tests]"]
+dev = ["coverage[toml] (>=5.0.2)", "idna", "mypy", "pyopenssl", "pytest", "types-pyopenssl"]
docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"]
idna = ["idna"]
mypy = ["idna", "mypy", "types-pyopenssl"]
@@ -4750,13 +4751,13 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
-version = "9.3.4"
+version = "9.3.6"
description = "Twilio API client and TwiML generator"
optional = false
python-versions = ">=3.7.0"
files = [
- {file = "twilio-9.3.4-py2.py3-none-any.whl", hash = "sha256:2cae99f0f7aecbd9da02fa59ad8f11b360db4a9281fc3fb3237ad50be21d8a9b"},
- {file = "twilio-9.3.4.tar.gz", hash = "sha256:38a6ab04752f44313dcf736eae45236a901528d3f53dfc21d3afd33539243c7f"},
+ {file = "twilio-9.3.6-py2.py3-none-any.whl", hash = "sha256:c5d7f4cfeb50a7928397b8f819c8f7fb2bb956a1a2cabbda1df1d7a40f9ce1d7"},
+ {file = "twilio-9.3.6.tar.gz", hash = "sha256:d42691f7fe1faaa5ba82942f169bfea4d7f01a0a542a456d82018fb49bd1f5b2"},
]
[package.dependencies]
diff --git a/schema.yml b/schema.yml
index d4f3eb78ac..c920d1ab11 100644
--- a/schema.yml
+++ b/schema.yml
@@ -33862,6 +33862,11 @@ paths:
operationId: stages_identification_list
description: IdentificationStage Viewset
parameters:
+ - in: query
+ name: captcha_stage
+ schema:
+ type: string
+ format: uuid
- in: query
name: case_insensitive_matching
schema:
@@ -40204,10 +40209,15 @@ components:
challenge:
type: object
additionalProperties: {}
+ last_used:
+ type: string
+ format: date-time
+ nullable: true
required:
- challenge
- device_class
- device_uid
+ - last_used
DeviceChallengeRequest:
type: object
description: Single device challenge
@@ -40221,10 +40231,15 @@ components:
challenge:
type: object
additionalProperties: {}
+ last_used:
+ type: string
+ format: date-time
+ nullable: true
required:
- challenge
- device_class
- device_uid
+ - last_used
DeviceClassesEnum:
enum:
- static
@@ -42494,6 +42509,8 @@ components:
type: string
flow_designation:
$ref: '#/components/schemas/FlowDesignationEnum'
+ captcha_stage:
+ $ref: '#/components/schemas/CaptchaChallenge'
enroll_url:
type: string
recovery_url:
@@ -42528,6 +42545,9 @@ components:
password:
type: string
nullable: true
+ captcha_token:
+ type: string
+ nullable: true
required:
- uid_field
IdentificationStage:
@@ -42573,6 +42593,12 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
+ captcha_stage:
+ type: string
+ format: uuid
+ nullable: true
+ description: When set, adds functionality exactly like a Captcha stage,
+ but baked into the Identification stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -42641,6 +42667,12 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
+ captcha_stage:
+ type: string
+ format: uuid
+ nullable: true
+ description: When set, adds functionality exactly like a Captcha stage,
+ but baked into the Identification stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -48231,6 +48263,12 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
+ captcha_stage:
+ type: string
+ format: uuid
+ nullable: true
+ description: When set, adds functionality exactly like a Captcha stage,
+ but baked into the Identification stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
diff --git a/web/package-lock.json b/web/package-lock.json
index cd7081bf4b..7e3799333c 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
- "@goauthentik/api": "^2024.8.3-1729699127",
+ "@goauthentik/api": "^2024.8.3-1729836831",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@@ -1775,9 +1775,9 @@
}
},
"node_modules/@goauthentik/api": {
- "version": "2024.8.3-1729699127",
- "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729699127.tgz",
- "integrity": "sha512-luo0SAASR6BTTtLszDgfdwofBejv4F3hCHgPxeSoTSFgE8/A2+zJD8EtWPZaa1udDkwPa9lbIeJSSmbgFke3jA=="
+ "version": "2024.8.3-1729836831",
+ "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729836831.tgz",
+ "integrity": "sha512-nOgvjYQiK+HhWuiZ635h/aSsq7Mfj5cDrIyBJt+IJRQuJFtnnHx8nscRXKK/8sBl9obH2zMCoZgeqytK8145bg=="
},
"node_modules/@goauthentik/web": {
"resolved": "",
diff --git a/web/package.json b/web/package.json
index 26a2b15b2b..1929e4fdb7 100644
--- a/web/package.json
+++ b/web/package.json
@@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
- "@goauthentik/api": "^2024.8.3-1729699127",
+ "@goauthentik/api": "^2024.8.3-1729836831",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
diff --git a/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts b/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts
index 75b392f714..aa1bbfbf5a 100644
--- a/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts
+++ b/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts
@@ -97,7 +97,7 @@ export class ApplicationWizardApplicationDetails extends WithBrandConfig(BasePro
@@ -150,35 +149,36 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
${msg("Advanced flow settings")}
-
-
-
- ${msg(
- "Flow used when a user access this provider and is not authenticated.",
- )}
-
-
-
-
+
+
+
+ ${msg(
+ "Flow used when a user access this provider and is not authenticated.",
+ )}
+
+
+
-
- ${msg("Flow used when logging out of this provider.")}
-
-
+ >
+
+
+ ${msg("Flow used when logging out of this provider.")}
+
+
diff --git a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts
index e6d66aea6f..867efbd0b3 100644
--- a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts
+++ b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts
@@ -161,11 +161,9 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
@@ -184,35 +182,36 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
${msg("Advanced flow settings")}
-
-
-
- ${msg(
- "Flow used when a user access this provider and is not authenticated.",
- )}
-
-
-
-
+
+
+
+ ${msg(
+ "Flow used when a user access this provider and is not authenticated.",
+ )}
+
+
+
-
- ${msg("Flow used when logging out of this provider.")}
-
-
+ >
+
+
+ ${msg("Flow used when logging out of this provider.")}
+
+
diff --git a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts
index 61c1f6403d..54cbe258ca 100644
--- a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts
+++ b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts
@@ -146,36 +146,37 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
- ${msg("Advanced flow settings")}
-
-
-
- ${msg(
- "Flow used when a user access this provider and is not authenticated.",
- )}
-
-
-
- ${msg("Advanced flow settings")}
+
@@ -199,60 +200,52 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
)}
- ${
- this.hasSigningKp
- ? html`
-
+
+ ${msg(
+ "When enabled, the assertion element of the SAML response will be signed.",
+ )}
+
+ `
+ : nothing}
- ${msg("RAC is in preview.")}
- ${msg("Send us feedback!")}
-
- ${this.provider?.assignedApplicationName
+ return html`${this.provider?.assignedApplicationName
? html``
: html`
${msg("Warning: Provider is not used by an Application.")}
diff --git a/web/src/admin/rbac/ObjectPermissionModal.ts b/web/src/admin/rbac/ObjectPermissionModal.ts
index 87892befe7..745596e038 100644
--- a/web/src/admin/rbac/ObjectPermissionModal.ts
+++ b/web/src/admin/rbac/ObjectPermissionModal.ts
@@ -7,7 +7,6 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
-import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -53,17 +52,13 @@ export class ObjectPermissionModal extends AKElement {
objectPk?: string | number;
static get styles(): CSSResult[] {
- return [PFBase, PFButton, PFBanner];
+ return [PFBase, PFButton];
}
render(): TemplateResult {
return html`
${msg("Update Permissions")}
-
- ${msg("RBAC is in preview.")}
- ${msg("Send us feedback!")}
- `
+ return html`
+ ${this.model === RbacPermissionsAssignedByUsersListModelEnum.CoreUser
+ ? this.renderCoreUser()
: nothing}
-
- ${this.model === RbacPermissionsAssignedByUsersListModelEnum.CoreUser
- ? this.renderCoreUser()
- : nothing}
- ${this.model === RbacPermissionsAssignedByUsersListModelEnum.RbacRole
- ? this.renderRbacRole()
- : nothing}
-
-
-
-
${msg("User Object Permissions")}
-
- ${msg("Permissions set on users which affect this object.")}
-
-
+ ${this.model === RbacPermissionsAssignedByUsersListModelEnum.RbacRole
+ ? this.renderRbacRole()
+ : nothing}
+
+
+
+
${msg("User Object Permissions")}
+
+ ${msg("Permissions set on users which affect this object.")}
+
+
-
-
-
-
-
${msg("Role Object Permissions")}
-
- ${msg("Permissions set on roles which affect this object.")}
-
-
+
+
+
+
+
+
${msg("Role Object Permissions")}
+
+ ${msg("Permissions set on roles which affect this object.")}
+
+
-
- `;
+
+
+ `;
}
renderCoreUser() {
diff --git a/web/src/admin/roles/RoleListPage.ts b/web/src/admin/roles/RoleListPage.ts
index 98f156cb93..fd68f93c0d 100644
--- a/web/src/admin/roles/RoleListPage.ts
+++ b/web/src/admin/roles/RoleListPage.ts
@@ -9,12 +9,10 @@ import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
-import { CSSResult, TemplateResult, html } from "lit";
+import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
-import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
-
import { RbacApi, Role } from "@goauthentik/api";
@customElement("ak-role-list")
@@ -37,10 +35,6 @@ export class RoleListPage extends TablePage
{
@property()
order = "name";
- static get styles(): CSSResult[] {
- return [...super.styles, PFBanner];
- }
-
async apiEndpoint(): Promise> {
return new RbacApi(DEFAULT_CONFIG).rbacRolesList(await this.defaultEndpointConfig());
}
@@ -78,10 +72,6 @@ export class RoleListPage extends TablePage {
description=${ifDefined(this.pageDescription())}
>
-
`;
diff --git a/web/src/admin/sources/kerberos/KerberosSourceViewPage.ts b/web/src/admin/sources/kerberos/KerberosSourceViewPage.ts
index 94a7cebd41..a095764535 100644
--- a/web/src/admin/sources/kerberos/KerberosSourceViewPage.ts
+++ b/web/src/admin/sources/kerberos/KerberosSourceViewPage.ts
@@ -18,6 +18,7 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
+import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
@@ -54,7 +55,17 @@ export class KerberosSourceViewPage extends AKElement {
syncState?: SyncStatus;
static get styles(): CSSResult[] {
- return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, PFList];
+ return [
+ PFBase,
+ PFPage,
+ PFButton,
+ PFGrid,
+ PFContent,
+ PFCard,
+ PFDescriptionList,
+ PFBanner,
+ PFList,
+ ];
}
constructor() {
@@ -121,6 +132,12 @@ export class KerberosSourceViewPage extends AKElement {
this.load();
}}
>
+
diff --git a/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts b/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts
index 414e42d147..fbe8a9a852 100644
--- a/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts
+++ b/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts
@@ -10,6 +10,8 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
+import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
+
import { AuthenticatorEndpointGDTCStage, StagesApi } from "@goauthentik/api";
@customElement("ak-stage-authenticator-endpoint-gdtc-form")
@@ -33,8 +35,16 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm
+ return html`
+
${msg(
"Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows.",
)}
diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts
index 6a9c65de08..7a20af84d6 100644
--- a/web/src/admin/stages/identification/IdentificationStageForm.ts
+++ b/web/src/admin/stages/identification/IdentificationStageForm.ts
@@ -21,6 +21,7 @@ import {
SourcesApi,
Stage,
StagesApi,
+ StagesCaptchaListRequest,
StagesPasswordListRequest,
UserFieldsEnum,
} from "@goauthentik/api";
@@ -140,19 +141,13 @@ export class IdentificationStageForm extends BaseStageForm
).stagesPasswordList(args);
return stages.results;
}}
- .groupBy=${(items: Stage[]) => {
- return groupBy(items, (stage) => stage.verboseNamePlural);
- }}
- .renderElement=${(stage: Stage): string => {
- return stage.name;
- }}
- .value=${(stage: Stage | undefined): string | undefined => {
- return stage?.pk;
- }}
- .selected=${(stage: Stage): boolean => {
- return stage.pk === this.instance?.passwordStage;
- }}
- ?blankable=${true}
+ .groupBy=${(items: Stage[]) =>
+ groupBy(items, (stage) => stage.verboseNamePlural)}
+ .renderElement=${(stage: Stage): string => stage.name}
+ .value=${(stage: Stage | undefined): string | undefined => stage?.pk}
+ .selected=${(stage: Stage): boolean =>
+ stage.pk === this.instance?.passwordStage}
+ blankable
>
@@ -161,6 +156,35 @@ export class IdentificationStageForm extends BaseStageForm
)}
+
+ => {
+ const args: StagesCaptchaListRequest = {
+ ordering: "name",
+ };
+ if (query !== undefined) {
+ args.search = query;
+ }
+ const stages = await new StagesApi(
+ DEFAULT_CONFIG,
+ ).stagesCaptchaList(args);
+ return stages.results;
+ }}
+ .groupBy=${(items: Stage[]) =>
+ groupBy(items, (stage) => stage.verboseNamePlural)}
+ .renderElement=${(stage: Stage): string => stage.name}
+ .value=${(stage: Stage | undefined): string | undefined => stage?.pk}
+ .selected=${(stage: Stage): boolean =>
+ stage.pk === this.instance?.captchaStage}
+ blankable
+ >
+
+
+ ${msg(
+ "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.",
+ )}
+
+
li:not(:last-child) {
+ padding-bottom: 1rem;
+ }
+ .authenticator-button {
+ display: flex;
+ align-items: center;
+ }
+ :host([theme="dark"]) .authenticator-button {
+ color: var(--ak-dark-foreground) !important;
+ }
+ i {
+ font-size: 1.5rem;
+ padding: 1rem 0;
+ width: 3rem;
+ }
+ .right {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
+ text-align: left;
+ }
+ .right > * {
+ height: 50%;
+ }
+`;
+
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage
extends BaseStage<
@@ -33,6 +64,10 @@ export class AuthenticatorValidateStage
>
implements StageHost
{
+ static get styles(): CSSResult[] {
+ return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, customCSS];
+ }
+
flowSlug = "";
set loading(value: boolean) {
@@ -47,14 +82,18 @@ export class AuthenticatorValidateStage
return this.host.brand;
}
+ @state()
+ _firstInitialized: boolean = false;
+
@state()
_selectedDeviceChallenge?: DeviceChallenge;
set selectedDeviceChallenge(value: DeviceChallenge | undefined) {
const previousChallenge = this._selectedDeviceChallenge;
this._selectedDeviceChallenge = value;
- if (!value) return;
- if (value === previousChallenge) return;
+ if (value === undefined || value === previousChallenge) {
+ return;
+ }
// We don't use this.submit here, as we don't want to advance the flow.
// We just want to notify the backend which challenge has been selected.
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
@@ -79,37 +118,39 @@ export class AuthenticatorValidateStage
return this.host?.submit(payload, options) || Promise.resolve();
}
- static get styles(): CSSResult[] {
- return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
- ul {
- padding-top: 1rem;
- }
- ul > li:not(:last-child) {
- padding-bottom: 1rem;
- }
- .authenticator-button {
- display: flex;
- align-items: center;
- }
- :host([theme="dark"]) .authenticator-button {
- color: var(--ak-dark-foreground) !important;
- }
- i {
- font-size: 1.5rem;
- padding: 1rem 0;
- width: 3rem;
- }
- .right {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 100%;
- text-align: left;
- }
- .right > * {
- height: 50%;
- }
- `);
+ willUpdate(_changed: PropertyValues) {
+ if (this._firstInitialized || !this.challenge) {
+ return;
+ }
+
+ this._firstInitialized = true;
+
+ // If user only has a single device, autoselect that device.
+ if (this.challenge.deviceChallenges.length === 1) {
+ this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
+ return;
+ }
+
+ // If TOTP is allowed from the backend and we have a pre-filled value
+ // from the password manager, autoselect TOTP.
+ const totpChallenge = this.challenge.deviceChallenges.find(
+ (challenge) => challenge.deviceClass === DeviceClassesEnum.Totp,
+ );
+ if (PasswordManagerPrefill.totp && totpChallenge) {
+ console.debug(
+ "authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
+ );
+ this.selectedDeviceChallenge = totpChallenge;
+ return;
+ }
+
+ // If the last used device is not Static, autoselect that device.
+ const lastUsedChallenge = this.challenge.deviceChallenges
+ .filter((deviceChallenge) => deviceChallenge.lastUsed)
+ .sort((a, b) => b.lastUsed!.valueOf() - a.lastUsed!.valueOf())[0];
+ if (lastUsedChallenge && lastUsedChallenge.deviceClass !== DeviceClassesEnum.Static) {
+ this.selectedDeviceChallenge = lastUsedChallenge;
+ }
}
renderDevicePickerSingle(deviceChallenge: DeviceChallenge) {
@@ -228,45 +269,28 @@ export class AuthenticatorValidateStage
}
render(): TemplateResult {
- if (!this.challenge) {
- return html` `;
- }
- // User only has a single device class, so we don't show a picker
- if (this.challenge?.deviceChallenges.length === 1) {
- this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
- }
- // TOTP is a bit special, assuming that TOTP is allowed from the backend,
- // and we have a pre-filled value from the password manager,
- // directly set the the TOTP device Challenge as active.
- const totpChallenge = this.challenge.deviceChallenges.find(
- (challenge) => challenge.deviceClass === DeviceClassesEnum.Totp,
- );
- if (PasswordManagerPrefill.totp && totpChallenge) {
- console.debug(
- "authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
- );
- this.selectedDeviceChallenge = totpChallenge;
- }
- return html`
- ${this.challenge.flowInfo?.title}
-
- ${this.selectedDeviceChallenge
- ? this.renderDeviceChallenge()
- : html`
-
- ${this.renderDevicePicker()}
-
- `}`;
+ return this.challenge
+ ? html`
+ ${this.challenge.flowInfo?.title}
+
+ ${this.selectedDeviceChallenge
+ ? this.renderDeviceChallenge()
+ : html`
+
+ ${this.renderDevicePicker()}
+
+ `}`
+ : html` `;
}
}
diff --git a/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts b/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
index 9065f2e4d8..01fcd4ef68 100644
--- a/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
+++ b/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
@@ -31,6 +31,34 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
`);
}
+ deviceMessage(): string {
+ switch (this.deviceChallenge?.deviceClass) {
+ case DeviceClassesEnum.Sms:
+ return msg("A code has been sent to you via SMS.");
+ case DeviceClassesEnum.Totp:
+ return msg(
+ "Open your two-factor authenticator app to view your authentication code.",
+ );
+ case DeviceClassesEnum.Static:
+ return msg("Enter a one-time recovery code for this user.");
+ }
+
+ return msg("Enter the code from your authenticator device.");
+ }
+
+ deviceIcon(): string {
+ switch (this.deviceChallenge?.deviceClass) {
+ case DeviceClassesEnum.Sms:
+ return "fa-key";
+ case DeviceClassesEnum.Totp:
+ return "fa-mobile-alt";
+ case DeviceClassesEnum.Static:
+ return "fa-sticky-note";
+ }
+
+ return "fa-mobile-alt";
+ }
+
render(): TemplateResult {
if (!this.challenge) {
return html` `;
@@ -44,19 +72,8 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
>
${this.renderUserInfo()}
-
- ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
- ? html`
${msg("A code has been sent to you via SMS.")}
`
- : html`
- ${msg(
- "Open your two-factor authenticator app to view your authentication code.",
- )}
-
`}
+
+
${this.deviceMessage()}
`;
}
}
diff --git a/web/src/flow/stages/captcha/CaptchaStage.ts b/web/src/flow/stages/captcha/CaptchaStage.ts
index 55e5538d59..af37ab383c 100644
--- a/web/src/flow/stages/captcha/CaptchaStage.ts
+++ b/web/src/flow/stages/captcha/CaptchaStage.ts
@@ -6,8 +6,8 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import type { TurnstileObject } from "turnstile-types";
import { msg } from "@lit/localize";
-import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
-import { customElement, state } from "lit/decorators.js";
+import { CSSResult, PropertyValues, html } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -22,6 +22,7 @@ import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/
interface TurnstileWindow extends Window {
turnstile: TurnstileObject;
}
+type TokenHandler = (token: string) => void;
const captchaContainerID = "captcha-container";
@@ -45,6 +46,11 @@ export class CaptchaStage extends BaseStage {
+ this.host.submit({ component: "ak-stage-captcha", token });
+ };
+
constructor() {
super();
this.captchaContainer = document.createElement("div");
@@ -102,11 +108,7 @@ export class CaptchaStage extends BaseStage {
const captchaId = grecaptcha.render(this.captchaContainer, {
sitekey: this.challenge.siteKey,
- callback: (token) => {
- this.host?.submit({
- token: token,
- });
- },
+ callback: this.onTokenChange,
size: "invisible",
});
grecaptcha.execute(captchaId);
@@ -122,12 +124,8 @@ export class CaptchaStage extends BaseStage {
- this.host?.submit({
- token: token,
- });
- },
});
hcaptcha.execute(captchaId);
return true;
@@ -141,16 +139,12 @@ export class CaptchaStage extends BaseStage {
- this.host?.submit({
- token: token,
- });
- },
+ callback: this.onTokenChange,
});
return true;
}
- renderBody(): TemplateResult {
+ renderBody() {
if (this.error) {
return html` `;
}
@@ -160,7 +154,7 @@ export class CaptchaStage extends BaseStage`;
}
- render(): TemplateResult {
+ render() {
if (!this.challenge) {
return html` `;
}
diff --git a/web/src/flow/stages/identification/IdentificationStage.ts b/web/src/flow/stages/identification/IdentificationStage.ts
index d0afed5a27..0983928eaa 100644
--- a/web/src/flow/stages/identification/IdentificationStage.ts
+++ b/web/src/flow/stages/identification/IdentificationStage.ts
@@ -4,10 +4,11 @@ import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/components/ak-flow-password-input.js";
import { BaseStage } from "@goauthentik/flow/stages/base";
+import "@goauthentik/flow/stages/captcha/CaptchaStage";
import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
-import { customElement } from "lit/decorators.js";
+import { customElement, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -46,6 +47,9 @@ export class IdentificationStage extends BaseStage<
> {
form?: HTMLFormElement;
+ @state()
+ captchaToken = "";
+
static get styles(): CSSResult[] {
return [
PFBase,
@@ -274,6 +278,18 @@ export class IdentificationStage extends BaseStage<
`
: nothing}
${this.renderNonFieldErrors()}
+ ${this.challenge.captchaStage
+ ? html`
+
+ {
+ this.captchaToken = token;
+ }}
+ >
+ `
+ : nothing}