Merge branch 'main' into web/update-provider-forms-for-invalidation

* main: (22 commits)
  lifecycle: fix missing krb5 deps for full testing in image (#11815)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11810)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11809)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11808)
  web: bump API Client version (#11807)
  core: bump goauthentik.io/api/v3 from 3.2024083.12 to 3.2024083.13 (#11806)
  core: bump ruff from 0.7.0 to 0.7.1 (#11805)
  core: bump twilio from 9.3.4 to 9.3.5 (#11804)
  core, web: update translations (#11803)
  providers/scim: handle no members in group in consistency check (#11801)
  stages/identification: add captcha to identification stage (#11711)
  website/docs: improve root page and redirect (#11798)
  providers/scim: clamp batch size for patch requests (#11797)
  web/admin: fix missing div in wizard forms (#11794)
  providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#11722)
  core, web: update translations (#11789)
  core: bump goauthentik.io/api/v3 from 3.2024083.11 to 3.2024083.12 (#11790)
  core: bump gssapi from 1.8.3 to 1.9.0 (#11791)
  web: bump API Client version (#11792)
  stages/authenticator_validate: autoselect last used 2fa device (#11087)
  ...
This commit is contained in:
Ken Sternberg
2024-10-28 09:37:16 -07:00
68 changed files with 1704 additions and 1345 deletions

View File

@ -46,6 +46,7 @@ class TestFlowInspector(APITestCase):
res.content, res.content,
{ {
"allow_show_password": False, "allow_show_password": False,
"captcha_stage": None,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"flow_info": { "flow_info": {
"background": flow.background_url, "background": flow.background_url,

View File

@ -197,6 +197,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
chunk_size = self._config.bulk.maxOperations chunk_size = self._config.bulk.maxOperations
if chunk_size < 1: if chunk_size < 1:
chunk_size = len(ops) chunk_size = len(ops)
if len(ops) < 1:
return
for chunk in batched(ops, chunk_size): for chunk in batched(ops, chunk_size):
req = PatchRequest(Operations=list(chunk)) req = PatchRequest(Operations=list(chunk))
self._request( self._request(
@ -237,13 +239,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
users_to_add = [] users_to_add = []
users_to_remove = [] users_to_remove = []
# Check users currently in group and if they shouldn't be in the group and remove them # 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: if user.value not in users_should:
users_to_remove.append(user.value) users_to_remove.append(user.value)
# Check users that should be in the group and add them # Check users that should be in the group and add them
for user in users_should: for user in users_should:
if len([x for x in current_group.members if x.value == user]) < 1: if len([x for x in current_group.members if x.value == user]) < 1:
users_to_add.append(user) 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( return self._patch_chunked(
scim_group.scim_id, scim_group.scim_id,
*[ *[

View File

@ -8,7 +8,7 @@ from django.http.response import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as __ from django.utils.translation import gettext as __
from django.utils.translation import gettext_lazy 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 rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from webauthn import options_to_json from webauthn import options_to_json
@ -45,6 +45,7 @@ class DeviceChallenge(PassiveSerializer):
device_class = CharField() device_class = CharField()
device_uid = CharField() device_uid = CharField()
challenge = JSONDictField() challenge = JSONDictField()
last_used = DateTimeField(allow_null=True)
def get_challenge_for_device( def get_challenge_for_device(

View File

@ -217,6 +217,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"device_class": device_class, "device_class": device_class,
"device_uid": device.pk, "device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, stage, device), "challenge": get_challenge_for_device(self.request, stage, device),
"last_used": device.last_used,
} }
) )
challenge.is_valid() challenge.is_valid()
@ -237,6 +238,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
self.request, self.request,
self.executor.current_stage, self.executor.current_stage,
), ),
"last_used": None,
} }
) )
challenge.is_valid() challenge.is_valid()

View File

@ -107,6 +107,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
"device_class": "sms", "device_class": "sms",
"device_uid": str(device.pk), "device_uid": str(device.pk),
"challenge": {}, "challenge": {},
"last_used": None,
}, },
}, },
) )

View File

@ -169,6 +169,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_class": "baz", "device_class": "baz",
"device_uid": "quox", "device_uid": "quox",
"challenge": {}, "challenge": {},
"last_used": None,
} }
}, },
) )
@ -188,6 +189,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_class": "static", "device_class": "static",
"device_uid": "1", "device_uid": "1",
"challenge": {}, "challenge": {},
"last_used": None,
}, },
}, },
) )

View File

@ -274,6 +274,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"device_class": device.__class__.__name__.lower().replace("device", ""), "device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk, "device_uid": device.pk,
"challenge": {}, "challenge": {},
"last_used": None,
} }
] ]
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -352,6 +353,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"device_class": device.__class__.__name__.lower().replace("device", ""), "device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk, "device_uid": device.pk,
"challenge": {}, "challenge": {},
"last_used": None,
} }
] ]
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -432,6 +434,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"device_class": device.__class__.__name__.lower().replace("device", ""), "device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk, "device_uid": device.pk,
"challenge": {}, "challenge": {},
"last_used": None,
} }
] ]
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan

View File

@ -1,10 +1,11 @@
"""authentik captcha stage""" """authentik captcha stage"""
from django.http.response import HttpResponse 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 requests import RequestException
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from authentik.flows.challenge import ( from authentik.flows.challenge import (
Challenge, Challenge,
@ -16,6 +17,7 @@ from authentik.lib.utils.http import get_http_session
from authentik.root.middleware import ClientIPMiddleware from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.captcha.models import CaptchaStage from authentik.stages.captcha.models import CaptchaStage
LOGGER = get_logger()
PLAN_CONTEXT_CAPTCHA = "captcha" PLAN_CONTEXT_CAPTCHA = "captcha"
@ -27,15 +29,8 @@ class CaptchaChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-captcha") component = CharField(default="ak-stage-captcha")
class CaptchaChallengeResponse(ChallengeResponse): def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str):
"""Validate captcha token""" """Validate captcha token"""
token = CharField()
component = CharField(default="ak-stage-captcha")
def validate_token(self, token: str) -> str:
"""Validate captcha token"""
stage: CaptchaStage = self.stage.executor.current_stage
try: try:
response = get_http_session().post( response = get_http_session().post(
stage.api_url, stage.api_url,
@ -45,20 +40,33 @@ class CaptchaChallengeResponse(ChallengeResponse):
data={ data={
"secret": stage.private_key, "secret": stage.private_key,
"response": token, "response": token,
"remoteip": ClientIPMiddleware.get_client_ip(self.stage.request), "remoteip": remote_ip,
}, },
) )
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
if stage.error_on_invalid_score: if stage.error_on_invalid_score:
if not data.get("success", False): if not data.get("success", False):
raise ValidationError( error_codes = data.get("error-codes", ["unknown-error"])
_( LOGGER.warning("Failed to verify captcha token", error_codes=error_codes)
"Failed to validate token: {error}".format(
error=data.get("error-codes", _("Unknown error")) # 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: if "score" in data:
score = float(data.get("score")) score = float(data.get("score"))
if stage.score_max_threshold > -1 and score > stage.score_max_threshold: if stage.score_max_threshold > -1 and score > stage.score_max_threshold:
@ -67,9 +75,24 @@ class CaptchaChallengeResponse(ChallengeResponse):
raise ValidationError(_("Invalid captcha response")) raise ValidationError(_("Invalid captcha response"))
except (RequestException, TypeError) as exc: except (RequestException, TypeError) as exc:
raise ValidationError(_("Failed to validate token")) from exc raise ValidationError(_("Failed to validate token")) from exc
return data return data
class CaptchaChallengeResponse(ChallengeResponse):
"""Validate captcha token"""
token = CharField()
component = CharField(default="ak-stage-captcha")
def validate_token(self, token: str) -> str:
"""Validate captcha token"""
stage: CaptchaStage = self.stage.executor.current_stage
client_ip = ClientIPMiddleware.get_client_ip(self.stage.request)
return verify_captcha_token(stage, token, client_ip)
class CaptchaStageView(ChallengeStageView): class CaptchaStageView(ChallengeStageView):
"""Simple captcha checker, logic is handled in django-captcha module""" """Simple captcha checker, logic is handled in django-captcha module"""

View File

@ -27,6 +27,7 @@ class IdentificationStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + [ fields = StageSerializer.Meta.fields + [
"user_fields", "user_fields",
"password_stage", "password_stage",
"captcha_stage",
"case_insensitive_matching", "case_insensitive_matching",
"show_matched_user", "show_matched_user",
"enrollment_flow", "enrollment_flow",
@ -46,6 +47,7 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet):
filterset_fields = [ filterset_fields = [
"name", "name",
"password_stage", "password_stage",
"captcha_stage",
"case_insensitive_matching", "case_insensitive_matching",
"show_matched_user", "show_matched_user",
"enrollment_flow", "enrollment_flow",

View File

@ -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",
),
),
]

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source from authentik.core.models import Source
from authentik.flows.models import Flow, Stage from authentik.flows.models import Flow, Stage
from authentik.stages.captcha.models import CaptchaStage
from authentik.stages.password.models import PasswordStage 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( case_insensitive_matching = models.BooleanField(
default=True, default=True,
help_text=_("When enabled, user fields are matched regardless of their casing."), help_text=_("When enabled, user fields are matched regardless of their casing."),

View File

@ -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.reflection import all_subclasses
from authentik.lib.utils.urls import reverse_with_qs from authentik.lib.utils.urls import reverse_with_qs
from authentik.root.middleware import ClientIPMiddleware 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.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate from authentik.stages.password.stage import authenticate
@ -75,6 +76,7 @@ class IdentificationChallenge(Challenge):
allow_show_password = BooleanField(default=False) allow_show_password = BooleanField(default=False)
application_pre = CharField(required=False) application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices) flow_designation = ChoiceField(FlowDesignation.choices)
captcha_stage = CaptchaChallenge(required=False)
enroll_url = CharField(required=False) enroll_url = CharField(required=False)
recovery_url = CharField(required=False) recovery_url = CharField(required=False)
@ -91,14 +93,16 @@ class IdentificationChallengeResponse(ChallengeResponse):
uid_field = CharField() uid_field = CharField()
password = CharField(required=False, allow_blank=True, allow_null=True) 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") component = CharField(default="ak-stage-identification")
pre_user: User | None = None pre_user: User | None = None
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: 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"] uid_field = attrs["uid_field"]
current_stage: IdentificationStage = self.stage.executor.current_stage 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) pre_user = self.stage.get_user(uid_field)
if not pre_user: if not pre_user:
@ -113,7 +117,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
self.stage.logger.info( self.stage.logger.info(
"invalid_login", "invalid_login",
identifier=uid_field, identifier=uid_field,
client_ip=ClientIPMiddleware.get_client_ip(self.stage.request), client_ip=client_ip,
action="invalid_identifier", action="invalid_identifier",
context={ context={
"stage": sanitize_item(self.stage), "stage": sanitize_item(self.stage),
@ -136,6 +140,15 @@ class IdentificationChallengeResponse(ChallengeResponse):
return attrs return attrs
raise ValidationError("Failed to authenticate.") raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user 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: if not current_stage.password_stage:
# No password stage select, don't validate the password # No password stage select, don't validate the password
return attrs return attrs
@ -206,6 +219,14 @@ class IdentificationStageView(ChallengeStageView):
"primary_action": self.get_primary_action(), "primary_action": self.get_primary_action(),
"user_fields": current_stage.user_fields, "user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage), "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) "allow_show_password": bool(current_stage.password_stage)
and current_stage.password_stage.allow_show_password, and current_stage.password_stage.allow_show_password,
"show_source_labels": current_stage.show_source_labels, "show_source_labels": current_stage.show_source_labels,

View File

@ -1,6 +1,7 @@
"""identification tests""" """identification tests"""
from django.urls import reverse from django.urls import reverse
from requests_mock import Mocker
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.core.tests.utils import create_test_admin_user, create_test_flow 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.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.sources.oauth.models import OAuthSource 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.api import IdentificationStageSerializer
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT
@ -133,6 +136,135 @@ class TestIdentificationStage(FlowTestCase):
user_fields=["email"], 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): def test_invalid_with_username(self):
"""Test invalid with username (user exists but stage only allows email)""" """Test invalid with username (user exists but stage only allows email)"""
form_data = {"uid_field": self.user.username} form_data = {"uid_field": self.user.username}

View File

@ -6974,7 +6974,7 @@
"spnego_server_name": { "spnego_server_name": {
"type": "string", "type": "string",
"title": "Spnego server name", "title": "Spnego server name",
"description": "Force the use of a specific server name for SPNEGO" "description": "Force the use of a specific server name for SPNEGO. Must be in the form HTTP@hostname"
}, },
"spnego_keytab": { "spnego_keytab": {
"type": "string", "type": "string",
@ -10679,6 +10679,11 @@
"title": "Password stage", "title": "Password stage",
"description": "When set, shows a password field, instead of showing the password field as separate step." "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": { "case_insensitive_matching": {
"type": "boolean", "type": "boolean",
"title": "Case insensitive matching", "title": "Case insensitive matching",

2
go.mod
View File

@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024083.11 goauthentik.io/api/v3 v3.2024083.13
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.8.0 golang.org/x/sync v0.8.0

4
go.sum
View File

@ -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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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.13 h1:xKh3feJYUeLw583zZ5ifgV0qjD37ZCOzgXPfbHQSbHM=
goauthentik.io/api/v3 v3.2024083.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2024083.13/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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -82,6 +82,9 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
if embedded { if embedded {
ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host) ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host)
ep.JwksUri = updateURL(jwksUri, 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 return ep
} }

View File

@ -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/authorize/", ep.AuthURL)
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) 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/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/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
} }

View File

@ -54,6 +54,8 @@ function cleanup {
} }
function prepare_debug { function prepare_debug {
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
touch /unittest.xml touch /unittest.xml

View File

@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2024\n" "Last-Translator: Marc Schmitt, 2024\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\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)" msgid "(You are already connected in another tab/window)"
msgstr "(Vous êtes déjà connecté dans un autre onglet/une autre fenêtre)" 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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." msgid "Used recovery-link to authenticate."
msgstr "Utiliser un lien de récupération pour se connecter." 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 #: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed" msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr "" msgstr ""
@ -3121,6 +3264,10 @@ msgstr "Base de données utilisateurs + mots de passes applicatifs"
msgid "User database + LDAP password" msgid "User database + LDAP password"
msgstr "Base de données utilisateurs + mot de passe LDAP" 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 #: authentik/stages/password/models.py
msgid "Selection of backends to test the password against." msgid "Selection of backends to test the password against."
msgstr "Sélection de backends pour tester le mot de passe." msgstr "Sélection de backends pour tester le mot de passe."

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n" "Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\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)" msgid "(You are already connected in another tab/window)"
msgstr "(您已经在另一个标签页/窗口连接了)" 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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." msgid "Used recovery-link to authenticate."
msgstr "已使用恢复链接进行身份验证。" 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 #: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed" msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr "仅允许使用密码同步的单个 LDAP 源" msgstr "仅允许使用密码同步的单个 LDAP 源"
@ -2876,6 +3002,10 @@ msgstr "用户数据库 + 应用程序密码"
msgid "User database + LDAP password" msgid "User database + LDAP password"
msgstr "用户数据库 + LDAP 密码" msgstr "用户数据库 + LDAP 密码"
#: authentik/stages/password/models.py
msgid "User database + Kerberos password"
msgstr "用户数据库 + Kerberos 密码"
#: authentik/stages/password/models.py #: authentik/stages/password/models.py
msgid "Selection of backends to test the password against." msgid "Selection of backends to test the password against."
msgstr "选择用于测试密码的后端。" msgstr "选择用于测试密码的后端。"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n" "Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\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)" msgid "(You are already connected in another tab/window)"
msgstr "(您已经在另一个标签页/窗口连接了)" 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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." msgid "Used recovery-link to authenticate."
msgstr "已使用恢复链接进行身份验证。" 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 #: authentik/sources/ldap/api.py
msgid "Only a single LDAP Source with password synchronization is allowed" msgid "Only a single LDAP Source with password synchronization is allowed"
msgstr "仅允许使用密码同步的单个 LDAP 源" msgstr "仅允许使用密码同步的单个 LDAP 源"
@ -2875,6 +3001,10 @@ msgstr "用户数据库 + 应用程序密码"
msgid "User database + LDAP password" msgid "User database + LDAP password"
msgstr "用户数据库 + LDAP 密码" msgstr "用户数据库 + LDAP 密码"
#: authentik/stages/password/models.py
msgid "User database + Kerberos password"
msgstr "用户数据库 + Kerberos 密码"
#: authentik/stages/password/models.py #: authentik/stages/password/models.py
msgid "Selection of backends to test the password against." msgid "Selection of backends to test the password against."
msgstr "选择用于测试密码的后端。" msgstr "选择用于测试密码的后端。"

97
poetry.lock generated
View File

@ -1849,35 +1849,36 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
[[package]] [[package]]
name = "gssapi" name = "gssapi"
version = "1.8.3" version = "1.9.0"
description = "Python GSSAPI Wrapper" description = "Python GSSAPI Wrapper"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "gssapi-1.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e4a83e9b275fe69b5d40be6d5479889866b80333a12c51a9243f2712d4f0554"}, {file = "gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1"},
{file = "gssapi-1.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d57d67547e18f4e44a688bfb20abbf176d1b8df547da2b31c3f2df03cfdc269"}, {file = "gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6"},
{file = "gssapi-1.8.3-cp310-cp310-win32.whl", hash = "sha256:3a3f63105f39c4af29ffc8f7b6542053d87fe9d63010c689dd9a9f5571facb8e"}, {file = "gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e"},
{file = "gssapi-1.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:b031c0f186ab4275186da385b2c7470dd47c9b27522cb3b753757c9ac4bebf11"}, {file = "gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962"},
{file = "gssapi-1.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b03d6b30f1fcd66d9a688b45a97e302e4dd3f1386d5c333442731aec73cdb409"}, {file = "gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560"},
{file = "gssapi-1.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca6ceb17fc15eda2a69f2e8c6cf10d11e2edb32832255e5d4c65b21b6db4680a"}, {file = "gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd"},
{file = "gssapi-1.8.3-cp311-cp311-win32.whl", hash = "sha256:edc8ef3a9e397dbe18bb6016f8e2209969677b534316d20bb139da2865a38efe"}, {file = "gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe"},
{file = "gssapi-1.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8fdb1ff130cee49bc865ec1624dee8cf445cd6c6e93b04bffef2c6f363a60cb9"}, {file = "gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f"},
{file = "gssapi-1.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:19c373b3ba63ce19cd3163aa1495635e3d01b0de6cc4ff1126095eded1df6e01"}, {file = "gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f"},
{file = "gssapi-1.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f1a8046d695f2c9b8d640a6e385780d3945c0741571ed6fee6f94c31e431dc"}, {file = "gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f"},
{file = "gssapi-1.8.3-cp312-cp312-win32.whl", hash = "sha256:338db18612e3e6ed64e92b6d849242a535fdc98b365f21122992fb8cae737617"}, {file = "gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec"},
{file = "gssapi-1.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5731c5b40ecc3116cfe7fb7e1d1e128583ec8b3df1e68bf8cd12073160793acd"}, {file = "gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76"},
{file = "gssapi-1.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e556878da197ad115a566d36e46a8082d0079731d9c24d1ace795132d725ff2a"}, {file = "gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c"},
{file = "gssapi-1.8.3-cp37-cp37m-win32.whl", hash = "sha256:e2bb081f2db2111377effe7d40ba23f9a87359b9d2f4881552b731e9da88b36b"}, {file = "gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e"},
{file = "gssapi-1.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4d9ed83f2064cda60aad90e6840ae282096801b2c814b8cbd390bf0df4635aab"}, {file = "gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28"},
{file = "gssapi-1.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d91fe6e2a5c89b32102ea8e374b8ae13b9031d43d7b55f3abc1f194ddce820d"}, {file = "gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9"},
{file = "gssapi-1.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5b28237afc0668046934792756dd4b6b7e957b0d95a608d02f296734a2819ad"}, {file = "gssapi-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0e378d62b2fc352ca0046030cda5911d808a965200f612fdd1d74501b83e98f"},
{file = "gssapi-1.8.3-cp38-cp38-win32.whl", hash = "sha256:791e44f7bea602b8e3da1ec56fbdb383b8ee3326fdeb736f904c2aa9af13a67d"}, {file = "gssapi-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b74031c70864d04864b7406c818f41be0c1637906fb9654b06823bcc79f151dc"},
{file = "gssapi-1.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:5b4bf84d0a6d7779a4bf11dacfd3db57ae02dd53562e2aeadac4219a68eaee07"}, {file = "gssapi-1.9.0-cp38-cp38-win32.whl", hash = "sha256:f2f3a46784d8127cc7ef10d3367dedcbe82899ea296710378ccc9b7cefe96f4c"},
{file = "gssapi-1.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e40efc88ccefefd6142f8c47b8af498731938958b808bad49990442a91f45160"}, {file = "gssapi-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:a81f30cde21031e7b1f8194a3eea7285e39e551265e7744edafd06eadc1c95bc"},
{file = "gssapi-1.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee74b9211c977b9181ff4652d886d7712c9a221560752a35393b58e5ea07887a"}, {file = "gssapi-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbc93fdadd5aab9bae594538b2128044b8c5cdd1424fe015a465d8a8a587411a"},
{file = "gssapi-1.8.3-cp39-cp39-win32.whl", hash = "sha256:465c6788f2ac6ef7c738394ba8fde1ede6004e5721766f386add63891d8c90af"}, {file = "gssapi-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b2a3c0a9beb895942d4b8e31f515e52c17026e55aeaa81ee0df9bbfdac76098"},
{file = "gssapi-1.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:8fb8ee70458f47b51ed881a6881f30b187c987c02af16cc0fff0079255d4d465"}, {file = "gssapi-1.9.0-cp39-cp39-win32.whl", hash = "sha256:060b58b455d29ab8aca74770e667dca746264bee660ac5b6a7a17476edc2c0b8"},
{file = "gssapi-1.8.3.tar.gz", hash = "sha256:aa3c8d0b1526f52559552bb2c9d2d6be013d76a8e5db00b39a1db5727e93b0b0"}, {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] [package.dependencies]
@ -4292,29 +4293,29 @@ pyasn1 = ">=0.1.3"
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.7.0" version = "0.7.1"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"}, {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
{file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"}, {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
{file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"}, {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
{file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"}, {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
{file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"}, {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
{file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"}, {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
{file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"}, {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
] ]
[[package]] [[package]]
@ -4750,13 +4751,13 @@ wsproto = ">=0.14"
[[package]] [[package]]
name = "twilio" name = "twilio"
version = "9.3.4" version = "9.3.5"
description = "Twilio API client and TwiML generator" description = "Twilio API client and TwiML generator"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.7.0"
files = [ files = [
{file = "twilio-9.3.4-py2.py3-none-any.whl", hash = "sha256:2cae99f0f7aecbd9da02fa59ad8f11b360db4a9281fc3fb3237ad50be21d8a9b"}, {file = "twilio-9.3.5-py2.py3-none-any.whl", hash = "sha256:d6a97a77b98cc176a61c960f11894af385bc1c11b93e2e8b79fdfb9601788fb0"},
{file = "twilio-9.3.4.tar.gz", hash = "sha256:38a6ab04752f44313dcf736eae45236a901528d3f53dfc21d3afd33539243c7f"}, {file = "twilio-9.3.5.tar.gz", hash = "sha256:608d78a903d403465aac1840c58a6546a090b7e222d2bf539a93c3831072880c"},
] ]
[package.dependencies] [package.dependencies]

View File

@ -33862,6 +33862,11 @@ paths:
operationId: stages_identification_list operationId: stages_identification_list
description: IdentificationStage Viewset description: IdentificationStage Viewset
parameters: parameters:
- in: query
name: captcha_stage
schema:
type: string
format: uuid
- in: query - in: query
name: case_insensitive_matching name: case_insensitive_matching
schema: schema:
@ -40204,10 +40209,15 @@ components:
challenge: challenge:
type: object type: object
additionalProperties: {} additionalProperties: {}
last_used:
type: string
format: date-time
nullable: true
required: required:
- challenge - challenge
- device_class - device_class
- device_uid - device_uid
- last_used
DeviceChallengeRequest: DeviceChallengeRequest:
type: object type: object
description: Single device challenge description: Single device challenge
@ -40221,10 +40231,15 @@ components:
challenge: challenge:
type: object type: object
additionalProperties: {} additionalProperties: {}
last_used:
type: string
format: date-time
nullable: true
required: required:
- challenge - challenge
- device_class - device_class
- device_uid - device_uid
- last_used
DeviceClassesEnum: DeviceClassesEnum:
enum: enum:
- static - static
@ -42494,6 +42509,8 @@ components:
type: string type: string
flow_designation: flow_designation:
$ref: '#/components/schemas/FlowDesignationEnum' $ref: '#/components/schemas/FlowDesignationEnum'
captcha_stage:
$ref: '#/components/schemas/CaptchaChallenge'
enroll_url: enroll_url:
type: string type: string
recovery_url: recovery_url:
@ -42528,6 +42545,9 @@ components:
password: password:
type: string type: string
nullable: true nullable: true
captcha_token:
type: string
nullable: true
required: required:
- uid_field - uid_field
IdentificationStage: IdentificationStage:
@ -42573,6 +42593,12 @@ components:
nullable: true nullable: true
description: When set, shows a password field, instead of showing the password description: When set, shows a password field, instead of showing the password
field as separate step. 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: case_insensitive_matching:
type: boolean type: boolean
description: When enabled, user fields are matched regardless of their casing. description: When enabled, user fields are matched regardless of their casing.
@ -42641,6 +42667,12 @@ components:
nullable: true nullable: true
description: When set, shows a password field, instead of showing the password description: When set, shows a password field, instead of showing the password
field as separate step. 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: case_insensitive_matching:
type: boolean type: boolean
description: When enabled, user fields are matched regardless of their casing. description: When enabled, user fields are matched regardless of their casing.
@ -42943,7 +42975,8 @@ components:
readOnly: true readOnly: true
spnego_server_name: spnego_server_name:
type: string type: string
description: Force the use of a specific server name for SPNEGO description: Force the use of a specific server name for SPNEGO. Must be
in the form HTTP@hostname
spnego_ccache: spnego_ccache:
type: string type: string
description: Credential cache to use for SPNEGO in form type:residual description: Credential cache to use for SPNEGO in form type:residual
@ -43112,7 +43145,8 @@ components:
be in the form TYPE:residual be in the form TYPE:residual
spnego_server_name: spnego_server_name:
type: string type: string
description: Force the use of a specific server name for SPNEGO description: Force the use of a specific server name for SPNEGO. Must be
in the form HTTP@hostname
spnego_keytab: spnego_keytab:
type: string type: string
writeOnly: true writeOnly: true
@ -48231,6 +48265,12 @@ components:
nullable: true nullable: true
description: When set, shows a password field, instead of showing the password description: When set, shows a password field, instead of showing the password
field as separate step. 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: case_insensitive_matching:
type: boolean type: boolean
description: When enabled, user fields are matched regardless of their casing. description: When enabled, user fields are matched regardless of their casing.
@ -48410,7 +48450,8 @@ components:
be in the form TYPE:residual be in the form TYPE:residual
spnego_server_name: spnego_server_name:
type: string type: string
description: Force the use of a specific server name for SPNEGO description: Force the use of a specific server name for SPNEGO. Must be
in the form HTTP@hostname
spnego_keytab: spnego_keytab:
type: string type: string
writeOnly: true writeOnly: true

8
web/package-lock.json generated
View File

@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@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-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
@ -1775,9 +1775,9 @@
} }
}, },
"node_modules/@goauthentik/api": { "node_modules/@goauthentik/api": {
"version": "2024.8.3-1729699127", "version": "2024.8.3-1729836831",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729699127.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729836831.tgz",
"integrity": "sha512-luo0SAASR6BTTtLszDgfdwofBejv4F3hCHgPxeSoTSFgE8/A2+zJD8EtWPZaa1udDkwPa9lbIeJSSmbgFke3jA==" "integrity": "sha512-nOgvjYQiK+HhWuiZ635h/aSsq7Mfj5cDrIyBJt+IJRQuJFtnnHx8nscRXKK/8sBl9obH2zMCoZgeqytK8145bg=="
}, },
"node_modules/@goauthentik/web": { "node_modules/@goauthentik/web": {
"resolved": "", "resolved": "",

View File

@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@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-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",

View File

@ -29,7 +29,7 @@ export class ApplicationWizardPageBase
return AwadStyles; return AwadStyles;
} }
@consume({ context: applicationWizardContext }) @consume({ context: applicationWizardContext, subscribe: true })
public wizard!: ApplicationWizardState; public wizard!: ApplicationWizardState;
@query("form") @query("form")

View File

@ -1,7 +1,12 @@
import { createContext } from "@lit/context"; import { createContext } from "@lit/context";
import { LocalTypeCreate } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
import { ApplicationWizardState } from "./types"; import { ApplicationWizardState } from "./types";
export const applicationWizardContext = createContext<ApplicationWizardState>( export const applicationWizardContext = createContext<ApplicationWizardState>(
Symbol("ak-application-wizard-state-context"), Symbol("ak-application-wizard-state-context"),
); );
export const applicationWizardProvidersContext = createContext<LocalTypeCreate[]>(
Symbol("ak-application-wizard-providers-context"),
);

View File

@ -1,3 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard"; import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
@ -5,7 +6,10 @@ import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { applicationWizardContext } from "./ContextIdentity"; import { ProvidersApi, ProxyMode } from "@goauthentik/api";
import { applicationWizardContext, applicationWizardProvidersContext } from "./ContextIdentity";
import { providerTypeRenderers } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
import { newSteps } from "./steps"; import { newSteps } from "./steps";
import { import {
ApplicationStep, ApplicationStep,
@ -19,6 +23,7 @@ const freshWizardState = (): ApplicationWizardState => ({
app: {}, app: {},
provider: {}, provider: {},
errors: {}, errors: {},
proxyMode: ProxyMode.Proxy,
}); });
@customElement("ak-application-wizard") @customElement("ak-application-wizard")
@ -46,6 +51,11 @@ export class ApplicationWizard extends CustomListenerElement(
initialValue: this.wizardState, initialValue: this.wizardState,
}); });
wizardProviderProvider = new ContextProvider(this, {
context: applicationWizardProvidersContext,
initialValue: [],
});
/** /**
* One of our steps has multiple display variants, one for each type of service provider. We * One of our steps has multiple display variants, one for each type of service provider. We
* want to *preserve* a customer's decisions about different providers; never make someone "go * want to *preserve* a customer's decisions about different providers; never make someone "go
@ -56,6 +66,21 @@ export class ApplicationWizard extends CustomListenerElement(
*/ */
providerCache: Map<string, OneOfProvider> = new Map(); providerCache: Map<string, OneOfProvider> = new Map();
connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
const wizardReadyProviders = Object.keys(providerTypeRenderers);
this.wizardProviderProvider.setValue(
providerTypes
.filter((providerType) => wizardReadyProviders.includes(providerType.modelName))
.map((providerType) => ({
...providerType,
renderer: providerTypeRenderers[providerType.modelName],
})),
);
});
}
// And this is where all the special cases go... // And this is where all the special cases go...
handleUpdate(detail: ApplicationWizardStateUpdate) { handleUpdate(detail: ApplicationWizardStateUpdate) {
if (detail.status === "submitted") { if (detail.status === "submitted") {

View File

@ -1,176 +1,28 @@
import "@goauthentik/admin/common/ak-license-notice"; import "@goauthentik/admin/common/ak-license-notice";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api"; import type { TypeCreate } from "@goauthentik/api";
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
import type {
LDAPProviderRequest,
ModelRequest,
OAuth2ProviderRequest,
ProxyProviderRequest,
RACProviderRequest,
RadiusProviderRequest,
SAMLProviderRequest,
SCIMProviderRequest,
} from "@goauthentik/api";
import { OneOfProvider } from "../types";
type ProviderRenderer = () => TemplateResult; type ProviderRenderer = () => TemplateResult;
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
type ProviderNoteProvider = () => TemplateResult | undefined;
type ProviderNote = ProviderNoteProvider | undefined;
export type LocalTypeCreate = TypeCreate & { export type LocalTypeCreate = TypeCreate & {
formName: string;
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
renderer: ProviderRenderer; renderer: ProviderRenderer;
}; };
export const providerModelsList: LocalTypeCreate[] = [ export const providerTypeRenderers = {
{ oauth2provider: () =>
formName: "oauth2provider",
name: msg("OAuth2/OpenID Provider"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`, html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
modelName: ProviderModelEnum.Oauth2Oauth2provider, ldapprovider: () =>
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
...(provider as OAuth2ProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/openidconnect.svg",
},
{
formName: "ldapprovider",
name: msg("LDAP Provider"),
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
renderer: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`, html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
modelName: ProviderModelEnum.LdapLdapprovider, proxyprovider: () =>
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.LdapLdapprovider,
...(provider as LDAPProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/ldap.png",
},
{
formName: "proxyprovider-proxy",
name: msg("Transparent Reverse Proxy"),
description: msg("For transparent reverse proxies with required authentication"),
renderer: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`, html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider, racprovider: () =>
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.Proxy,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwardsingle",
name: msg("Forward Auth (Single Application)"),
description: msg("For nginx's auth_request or traefik's forwardAuth"),
renderer: () =>
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardSingle,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwarddomain",
name: msg("Forward Auth (Domain Level)"),
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
renderer: () =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardDomain,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "racprovider",
name: msg("Remote Access Provider"),
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
renderer: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`, html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
modelName: ProviderModelEnum.RacRacprovider, samlprovider: () =>
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RacRacprovider,
...(provider as RACProviderRequest),
}),
note: () => html`<ak-license-notice></ak-license-notice>`,
requiresEnterprise: true,
component: "",
iconUrl: "/static/authentik/sources/rac.svg",
},
{
formName: "samlprovider",
name: msg("SAML Provider"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`, html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
modelName: ProviderModelEnum.SamlSamlprovider, radiusprovider: () =>
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.SamlSamlprovider,
...(provider as SAMLProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/saml.png",
},
{
formName: "radiusprovider",
name: msg("Radius Provider"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`, html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
modelName: ProviderModelEnum.RadiusRadiusprovider, scimprovider: () =>
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
...(provider as RadiusProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/radius.svg",
},
{
formName: "scimprovider",
name: msg("SCIM Provider"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`, html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
modelName: ProviderModelEnum.ScimScimprovider, };
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ScimScimprovider,
...(provider as SCIMProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/scim.png",
},
];
export const providerRendererList = new Map<string, ProviderRenderer>(
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
);
export default providerModelsList;

View File

@ -7,34 +7,29 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/wizard/TypeCreateWizardPage"; import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage"; import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { consume } from "@lit/context";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit"; import { html } from "lit";
import BasePanel from "../BasePanel"; import BasePanel from "../BasePanel";
import { applicationWizardProvidersContext } from "../ContextIdentity";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices"; import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
@customElement("ak-application-wizard-authentication-method-choice") @customElement("ak-application-wizard-authentication-method-choice")
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) { export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
@consume({ context: applicationWizardProvidersContext })
public providerModelsList: LocalTypeCreate[];
render() { render() {
const selectedTypes = providerModelsList.filter( const selectedTypes = this.providerModelsList.filter(
(t) => t.formName === this.wizard.providerModel, (t) => t.modelName === this.wizard.providerModel,
); );
// As a hack, the Application wizard has separate provider paths for our three types of return this.providerModelsList.length > 0
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
// modelName to the formName, so we get the right one. This information isn't modified
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
const typesForWizard = providerModelsList.map((provider) => ({
...provider,
modelName: provider.formName,
}));
return providerModelsList.length > 0
? html`<form class="pf-c-form pf-m-horizontal"> ? html`<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create <ak-wizard-page-type-create
.types=${typesForWizard} .types=${this.providerModelsList}
name="selectProviderType" name="selectProviderType"
layout=${TypeCreateWizardPageLayouts.grid} layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined} .selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@ -42,7 +37,7 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm
this.dispatchWizardUpdate({ this.dispatchWizardUpdate({
update: { update: {
...this.wizard, ...this.wizard,
providerModel: ev.detail.formName, providerModel: ev.detail.modelName,
errors: {}, errors: {},
}, },
status: this.valid ? "valid" : "invalid", status: this.valid ? "valid" : "invalid",

View File

@ -21,14 +21,14 @@ import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import { import {
type ApplicationRequest, type ApplicationRequest,
CoreApi, CoreApi,
type ModelRequest, ProviderModelEnum,
ProxyMode,
type TransactionApplicationRequest, type TransactionApplicationRequest,
type TransactionApplicationResponse, type TransactionApplicationResponse,
ValidationError, ValidationError,
} from "@goauthentik/api"; } from "@goauthentik/api";
import BasePanel from "../BasePanel"; import BasePanel from "../BasePanel";
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest { function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
return { return {
@ -38,14 +38,19 @@ function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest
}; };
} }
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
type State = { type State = {
state: "idle" | "running" | "error" | "success"; state: "idle" | "running" | "error" | "success";
label: string | TemplateResult; label: string | TemplateResult;
icon: string[]; icon: string[];
}; };
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
.reduce((acc: Map<string, string>, value) => {
acc.set(value.split(".")[1], value);
return acc;
}, new Map());
const idleState: State = { const idleState: State = {
state: "idle", state: "idle",
label: "", label: "",
@ -98,19 +103,25 @@ export class ApplicationWizardCommitApplication extends BasePanel {
if (this.commitState === idleState) { if (this.commitState === idleState) {
this.response = undefined; this.response = undefined;
this.commitState = runningState; this.commitState = runningState;
const providerModel = providerModelsList.find(
({ formName }) => formName === this.wizard.providerModel, // Stringly-based API. Not the best, but it works. Just be aware that it is
); // stringly-based.
if (!providerModel) { const providerModel = providerMap.get(this.wizard.providerModel);
throw new Error( const provider = this.wizard.provider;
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`, provider.providerModel = providerModel;
);
// Special case for providers.
if (this.wizard.providerModel === "proxyprovider") {
provider.mode = this.wizard.proxyMode;
if (provider.model !== ProxyMode.ForwardDomain) {
provider.cookieDomain = "";
}
} }
const request: TransactionApplicationRequest = { const request: TransactionApplicationRequest = {
providerModel: providerModel.modelName as ProviderModelType,
app: cleanApplication(this.wizard.app), app: cleanApplication(this.wizard.app),
provider: providerModel.converter(this.wizard.provider), providerModel,
provider,
}; };
this.send(request); this.send(request);

View File

@ -1,12 +1,12 @@
import { consume } from "@lit/context";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import BasePanel from "../BasePanel"; import BasePanel from "../BasePanel";
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices"; import { applicationWizardProvidersContext } from "../ContextIdentity";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import "./ldap/ak-application-wizard-authentication-by-ldap"; import "./ldap/ak-application-wizard-authentication-by-ldap";
import "./oauth/ak-application-wizard-authentication-by-oauth"; import "./oauth/ak-application-wizard-authentication-by-oauth";
import "./proxy/ak-application-wizard-authentication-for-forward-domain-proxy";
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy"; import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
import "./rac/ak-application-wizard-authentication-for-rac"; import "./rac/ak-application-wizard-authentication-for-rac";
import "./radius/ak-application-wizard-authentication-by-radius"; import "./radius/ak-application-wizard-authentication-by-radius";
import "./saml/ak-application-wizard-authentication-by-saml-configuration"; import "./saml/ak-application-wizard-authentication-by-saml-configuration";
@ -14,14 +14,19 @@ import "./scim/ak-application-wizard-authentication-by-scim";
@customElement("ak-application-wizard-authentication-method") @customElement("ak-application-wizard-authentication-method")
export class ApplicationWizardApplicationDetails extends BasePanel { export class ApplicationWizardApplicationDetails extends BasePanel {
@consume({ context: applicationWizardProvidersContext })
public providerModelsList: LocalTypeCreate[];
render() { render() {
const handler = providerRendererList.get(this.wizard.providerModel); const handler: LocalTypeCreate | undefined = this.providerModelsList.find(
({ modelName }) => modelName === this.wizard.providerModel,
);
if (!handler) { if (!handler) {
throw new Error( throw new Error(
"Unrecognized authentication method in ak-application-wizard-authentication-method", "Unrecognized authentication method in ak-application-wizard-authentication-method",
); );
} }
return handler(); return handler.renderer();
} }
} }

View File

@ -1,268 +0,0 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "@goauthentik/admin/providers/proxy/ProxyProviderPropertyMappings.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { state } from "@lit/reactive-element/decorators.js";
import { TemplateResult, html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
ProxyMode,
ProxyProvider,
SourcesApi,
} from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
type MaybeTemplateResult = TemplateResult | typeof nothing;
export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
constructor() {
super();
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((oauthSources: PaginatedOAuthSourceList) => {
this.oauthSources = oauthSources;
});
}
propertyMappings?: PaginatedScopeMappingList;
oauthSources?: PaginatedOAuthSourceList;
@state()
showHttpBasic = true;
@state()
mode: ProxyMode = ProxyMode.Proxy;
get instance(): ProxyProvider | undefined {
return this.wizard.provider as ProxyProvider;
}
renderModeDescription(): MaybeTemplateResult {
return nothing;
}
renderProxyMode(): TemplateResult {
throw new Error("Must be implemented in a child class.");
}
renderHttpBasic() {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
help=${msg(
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
)}
>
</ak-text-input>`;
}
render() {
const errors = this.wizard.errors.provider;
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${this.renderModeDescription()}
<ak-text-input
name="name"
value=${ifDefined(this.instance?.name)}
required
.errorMessages=${errors?.name ?? []}
label=${msg("Name")}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
${this.renderProxyMode()}
<ak-text-input
name="accessTokenValidity"
value=${first(this.instance?.accessTokenValidity, "hours=24")}
label=${msg("Token validity")}
help=${msg("Configure how long tokens are valid for.")}
.errorMessages=${errors?.accessTokenValidity ?? []}
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.certificate ?? undefined)}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-textarea-input
name="skipPathRegex"
label=${
this.mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")
}
value=${ifDefined(this.instance?.skipPathRegex)}
.errorMessages=${errors?.skipPathRegex ?? []}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>`}
>
</ak-textarea-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
label=${msg("Intercept header authentication")}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
></ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
label=${msg("Send HTTP-Basic Authentication")}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
></ak-switch-input>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}
export default AkTypeProxyApplicationWizardPage;

View File

@ -1,74 +0,0 @@
import "@goauthentik/components/ak-text-input";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
static get styles() {
return super.styles.concat(PFList);
}
renderModeDescription() {
return html`<p>
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div>
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>`;
}
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
return html`
<ak-text-input
name="externalHost"
label=${msg("External host")}
value=${ifDefined(provider?.externalHost)}
.errorMessages=${errors?.externalHost ?? []}
required
help=${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
>
</ak-text-input>
<ak-text-input
name="cookieDomain"
label=${msg("Cookie domain")}
value="${ifDefined(provider?.cookieDomain)}"
.errorMessages=${errors?.cookieDomain ?? []}
required
help=${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
></ak-text-input>
`;
}
}
export default AkForwardDomainProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-forward-proxy-domain": AkForwardDomainProxyApplicationWizardPage;
}
}

View File

@ -1,55 +1,46 @@
import { first } from "@goauthentik/common/utils"; import {
import "@goauthentik/components/ak-switch-input"; ProxyModeValue,
import "@goauthentik/components/ak-text-input"; renderForm,
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js"; import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit"; import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ProxyProvider } from "@goauthentik/api"; import BaseProviderPanel from "../BaseProviderPanel.js";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-reverse-proxy") @customElement("ak-application-wizard-authentication-for-reverse-proxy")
export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage { export class AkReverseProxyApplicationWizardPage extends BaseProviderPanel {
renderModeDescription() { @state()
return html`<p class="pf-u-mb-xl"> showHttpBasic = true;
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>`;
}
renderProxyMode() { render() {
const provider = this.wizard.provider as ProxyProvider | undefined; const onSetMode: SetMode = (ev: CustomEvent<ProxyModeValue>) => {
const errors = this.wizard.errors.provider; this.dispatchWizardUpdate({
update: {
...this.wizard,
proxyMode: ev.detail.value,
},
});
// We deliberately chose not to make the forms "controlled," but we do need this form to
// respond immediately to a state change in the wizard.
window.setTimeout(() => this.requestUpdate(), 0);
};
return html` <ak-text-input const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
name="externalHost" const el = ev.target as HTMLInputElement;
value=${ifDefined(provider?.externalHost)} this.showHttpBasic = el.checked;
required };
label=${msg("External host")}
.errorMessages=${errors?.externalHost ?? []} return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
help=${msg( <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
"The external URL you'll access the application at. Include any non-standard port.", ${renderForm(this.wizard.provider ?? {}, this.wizard.errors.provider ?? [], {
)} mode: this.wizard.proxyMode,
></ak-text-input> onSetMode,
<ak-text-input showHttpBasic: this.showHttpBasic,
name="internalHost" onSetShowHttpBasic,
value=${ifDefined(provider?.internalHost)} })}
.errorMessages=${errors?.internalHost ?? []} </form>`;
required
label=${msg("Internal host")}
help=${msg("Upstream host that the requests are forwarded to.")}
></ak-text-input>
<ak-switch-input
name="internalHostSslValidation"
?checked=${first(provider?.internalHostSslValidation, true)}
label=${msg("Internal host SSL Validation")}
help=${msg("Validate SSL Certificates of upstream servers.")}
>
</ak-switch-input>`;
} }
} }

View File

@ -1,48 +0,0 @@
import "@goauthentik/components/ak-text-input";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-single-forward-proxy")
export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
renderModeDescription() {
return html`<p class="pf-u-mb-xl">
${msg(
html`Use this provider with nginx's <code>auth_request</code> or traefik's
<code>forwardAuth</code>. Each application/domain needs its own provider.
Additionally, on each domain, <code>/outpost.goauthentik.io</code> must be
routed to the outpost (when using a managed outpost, this is done for you).`,
)}
</p>`;
}
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-text-input
name="externalHost"
value=${ifDefined(provider?.externalHost)}
required
label=${msg("External host")}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>`;
}
}
export default AkForwardSingleProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-single-forward-proxy": AkForwardSingleProxyApplicationWizardPage;
}
}

View File

@ -1,3 +1,5 @@
import { renderForm } from "@goauthentik/admin/providers/scim/SCIMProviderFormForm.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js"; import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit"; import { html } from "lit";

View File

@ -5,6 +5,7 @@ import {
type LDAPProviderRequest, type LDAPProviderRequest,
type OAuth2ProviderRequest, type OAuth2ProviderRequest,
type ProvidersSamlImportMetadataCreateRequest, type ProvidersSamlImportMetadataCreateRequest,
ProxyMode,
type ProxyProviderRequest, type ProxyProviderRequest,
type RACProviderRequest, type RACProviderRequest,
type RadiusProviderRequest, type RadiusProviderRequest,
@ -27,6 +28,7 @@ export interface ApplicationWizardState {
providerModel: string; providerModel: string;
app: Partial<ApplicationRequest>; app: Partial<ApplicationRequest>;
provider: OneOfProvider; provider: OneOfProvider;
proxyMode?: ProxyMode;
errors: ValidationError; errors: ValidationError;
} }

View File

@ -43,8 +43,12 @@ export class ProviderWizard extends AKElement {
@query("ak-wizard") @query("ak-wizard")
wizard?: Wizard; wizard?: Wizard;
async firstUpdated(): Promise<void> { connectedCallback() {
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(); super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
console.log(providerTypes);
this.providerTypes = providerTypes;
});
} }
render(): TemplateResult { render(): TemplateResult {

View File

@ -1,39 +1,18 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize"; import { CSSResult } from "lit";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css"; import PFList from "@patternfly/patternfly/components/List/list.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css"; import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import { import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api";
FlowsInstancesListDesignationEnum,
ProvidersApi,
ProxyMode,
ProxyProvider,
} from "@goauthentik/api";
import { import { SetMode, SetShowHttpBasic, renderForm } from "./ProxyProviderFormForm.js";
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
@customElement("ak-provider-proxy-form") @customElement("ak-provider-proxy-form")
export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> { export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
@ -73,376 +52,22 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
} }
} }
renderHttpBasic(): TemplateResult { renderForm() {
return html`<ak-text-input const onSetMode: SetMode = (ev) => {
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
help=${msg(
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
)}
>
</ak-text-input>`;
}
renderModeSelector(): TemplateResult {
const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => {
this.mode = ev.detail.value; this.mode = ev.detail.value;
}; };
// prettier-ignore const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
return html`
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode} data-ouid-component-name="proxy-type-toggle">
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
</ak-toggle-group>
`;
}
renderSettings(): TemplateResult {
switch (this.mode) {
case ProxyMode.Proxy:
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>
<ak-form-element-horizontal
label=${msg("External host")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Internal host")}
?required=${true}
name="internalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Upstream host that the requests are forwarded to.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.internalHostSslValidation, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Internal host SSL Validation")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg("Validate SSL Certificates of upstream servers.")}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardSingle:
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).",
)}
</p>
<ak-form-element-horizontal
label=${msg("External host")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardDomain:
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div class="pf-u-mb-xl">
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>
<ak-form-element-horizontal
label=${msg("Authentication URL")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${first(this.instance?.externalHost, window.location.origin)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Cookie domain")}
name="cookieDomain"
?required=${true}
>
<input
type="text"
value="${ifDefined(this.instance?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.UnknownDefaultOpenApi:
return html`<p>${msg("Unknown proxy mode")}</p>`;
}
}
renderForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
<div class="pf-c-card__footer">${this.renderSettings()}</div>
</div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
<input
type="text"
value="${first(this.instance?.accessTokenValidity, "hours=24")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg("Configure how long tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${this.instance?.certificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${this.mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">
${this.instance?.skipPathRegex}</textarea
>
<p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="interceptHeaderAuth">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Intercept header authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement; const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked; this.showHttpBasic = el.checked;
}} };
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Send HTTP-Basic Authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
</p>
</ak-form-element-horizontal>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group> return renderForm(this.instance ?? {}, [], {
<span slot="header"> ${msg("Advanced flow settings")} </span> mode: this.mode,
<div slot="body" class="pf-c-form"> onSetMode,
<ak-form-element-horizontal showHttpBasic: this.showHttpBasic,
label=${msg("Authentication flow")} onSetShowHttpBasic,
?required=${false} });
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
} }
} }

View File

@ -0,0 +1,381 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsInstancesListDesignationEnum, ProxyMode, ProxyProvider } from "@goauthentik/api";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
export type ProxyModeValue = { value: ProxyMode };
export type SetMode = (ev: CustomEvent<ProvxyModeValue>) => void;
export type SetShowHttpBasic = (ev: Event) => void;
export interface ProxyModeExtraArgs {
mode: ProxyMode;
onSetMode: SetMode;
showHttpBasic: boolean;
onSetShowHttpBasic: SetShowHttpBasic;
}
function renderHttpBasic(provider: ProxyProvider) {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(provider?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(provider?.basicAuthPasswordAttribute)}"
help=${msg("User/Group Attribute used for the password part of the HTTP-Basic Header.")}
>
</ak-text-input>`;
}
function renderModeSelector(mode: ProxyMode, onSet: SetMode) {
// prettier-ignore
return html` <ak-toggle-group
value=${mode}
@ak-toggle=${onSet}
data-ouid-component-name="proxy-type-toggle"
>
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
</ak-toggle-group>`;
}
function renderProxySettings(provider: ProxyProvider) {
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>
<ak-form-element-horizontal label=${msg("External host")} required name="externalHost">
<input
type="text"
value="${ifDefined(provider?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Internal host")} required name="internalHost">
<input
type="text"
value="${ifDefined(provider?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Upstream host that the requests are forwarded to.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.internalHostSslValidation ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Internal host SSL Validation")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg("Validate SSL Certificates of upstream servers.")}
</p>
</ak-form-element-horizontal>`;
}
function renderForwardSingleSettings(provider: ProxyProvider) {
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).",
)}
</p>
<ak-form-element-horizontal label=${msg("External host")} required name="externalHost">
<input
type="text"
value="${ifDefined(provider?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>`;
}
function renderForwardDomainSettings(provider: ProxyProvider) {
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div class="pf-u-mb-xl">
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>
<ak-form-element-horizontal label=${msg("Authentication URL")} required name="externalHost">
<input
type="text"
value="${provider?.externalHost ?? window.location.origin}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Cookie domain")} name="cookieDomain" required>
<input
type="text"
value="${ifDefined(provider?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
</p>
</ak-form-element-horizontal>`;
}
function renderSettings(provider: ProxyProvider, mode: ProxyMode) {
return match(mode)
.with(ProxyMode.Proxy, () => renderProxySettings(provider))
.with(ProxyMode.ForwardSingle, () => renderForwardSingleSettings(provider))
.with(ProxyMode.ForwardDomain, () => renderForwardDomainSettings(provider))
.exhaustive();
}
export function renderForm(
provider?: Partial<ProxyProvider>,
errors: ValidationError,
args: ProxyModeExtraArgs,
) {
const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args;
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(provider?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">${renderModeSelector(mode, onSetMode)}</div>
<div class="pf-c-card__footer">${renderSettings(provider, mode)}</div>
</div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
<input
type="text"
value="${provider?.accessTokenValidity ?? "hours=24"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${msg("Configure how long tokens are valid for.")}</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${provider?.certificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(provider?.propertyMappings)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">${provider?.skipPathRegex}</textarea>
<p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="interceptHeaderAuth">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.interceptHeaderAuth ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Intercept header authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.basicAuthEnabled ?? false}
@change=${onSetShowHttpBasic}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Send HTTP-Basic Authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
</p>
</ak-form-element-horizontal>
${showHttpBasic ? renderHttpBasic(provider) : nothing}
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -350,14 +350,12 @@ export function renderForm(
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Default relay state")} label=${msg("Default relay state")}
?required=${true}
name="defaultRelayState" name="defaultRelayState"
> >
<input <input
type="text" type="text"
value="${provider?.defaultRelayState || ""}" value="${provider?.defaultRelayState || ""}"
class="pf-c-form-control" class="pf-c-form-control"
required
/> />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(

View File

@ -21,6 +21,7 @@ import {
SourcesApi, SourcesApi,
Stage, Stage,
StagesApi, StagesApi,
StagesCaptchaListRequest,
StagesPasswordListRequest, StagesPasswordListRequest,
UserFieldsEnum, UserFieldsEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -140,19 +141,13 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
).stagesPasswordList(args); ).stagesPasswordList(args);
return stages.results; return stages.results;
}} }}
.groupBy=${(items: Stage[]) => { .groupBy=${(items: Stage[]) =>
return groupBy(items, (stage) => stage.verboseNamePlural); groupBy(items, (stage) => stage.verboseNamePlural)}
}} .renderElement=${(stage: Stage): string => stage.name}
.renderElement=${(stage: Stage): string => { .value=${(stage: Stage | undefined): string | undefined => stage?.pk}
return stage.name; .selected=${(stage: Stage): boolean =>
}} stage.pk === this.instance?.passwordStage}
.value=${(stage: Stage | undefined): string | undefined => { blankable
return stage?.pk;
}}
.selected=${(stage: Stage): boolean => {
return stage.pk === this.instance?.passwordStage;
}}
?blankable=${true}
> >
</ak-search-select> </ak-search-select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
@ -161,6 +156,35 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Captcha stage")} name="captchaStage">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
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
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="caseInsensitiveMatching"> <ak-form-element-horizontal name="caseInsensitiveMatching">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -6,7 +6,7 @@ import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/ba
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -25,62 +25,7 @@ import {
FlowsApi, FlowsApi,
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate") const customCSS = css`
export class AuthenticatorValidateStage
extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
>
implements StageHost
{
flowSlug = "";
set loading(value: boolean) {
this.host.loading = value;
}
get loading(): boolean {
return this.host.loading;
}
get brand(): CurrentBrand | undefined {
return this.host.brand;
}
@state()
_selectedDeviceChallenge?: DeviceChallenge;
set selectedDeviceChallenge(value: DeviceChallenge | undefined) {
const previousChallenge = this._selectedDeviceChallenge;
this._selectedDeviceChallenge = value;
if (!value) return;
if (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({
flowSlug: this.host?.flowSlug || "",
query: window.location.search.substring(1),
flowChallengeResponseRequest: {
// @ts-ignore
component: this.challenge.component || "",
selectedChallenge: value,
},
});
}
get selectedDeviceChallenge(): DeviceChallenge | undefined {
return this._selectedDeviceChallenge;
}
submit(
payload: AuthenticatorValidationChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> {
return this.host?.submit(payload, options) || Promise.resolve();
}
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
ul { ul {
padding-top: 1rem; padding-top: 1rem;
} }
@ -109,7 +54,103 @@ export class AuthenticatorValidateStage
.right > * { .right > * {
height: 50%; height: 50%;
} }
`); `;
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage
extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
>
implements StageHost
{
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, customCSS];
}
flowSlug = "";
set loading(value: boolean) {
this.host.loading = value;
}
get loading(): boolean {
return this.host.loading;
}
get brand(): CurrentBrand | undefined {
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 === 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({
flowSlug: this.host?.flowSlug || "",
query: window.location.search.substring(1),
flowChallengeResponseRequest: {
// @ts-ignore
component: this.challenge.component || "",
selectedChallenge: value,
},
});
}
get selectedDeviceChallenge(): DeviceChallenge | undefined {
return this._selectedDeviceChallenge;
}
submit(
payload: AuthenticatorValidationChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> {
return this.host?.submit(payload, options) || Promise.resolve();
}
willUpdate(_changed: PropertyValues<this>) {
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) { renderDevicePickerSingle(deviceChallenge: DeviceChallenge) {
@ -228,26 +269,8 @@ export class AuthenticatorValidateStage
} }
render(): TemplateResult { render(): TemplateResult {
if (!this.challenge) { return this.challenge
return html`<ak-empty-state loading> </ak-empty-state>`; ? html`<header class="pf-c-login__main-header">
}
// 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`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1> <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header> </header>
${this.selectedDeviceChallenge ${this.selectedDeviceChallenge
@ -266,7 +289,8 @@ export class AuthenticatorValidateStage
</div> </div>
<footer class="pf-c-login__main-footer"> <footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul> <ul class="pf-c-login__main-footer-links"></ul>
</footer>`}`; </footer>`}`
: html`<ak-empty-state loading> </ak-empty-state>`;
} }
} }

View File

@ -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 { render(): TemplateResult {
if (!this.challenge) { if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`; return html`<ak-empty-state loading> </ak-empty-state>`;
@ -44,19 +72,8 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
> >
${this.renderUserInfo()} ${this.renderUserInfo()}
<div class="icon-description"> <div class="icon-description">
<i <i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms <p>${this.deviceMessage()}</p>
? "fa-key"
: "fa-mobile-alt"}"
aria-hidden="true"
></i>
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
: html`<p>
${msg(
"Open your two-factor authenticator app to view your authentication code.",
)}
</p>`}
</div> </div>
<ak-form-element <ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static

View File

@ -59,7 +59,7 @@ export class BaseDeviceStage<
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined; (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}} }}
> >
${msg("Return to device picker")} ${msg("Select another authentication method")}
</button>`; </button>`;
} }
} }

View File

@ -6,8 +6,8 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import type { TurnstileObject } from "turnstile-types"; import type { TurnstileObject } from "turnstile-types";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; import { CSSResult, PropertyValues, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -22,6 +22,7 @@ import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/
interface TurnstileWindow extends Window { interface TurnstileWindow extends Window {
turnstile: TurnstileObject; turnstile: TurnstileObject;
} }
type TokenHandler = (token: string) => void;
const captchaContainerID = "captcha-container"; const captchaContainerID = "captcha-container";
@ -45,6 +46,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
@state() @state()
scriptElement?: HTMLScriptElement; scriptElement?: HTMLScriptElement;
@property()
onTokenChange: TokenHandler = (token: string) => {
this.host.submit({ component: "ak-stage-captcha", token });
};
constructor() { constructor() {
super(); super();
this.captchaContainer = document.createElement("div"); this.captchaContainer = document.createElement("div");
@ -102,11 +108,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
grecaptcha.ready(() => { grecaptcha.ready(() => {
const captchaId = grecaptcha.render(this.captchaContainer, { const captchaId = grecaptcha.render(this.captchaContainer, {
sitekey: this.challenge.siteKey, sitekey: this.challenge.siteKey,
callback: (token) => { callback: this.onTokenChange,
this.host?.submit({
token: token,
});
},
size: "invisible", size: "invisible",
}); });
grecaptcha.execute(captchaId); grecaptcha.execute(captchaId);
@ -122,12 +124,8 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
document.body.appendChild(this.captchaContainer); document.body.appendChild(this.captchaContainer);
const captchaId = hcaptcha.render(this.captchaContainer, { const captchaId = hcaptcha.render(this.captchaContainer, {
sitekey: this.challenge.siteKey, sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
size: "invisible", size: "invisible",
callback: (token) => {
this.host?.submit({
token: token,
});
},
}); });
hcaptcha.execute(captchaId); hcaptcha.execute(captchaId);
return true; return true;
@ -141,16 +139,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
document.body.appendChild(this.captchaContainer); document.body.appendChild(this.captchaContainer);
(window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, { (window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, {
sitekey: this.challenge.siteKey, sitekey: this.challenge.siteKey,
callback: (token) => { callback: this.onTokenChange,
this.host?.submit({
token: token,
});
},
}); });
return true; return true;
} }
renderBody(): TemplateResult { renderBody() {
if (this.error) { if (this.error) {
return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`; return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
} }
@ -160,7 +154,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`; return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
} }
render(): TemplateResult { render() {
if (!this.challenge) { if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`; return html`<ak-empty-state loading> </ak-empty-state>`;
} }

View File

@ -4,10 +4,11 @@ import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/components/ak-flow-password-input.js"; import "@goauthentik/flow/components/ak-flow-password-input.js";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
import "@goauthentik/flow/stages/captcha/CaptchaStage";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; 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 PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -46,6 +47,9 @@ export class IdentificationStage extends BaseStage<
> { > {
form?: HTMLFormElement; form?: HTMLFormElement;
@state()
captchaToken = "";
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -274,6 +278,18 @@ export class IdentificationStage extends BaseStage<
` `
: nothing} : nothing}
${this.renderNonFieldErrors()} ${this.renderNonFieldErrors()}
${this.challenge.captchaStage
? html`
<input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
<ak-stage-captcha
style="visibility: hidden; position:absolute;"
.challenge=${this.challenge.captchaStage}
.onTokenChange=${(token: string) => {
this.captchaToken = token;
}}
></ak-stage-captcha>
`
: nothing}
<div class="pf-c-form__group pf-m-action"> <div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block"> <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primaryAction} ${this.challenge.primaryAction}

View File

@ -89,7 +89,12 @@ export async function setFormGroup(name: string | RegExp, setting: "open" | "clo
typeof name === "string" ? (sample) => sample === name : (sample) => name.test(sample); typeof name === "string" ? (sample) => sample === name : (sample) => name.test(sample);
const formGroup = await (async () => { const formGroup = await (async () => {
for await (const group of $$("ak-form-group")) { for await (const group of browser.$$("ak-form-group")) {
// Delightfully, wizards may have slotted elements that *exist* but are not *attached*,
// and this can break the damn tests.
if (!(await group.isDisplayed())) {
continue;
}
if ( if (
comparator(await group.$("div.pf-c-form__field-group-header-title-text").getText()) comparator(await group.$("div.pf-c-form__field-group-header-title-text").getText())
) { ) {
@ -112,8 +117,7 @@ export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
const buttons = await context.$$("button"); const buttons = await context.$$("button");
let button: WebdriverIO.Element; let button: WebdriverIO.Element;
for (const b of buttons) { for (const b of buttons) {
const label = await b.getText(); if (b.isDisplayed() && (await b.getText()).indexOf(name) !== -1) {
if (label.indexOf(name) !== -1) {
button = b; button = b;
break; break;
} }
@ -123,16 +127,9 @@ export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
await doBlur(button); await doBlur(button);
} }
const tap = <T>(a: T): T => {
console.log(a);
return a;
};
export async function clickToggleGroup(name: string, value: string | RegExp) { export async function clickToggleGroup(name: string, value: string | RegExp) {
const comparator = const comparator =
typeof name === "string" typeof name === "string" ? (sample) => sample === value : (sample) => value.test(sample);
? (sample) => tap(sample) === tap(value)
: (sample) => value.test(sample);
const button = await (async () => { const button = await (async () => {
for await (const button of $(`[data-ouid-component-name=${name}]`).$$( for await (const button of $(`[data-ouid-component-name=${name}]`).$$(

View File

@ -35,8 +35,8 @@ export const simpleLDAPProviderForm: TestProvider = () => [
[clickButton, "Next"], [clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")], [setTextInput, "name", newObjectName("New LDAP Provider")],
// This will never not weird me out. // This will never not weird me out.
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
[setFormGroup, /Flow settings/, "open"], [setFormGroup, /Flow settings/, "open"],
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
[setSearchSelect, "invalidationFlow", "default-invalidation-flow"], [setSearchSelect, "invalidationFlow", "default-invalidation-flow"],
]; ];

View File

@ -5036,10 +5036,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3cd84e82e83e35ad"> <trans-unit id="s3cd84e82e83e35ad">
<source>Please enter your code</source> <source>Please enter your code</source>
</trans-unit> </trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Zurück zur Geräteauswahl</target>
</trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
<target>Authentifizierung erneut versuchen</target> <target>Authentifizierung erneut versuchen</target>
@ -7015,6 +7011,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -5294,10 +5294,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Please enter your code</source> <source>Please enter your code</source>
<target>Please enter your code</target> <target>Please enter your code</target>
</trans-unit> </trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Return to device picker</target>
</trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
<target>Retry authentication</target> <target>Retry authentication</target>
@ -7280,6 +7276,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4962,10 +4962,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3cd84e82e83e35ad"> <trans-unit id="s3cd84e82e83e35ad">
<source>Please enter your code</source> <source>Please enter your code</source>
</trans-unit> </trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Regresar al selector de dispositivos</target>
</trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
<target>Reintentar la autenticación</target> <target>Reintentar la autenticación</target>
@ -6932,6 +6928,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6616,11 +6616,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source>Please enter your code</source> <source>Please enter your code</source>
<target>Veuillez saisir votre code</target> <target>Veuillez saisir votre code</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Retourner à la sélection d'appareil</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -9131,90 +9126,128 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit> </trans-unit>
<trans-unit id="sbfee780fa0a2c83e"> <trans-unit id="sbfee780fa0a2c83e">
<source>Device type <x id="0" equiv-text="${device.verboseName}"/> cannot be deleted</source> <source>Device type <x id="0" equiv-text="${device.verboseName}"/> cannot be deleted</source>
<target>Le type d'appareil <x id="0" equiv-text="${device.verboseName}"/> ne peut pas être supprimé</target>
</trans-unit> </trans-unit>
<trans-unit id="s336936629cdeb3e5"> <trans-unit id="s336936629cdeb3e5">
<source>Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows.</source> <source>Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows.</source>
<target>Étape utilisée pour vérifier le navigateur des utilisateurs avec le connecteur de confiance des appareils Google Chrome Enterprise. Cette étape peut être utilisée dans les flux d'authentification et d'autorisation.</target>
</trans-unit> </trans-unit>
<trans-unit id="s85fe794c71b4ace8"> <trans-unit id="s85fe794c71b4ace8">
<source>Google Verified Access API</source> <source>Google Verified Access API</source>
<target>API Google Verified Access</target>
</trans-unit> </trans-unit>
<trans-unit id="s013620384af7c8b4"> <trans-unit id="s013620384af7c8b4">
<source>Device type <x id="0" equiv-text="${device.verboseName}"/> cannot be edited</source> <source>Device type <x id="0" equiv-text="${device.verboseName}"/> cannot be edited</source>
<target>Le type d'appareil <x id="0" equiv-text="${device.verboseName}"/> ne peut pas être édité</target>
</trans-unit> </trans-unit>
<trans-unit id="s4347135696fc7cde"> <trans-unit id="s4347135696fc7cde">
<source>Advanced flow settings</source> <source>Advanced flow settings</source>
<target>Paramètres avancés des flux</target>
</trans-unit> </trans-unit>
<trans-unit id="sf52ff57fd136cc2f"> <trans-unit id="sf52ff57fd136cc2f">
<source>Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.</source> <source>Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.</source>
<target>Activer cette option pour écrire les changements de mot de passe fait dans authentik dans Kerberos. Ignoré si la synchronisation est désactivée.</target>
</trans-unit> </trans-unit>
<trans-unit id="s14a16542f956e11d"> <trans-unit id="s14a16542f956e11d">
<source>Realm settings</source> <source>Realm settings</source>
<target>Paramètres du realm</target>
</trans-unit> </trans-unit>
<trans-unit id="s9c2eae548d3c1c30"> <trans-unit id="s9c2eae548d3c1c30">
<source>Realm</source> <source>Realm</source>
<target>Realm</target>
</trans-unit> </trans-unit>
<trans-unit id="s6b032212997e2491"> <trans-unit id="s6b032212997e2491">
<source>Kerberos 5 configuration</source> <source>Kerberos 5 configuration</source>
<target>Configuration Kerberos 5</target>
</trans-unit> </trans-unit>
<trans-unit id="sbf50181022f47de3"> <trans-unit id="sbf50181022f47de3">
<source>Kerberos 5 configuration. See man krb5.conf(5) for configuration format. If left empty, a default krb5.conf will be used.</source> <source>Kerberos 5 configuration. See man krb5.conf(5) for configuration format. If left empty, a default krb5.conf will be used.</source>
<target>Configuration Kerbers 5. Cf. man krb5.conf(5) pour le format de configuration. Si laissé vide, un krb5.conf par défaut sera utilisé.</target>
</trans-unit> </trans-unit>
<trans-unit id="s2386539a0bd62fab"> <trans-unit id="s2386539a0bd62fab">
<source>Sync connection settings</source> <source>Sync connection settings</source>
<target>Paramètres de synchronisation</target>
</trans-unit> </trans-unit>
<trans-unit id="s0d1a6f3fe81351f8"> <trans-unit id="s0d1a6f3fe81351f8">
<source>Sync principal</source> <source>Sync principal</source>
<target>Principal de synchronisation</target>
</trans-unit> </trans-unit>
<trans-unit id="sa691d6e1974295fa"> <trans-unit id="sa691d6e1974295fa">
<source>Principal used to authenticate to the KDC for syncing.</source> <source>Principal used to authenticate to the KDC for syncing.</source>
<target>Principal utilisé pour s'authentifier au KDC pour synchroniser.</target>
</trans-unit> </trans-unit>
<trans-unit id="s977b9c629eed3d33"> <trans-unit id="s977b9c629eed3d33">
<source>Sync password</source> <source>Sync password</source>
<target>Mot de passe de synchronisation</target>
</trans-unit> </trans-unit>
<trans-unit id="s77772860385de948"> <trans-unit id="s77772860385de948">
<source>Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.</source> <source>Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.</source>
<target>Mot de passe utilisé pour s'authentifier au KDC pour synchroniser. Optional si une keytab de synchronisation ou un credentials cache de synchronisation est fourni.</target>
</trans-unit> </trans-unit>
<trans-unit id="sc59ec59c3d5e74dc"> <trans-unit id="sc59ec59c3d5e74dc">
<source>Sync keytab</source> <source>Sync keytab</source>
<target>Keytab de synchronisation</target>
</trans-unit> </trans-unit>
<trans-unit id="scd42997958453f05"> <trans-unit id="scd42997958453f05">
<source>Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.</source> <source>Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.</source>
<target>Keytab utilisée pour s'authentifier au KDC pour synchroniser. Optional si un mot de passe de synchronisation ou un credentials cache de synchronisation est fourni. Doit être encodé en base64 ou de la forme TYPE:residual.</target>
</trans-unit> </trans-unit>
<trans-unit id="s60eaf439ccdca1f2"> <trans-unit id="s60eaf439ccdca1f2">
<source>Sync credentials cache</source> <source>Sync credentials cache</source>
<target>Credentials cache de synchronisation</target>
</trans-unit> </trans-unit>
<trans-unit id="s95722900b0c9026f"> <trans-unit id="s95722900b0c9026f">
<source>Credentials cache used to authenticate to the KDC for syncing. Optional if Sync password or Sync keytab is provided. Must be in the form TYPE:residual.</source> <source>Credentials cache used to authenticate to the KDC for syncing. Optional if Sync password or Sync keytab is provided. Must be in the form TYPE:residual.</source>
<target>Credentials cache utilisé pour s'authentifier au KDC pour synchroniser. Optional si un mot de passe de synchronisation ou une keytab de synchronisation est fourni. Doit être de la forme TYPE:residual.</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9c055db98d7994a"> <trans-unit id="sf9c055db98d7994a">
<source>SPNEGO settings</source> <source>SPNEGO settings</source>
<target>Paramètres SPNEGO</target>
</trans-unit> </trans-unit>
<trans-unit id="sab580a45dc46937f"> <trans-unit id="sab580a45dc46937f">
<source>SPNEGO server name</source> <source>SPNEGO server name</source>
<target>Nom de serveur SPNEGO</target>
</trans-unit> </trans-unit>
<trans-unit id="s7a79d6174d17ab2d"> <trans-unit id="s7a79d6174d17ab2d">
<source>Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain</source> <source>Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain</source>
<target>Force l'utilisation d'un nom de serveur spécifique pour SPNEGO. Doit être de la forme HTTP@hostname</target>
</trans-unit> </trans-unit>
<trans-unit id="sa4ba2b2081472ccd"> <trans-unit id="sa4ba2b2081472ccd">
<source>SPNEGO keytab</source> <source>SPNEGO keytab</source>
<target>Keytab SPNEGO</target>
</trans-unit> </trans-unit>
<trans-unit id="s64adda975c1106c0"> <trans-unit id="s64adda975c1106c0">
<source>Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.</source> <source>Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.</source>
<target>Keytab utilisée pour SPNEGO. Optional si un credentials cache SPNEGO est fourni. Doit être encodé en base64 ou de la forme TYPE:residual.</target>
</trans-unit> </trans-unit>
<trans-unit id="s92247825b92587b5"> <trans-unit id="s92247825b92587b5">
<source>SPNEGO credentials cache</source> <source>SPNEGO credentials cache</source>
<target>Credentials cache SPNEGO</target>
</trans-unit> </trans-unit>
<trans-unit id="sd9757c345e4062f8"> <trans-unit id="sd9757c345e4062f8">
<source>Credentials cache used for SPNEGO. Optional if SPNEGO keytab is provided. Must be in the form TYPE:residual.</source> <source>Credentials cache used for SPNEGO. Optional if SPNEGO keytab is provided. Must be in the form TYPE:residual.</source>
<target>Credentials cache utilisé pour SPNEGO. Optional si une keytab SPNEGO est fournie. Doit être de la forme TYPE:residual.</target>
</trans-unit> </trans-unit>
<trans-unit id="s734ab8fbcae0b69e"> <trans-unit id="s734ab8fbcae0b69e">
<source>Kerberos Attribute mapping</source> <source>Kerberos Attribute mapping</source>
<target>Mappage d'attributs Kerberos</target>
</trans-unit> </trans-unit>
<trans-unit id="s2c378e86e025fdb2"> <trans-unit id="s2c378e86e025fdb2">
<source>Update Kerberos Source</source> <source>Update Kerberos Source</source>
<target>Mettre à jour la source Kerberos</target>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
<target>Base de données utilisateurs + mot de passe Kerberos</target>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6590,11 +6590,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Please enter your code</source> <source>Please enter your code</source>
<target>코드를 입력하세요.</target> <target>코드를 입력하세요.</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>디바이스 선택기로 돌아가기</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -8849,6 +8844,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6575,11 +6575,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<source>Please enter your code</source> <source>Please enter your code</source>
<target>Voer uw code in</target> <target>Voer uw code in</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Terug naar apparaatkeuze</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -8695,6 +8690,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6620,11 +6620,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<source>Please enter your code</source> <source>Please enter your code</source>
<target>Proszę wprowadź swój kod</target> <target>Proszę wprowadź swój kod</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Wróć do wyboru urządzeń</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -9114,6 +9109,15 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6578,11 +6578,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Please enter your code</source> <source>Please enter your code</source>
<target>Ƥĺēàśē ēńţēŕ ŷōũŕ ćōďē</target> <target>Ƥĺēàśē ēńţēŕ ŷōũŕ ćōďē</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Ŕēţũŕń ţō ďēvĩćē ƥĩćķēŕ</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -9154,4 +9149,13 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit> </trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit>
</body></file></xliff> </body></file></xliff>

View File

@ -6619,11 +6619,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Please enter your code</source> <source>Please enter your code</source>
<target>Пожалуйста, введите ваш код</target> <target>Пожалуйста, введите ваш код</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Вернуться к выбору устройства</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -9177,6 +9172,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4955,10 +4955,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3cd84e82e83e35ad"> <trans-unit id="s3cd84e82e83e35ad">
<source>Please enter your code</source> <source>Please enter your code</source>
</trans-unit> </trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>Aygıt seçiciye geri dön</target>
</trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
<target>Kimlik doğrulamayı yeniden deneyin</target> <target>Kimlik doğrulamayı yeniden deneyin</target>
@ -6925,6 +6921,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4712,9 +4712,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3cd84e82e83e35ad"> <trans-unit id="s3cd84e82e83e35ad">
<source>Please enter your code</source> <source>Please enter your code</source>
</trans-unit> </trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
</trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
</trans-unit> </trans-unit>
@ -5863,6 +5860,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit> </trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -6618,11 +6618,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Please enter your code</source> <source>Please enter your code</source>
<target>请输入您的代码</target> <target>请输入您的代码</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>返回设备选择器</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -9217,6 +9212,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4999,10 +4999,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3cd84e82e83e35ad"> <trans-unit id="s3cd84e82e83e35ad">
<source>Please enter your code</source> <source>Please enter your code</source>
</trans-unit> </trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>返回设备选择器</target>
</trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
<target>重试身份验证</target> <target>重试身份验证</target>
@ -6973,6 +6969,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6566,11 +6566,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Please enter your code</source> <source>Please enter your code</source>
<target>請輸入您的認證碼</target> <target>請輸入您的認證碼</target>
</trans-unit>
<trans-unit id="s18b910437b73e8e8">
<source>Return to device picker</source>
<target>回到選擇裝置頁面</target>
</trans-unit> </trans-unit>
<trans-unit id="se409d01b52c4e12f"> <trans-unit id="se409d01b52c4e12f">
<source>Retry authentication</source> <source>Retry authentication</source>
@ -8810,6 +8805,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s03e4044abe0b556c"> <trans-unit id="s03e4044abe0b556c">
<source>User database + Kerberos password</source> <source>User database + Kerberos password</source>
</trans-unit>
<trans-unit id="s98bb2ae796f1ceef">
<source>Select another authentication method</source>
</trans-unit>
<trans-unit id="s21d95b4651ad7a1e">
<source>Enter a one-time recovery code for this user.</source>
</trans-unit>
<trans-unit id="s2e1d5a7d320c25ef">
<source>Enter the code from your authenticator device.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -5,10 +5,10 @@ title: Authenticator validation stage
This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages: This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages:
- [Duo authenticator stage](../authenticator_duo/index.md) - [Duo authenticator stage](../authenticator_duo/index.md)
- [SMS authenticator stage](../authenticator_sms/index.md). - [SMS authenticator stage](../authenticator_sms/index.md)
- [Static authenticator stage](../authenticator_static/index.md). - [Static authenticator stage](../authenticator_static/index.md)
- [TOTP authenticator stage](../authenticator_totp/index.md) - [TOTP authenticator stage](../authenticator_totp/index.md)
- [WebAuth authenticator stage](../authenticator_webauthn/index.md). - [WebAuthn authenticator stage](../authenticator_webauthn/index.md)
You can select which type of device classes are allowed. You can select which type of device classes are allowed.
@ -75,3 +75,7 @@ Optionally restrict which WebAuthn device types can be used to authenticate.
When no restriction is set, all WebAuthn devices a user has registered are allowed. When no restriction is set, all WebAuthn devices a user has registered are allowed.
These restrictions only apply to WebAuthn devices created with authentik 2024.4 or later. These restrictions only apply to WebAuthn devices created with authentik 2024.4 or later.
#### Automatic device selection
If the user has more than one device, the user is prompted to select which device they want to use for validation. After the user successfully authenticates with a certain device, that device is marked as "last used". In subsequent prompts by the Authenticator validation stage, the last used device is automatically selected for the user. Should they wish to use another device, the user can return to the device selection screen.

View File

@ -16,7 +16,15 @@ Select which fields the user can use to identify themselves. Multiple fields can
## Password stage ## Password stage
To prompt users for their password on the same step as identifying themselves, a password stage can be selected here. If a password stage is selected in the Identification stage, the password stage should not be bound to the flow. To prompt users for their password on the same step as identifying themselves, a Password stage can be selected here. If a Password stage is selected in the Identification stage, the Password stage should not be bound to the flow.
## CAPTCHA stage
:::warning
The CAPTCHA stage you use must be configured to use the "Invisible" mode, otherwise the widget will be rendered incorrectly.
:::
To run a CAPTCHA process in the background while the user is entering their identification, a CAPTCHA stage can be selected here. If a CAPTCHA stage is selected in the Identification stage, the CAPTCHA stage should not be bound to the flow.
## Enrollment/Recovery Flow ## Enrollment/Recovery Flow

View File

@ -1,23 +1,8 @@
import React from "react"; import React from "react";
import clsx from "clsx"; import { Redirect } from "@docusaurus/router";
import Layout from "@theme/Layout";
import BrowserOnly from "@docusaurus/BrowserOnly";
function Home() { function Home() {
return ( return <Redirect to="/docs" />;
<Layout title={`authentik Documentation`}>
<BrowserOnly>
{() => {
window.location.href = "/docs";
}}
</BrowserOnly>
<header className={clsx("hero hero--primary")}>
<div className="container">
<h1 className="hero__title">authentik Documentation</h1>
</div>
</header>
</Layout>
);
} }
export default Home; export default Home;