diff --git a/authentik/flows/tests/test_inspector.py b/authentik/flows/tests/test_inspector.py index 2a01ea370c..81a04a797b 100644 --- a/authentik/flows/tests/test_inspector.py +++ b/authentik/flows/tests/test_inspector.py @@ -46,6 +46,7 @@ class TestFlowInspector(APITestCase): res.content, { "allow_show_password": False, + "captcha_stage": None, "component": "ak-stage-identification", "flow_info": { "background": flow.background_url, diff --git a/authentik/providers/scim/clients/groups.py b/authentik/providers/scim/clients/groups.py index 44b3405dff..8221d7d131 100644 --- a/authentik/providers/scim/clients/groups.py +++ b/authentik/providers/scim/clients/groups.py @@ -197,6 +197,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): chunk_size = self._config.bulk.maxOperations if chunk_size < 1: chunk_size = len(ops) + if len(ops) < 1: + return for chunk in batched(ops, chunk_size): req = PatchRequest(Operations=list(chunk)) self._request( @@ -237,13 +239,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): users_to_add = [] users_to_remove = [] # Check users currently in group and if they shouldn't be in the group and remove them - for user in current_group.members: + for user in current_group.members or []: if user.value not in users_should: users_to_remove.append(user.value) # Check users that should be in the group and add them for user in users_should: if len([x for x in current_group.members if x.value == user]) < 1: users_to_add.append(user) + # Only send request if we need to make changes + if len(users_to_add) < 1 and len(users_to_remove) < 1: + return return self._patch_chunked( scim_group.scim_id, *[ diff --git a/authentik/stages/authenticator_validate/challenge.py b/authentik/stages/authenticator_validate/challenge.py index c11439684f..1f9a656a38 100644 --- a/authentik/stages/authenticator_validate/challenge.py +++ b/authentik/stages/authenticator_validate/challenge.py @@ -8,7 +8,7 @@ from django.http.response import Http404 from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as __ from django.utils.translation import gettext_lazy as _ -from rest_framework.fields import CharField +from rest_framework.fields import CharField, DateTimeField from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger from webauthn import options_to_json @@ -45,6 +45,7 @@ class DeviceChallenge(PassiveSerializer): device_class = CharField() device_uid = CharField() challenge = JSONDictField() + last_used = DateTimeField(allow_null=True) def get_challenge_for_device( diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index 96ae7e6215..bde76d37db 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -217,6 +217,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): "device_class": device_class, "device_uid": device.pk, "challenge": get_challenge_for_device(self.request, stage, device), + "last_used": device.last_used, } ) challenge.is_valid() @@ -237,6 +238,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): self.request, self.executor.current_stage, ), + "last_used": None, } ) challenge.is_valid() diff --git a/authentik/stages/authenticator_validate/tests/test_sms.py b/authentik/stages/authenticator_validate/tests/test_sms.py index 5cce796207..850854a892 100644 --- a/authentik/stages/authenticator_validate/tests/test_sms.py +++ b/authentik/stages/authenticator_validate/tests/test_sms.py @@ -107,6 +107,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase): "device_class": "sms", "device_uid": str(device.pk), "challenge": {}, + "last_used": None, }, }, ) diff --git a/authentik/stages/authenticator_validate/tests/test_stage.py b/authentik/stages/authenticator_validate/tests/test_stage.py index 98fe5d2fe4..82a2f51322 100644 --- a/authentik/stages/authenticator_validate/tests/test_stage.py +++ b/authentik/stages/authenticator_validate/tests/test_stage.py @@ -169,6 +169,7 @@ class AuthenticatorValidateStageTests(FlowTestCase): "device_class": "baz", "device_uid": "quox", "challenge": {}, + "last_used": None, } }, ) @@ -188,6 +189,7 @@ class AuthenticatorValidateStageTests(FlowTestCase): "device_class": "static", "device_uid": "1", "challenge": {}, + "last_used": None, }, }, ) diff --git a/authentik/stages/authenticator_validate/tests/test_webauthn.py b/authentik/stages/authenticator_validate/tests/test_webauthn.py index e4247b221a..05fe216081 100644 --- a/authentik/stages/authenticator_validate/tests/test_webauthn.py +++ b/authentik/stages/authenticator_validate/tests/test_webauthn.py @@ -274,6 +274,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): "device_class": device.__class__.__name__.lower().replace("device", ""), "device_uid": device.pk, "challenge": {}, + "last_used": None, } ] session[SESSION_KEY_PLAN] = plan @@ -352,6 +353,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): "device_class": device.__class__.__name__.lower().replace("device", ""), "device_uid": device.pk, "challenge": {}, + "last_used": None, } ] session[SESSION_KEY_PLAN] = plan @@ -432,6 +434,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): "device_class": device.__class__.__name__.lower().replace("device", ""), "device_uid": device.pk, "challenge": {}, + "last_used": None, } ] session[SESSION_KEY_PLAN] = plan diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py index 3967e6d3d3..73bcff5dec 100644 --- a/authentik/stages/captcha/stage.py +++ b/authentik/stages/captcha/stage.py @@ -1,10 +1,11 @@ """authentik captcha stage""" from django.http.response import HttpResponse -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext as _ from requests import RequestException from rest_framework.fields import CharField from rest_framework.serializers import ValidationError +from structlog.stdlib import get_logger from authentik.flows.challenge import ( Challenge, @@ -16,6 +17,7 @@ from authentik.lib.utils.http import get_http_session from authentik.root.middleware import ClientIPMiddleware from authentik.stages.captcha.models import CaptchaStage +LOGGER = get_logger() PLAN_CONTEXT_CAPTCHA = "captcha" @@ -27,6 +29,56 @@ class CaptchaChallenge(WithUserInfoChallenge): component = CharField(default="ak-stage-captcha") +def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str): + """Validate captcha token""" + try: + response = get_http_session().post( + stage.api_url, + headers={ + "Content-type": "application/x-www-form-urlencoded", + }, + data={ + "secret": stage.private_key, + "response": token, + "remoteip": remote_ip, + }, + ) + response.raise_for_status() + data = response.json() + if stage.error_on_invalid_score: + if not data.get("success", False): + error_codes = data.get("error-codes", ["unknown-error"]) + LOGGER.warning("Failed to verify captcha token", error_codes=error_codes) + + # These cases can usually be fixed by simply requesting a new token and retrying. + # [reCAPTCHA](https://developers.google.com/recaptcha/docs/verify#error_code_reference) + # [hCaptcha](https://docs.hcaptcha.com/#siteverify-error-codes-table) + # [Turnstile](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes) + retriable_error_codes = [ + "missing-input-response", + "invalid-input-response", + "timeout-or-duplicate", + "expired-input-response", + "already-seen-response", + ] + + if set(error_codes).issubset(set(retriable_error_codes)): + error_message = _("Invalid captcha response. Retrying may solve this issue.") + else: + error_message = _("Invalid captcha response") + raise ValidationError(error_message) + if "score" in data: + score = float(data.get("score")) + if stage.score_max_threshold > -1 and score > stage.score_max_threshold: + raise ValidationError(_("Invalid captcha response")) + if stage.score_min_threshold > -1 and score < stage.score_min_threshold: + raise ValidationError(_("Invalid captcha response")) + except (RequestException, TypeError) as exc: + raise ValidationError(_("Failed to validate token")) from exc + + return data + + class CaptchaChallengeResponse(ChallengeResponse): """Validate captcha token""" @@ -36,38 +88,9 @@ class CaptchaChallengeResponse(ChallengeResponse): def validate_token(self, token: str) -> str: """Validate captcha token""" stage: CaptchaStage = self.stage.executor.current_stage - try: - response = get_http_session().post( - stage.api_url, - headers={ - "Content-type": "application/x-www-form-urlencoded", - }, - data={ - "secret": stage.private_key, - "response": token, - "remoteip": ClientIPMiddleware.get_client_ip(self.stage.request), - }, - ) - response.raise_for_status() - data = response.json() - if stage.error_on_invalid_score: - if not data.get("success", False): - raise ValidationError( - _( - "Failed to validate token: {error}".format( - error=data.get("error-codes", _("Unknown error")) - ) - ) - ) - if "score" in data: - score = float(data.get("score")) - if stage.score_max_threshold > -1 and score > stage.score_max_threshold: - raise ValidationError(_("Invalid captcha response")) - if stage.score_min_threshold > -1 and score < stage.score_min_threshold: - raise ValidationError(_("Invalid captcha response")) - except (RequestException, TypeError) as exc: - raise ValidationError(_("Failed to validate token")) from exc - return data + client_ip = ClientIPMiddleware.get_client_ip(self.stage.request) + + return verify_captcha_token(stage, token, client_ip) class CaptchaStageView(ChallengeStageView): diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py index 9ad97320e8..c8de2d7436 100644 --- a/authentik/stages/identification/api.py +++ b/authentik/stages/identification/api.py @@ -27,6 +27,7 @@ class IdentificationStageSerializer(StageSerializer): fields = StageSerializer.Meta.fields + [ "user_fields", "password_stage", + "captcha_stage", "case_insensitive_matching", "show_matched_user", "enrollment_flow", @@ -46,6 +47,7 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet): filterset_fields = [ "name", "password_stage", + "captcha_stage", "case_insensitive_matching", "show_matched_user", "enrollment_flow", diff --git a/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py new file mode 100644 index 0000000000..734dc7631c --- /dev/null +++ b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.8 on 2024-08-29 11:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"), + ("authentik_stages_identification", "0014_identificationstage_pretend"), + ] + + operations = [ + migrations.AddField( + model_name="identificationstage", + name="captcha_stage", + field=models.ForeignKey( + default=None, + help_text="When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="authentik_stages_captcha.captchastage", + ), + ), + ] diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py index 27cfcb92f1..ed6728c932 100644 --- a/authentik/stages/identification/models.py +++ b/authentik/stages/identification/models.py @@ -8,6 +8,7 @@ from rest_framework.serializers import BaseSerializer from authentik.core.models import Source from authentik.flows.models import Flow, Stage +from authentik.stages.captcha.models import CaptchaStage from authentik.stages.password.models import PasswordStage @@ -43,6 +44,19 @@ class IdentificationStage(Stage): ), ) + captcha_stage = models.ForeignKey( + CaptchaStage, + null=True, + default=None, + on_delete=models.SET_NULL, + help_text=_( + ( + "When set, adds functionality exactly like a Captcha stage, but baked into the " + "Identification stage." + ), + ), + ) + case_insensitive_matching = models.BooleanField( default=True, help_text=_("When enabled, user fields are matched regardless of their casing."), diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index dffd119da9..1d2dfe8cab 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -29,6 +29,7 @@ from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_ from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.urls import reverse_with_qs from authentik.root.middleware import ClientIPMiddleware +from authentik.stages.captcha.stage import CaptchaChallenge, verify_captcha_token from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.signals import identification_failed from authentik.stages.password.stage import authenticate @@ -75,6 +76,7 @@ class IdentificationChallenge(Challenge): allow_show_password = BooleanField(default=False) application_pre = CharField(required=False) flow_designation = ChoiceField(FlowDesignation.choices) + captcha_stage = CaptchaChallenge(required=False) enroll_url = CharField(required=False) recovery_url = CharField(required=False) @@ -91,14 +93,16 @@ class IdentificationChallengeResponse(ChallengeResponse): uid_field = CharField() password = CharField(required=False, allow_blank=True, allow_null=True) + captcha_token = CharField(required=False, allow_blank=True, allow_null=True) component = CharField(default="ak-stage-identification") pre_user: User | None = None def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: - """Validate that user exists, and optionally their password""" + """Validate that user exists, and optionally their password and captcha token""" uid_field = attrs["uid_field"] current_stage: IdentificationStage = self.stage.executor.current_stage + client_ip = ClientIPMiddleware.get_client_ip(self.stage.request) pre_user = self.stage.get_user(uid_field) if not pre_user: @@ -113,7 +117,7 @@ class IdentificationChallengeResponse(ChallengeResponse): self.stage.logger.info( "invalid_login", identifier=uid_field, - client_ip=ClientIPMiddleware.get_client_ip(self.stage.request), + client_ip=client_ip, action="invalid_identifier", context={ "stage": sanitize_item(self.stage), @@ -136,6 +140,15 @@ class IdentificationChallengeResponse(ChallengeResponse): return attrs raise ValidationError("Failed to authenticate.") self.pre_user = pre_user + + # Captcha check + if captcha_stage := current_stage.captcha_stage: + captcha_token = attrs.get("captcha_token", None) + if not captcha_token: + self.stage.logger.warning("Token not set for captcha attempt") + verify_captcha_token(captcha_stage, captcha_token, client_ip) + + # Password check if not current_stage.password_stage: # No password stage select, don't validate the password return attrs @@ -206,6 +219,14 @@ class IdentificationStageView(ChallengeStageView): "primary_action": self.get_primary_action(), "user_fields": current_stage.user_fields, "password_fields": bool(current_stage.password_stage), + "captcha_stage": ( + { + "js_url": current_stage.captcha_stage.js_url, + "site_key": current_stage.captcha_stage.public_key, + } + if current_stage.captcha_stage + else None + ), "allow_show_password": bool(current_stage.password_stage) and current_stage.password_stage.allow_show_password, "show_source_labels": current_stage.show_source_labels, diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 57ffed1283..c39434e24a 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -1,6 +1,7 @@ """identification tests""" from django.urls import reverse +from requests_mock import Mocker from rest_framework.exceptions import ValidationError from authentik.core.tests.utils import create_test_admin_user, create_test_flow @@ -8,6 +9,8 @@ from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.tests import FlowTestCase from authentik.lib.generators import generate_id from authentik.sources.oauth.models import OAuthSource +from authentik.stages.captcha.models import CaptchaStage +from authentik.stages.captcha.tests import RECAPTCHA_PRIVATE_KEY, RECAPTCHA_PUBLIC_KEY from authentik.stages.identification.api import IdentificationStageSerializer from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.password import BACKEND_INBUILT @@ -133,6 +136,135 @@ class TestIdentificationStage(FlowTestCase): user_fields=["email"], ) + @Mocker() + def test_valid_with_captcha(self, mock: Mocker): + """Test with valid email and captcha token in single step""" + mock.post( + "https://www.recaptcha.net/recaptcha/api/siteverify", + json={ + "success": True, + "score": 0.5, + }, + ) + + captcha_stage = CaptchaStage.objects.create( + name="captcha", + public_key=RECAPTCHA_PUBLIC_KEY, + private_key=RECAPTCHA_PRIVATE_KEY, + ) + self.stage.captcha_stage = captcha_stage + self.stage.save() + + form_data = {"uid_field": self.user.email, "captcha_token": "PASSED"} + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + response = self.client.post(url, form_data) + self.assertEqual(response.status_code, 200) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) + + @Mocker() + def test_invalid_with_captcha(self, mock: Mocker): + """Test with valid email and invalid captcha token in single step""" + mock.post( + "https://www.recaptcha.net/recaptcha/api/siteverify", + json={ + "success": False, + "score": 0.5, + }, + ) + + captcha_stage = CaptchaStage.objects.create( + name="captcha", + public_key=RECAPTCHA_PUBLIC_KEY, + private_key=RECAPTCHA_PRIVATE_KEY, + ) + + self.stage.captcha_stage = captcha_stage + self.stage.save() + + form_data = { + "uid_field": self.user.email, + "captcha_token": "FAILED", + } + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + response = self.client.post(url, form_data) + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + response_errors={ + "non_field_errors": [{"code": "invalid", "string": "Invalid captcha response"}] + }, + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], + show_source_labels=False, + user_fields=["email"], + ) + + @Mocker() + def test_invalid_with_captcha_retriable(self, mock: Mocker): + """Test with valid email and invalid captcha token in single step""" + mock.post( + "https://www.recaptcha.net/recaptcha/api/siteverify", + json={ + "success": False, + "score": 0.5, + "error-codes": ["timeout-or-duplicate"], + }, + ) + + captcha_stage = CaptchaStage.objects.create( + name="captcha", + public_key=RECAPTCHA_PUBLIC_KEY, + private_key=RECAPTCHA_PRIVATE_KEY, + ) + + self.stage.captcha_stage = captcha_stage + self.stage.save() + + form_data = { + "uid_field": self.user.email, + "captcha_token": "FAILED", + } + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + response = self.client.post(url, form_data) + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + response_errors={ + "non_field_errors": [ + { + "code": "invalid", + "string": "Invalid captcha response. Retrying may solve this issue.", + } + ] + }, + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], + show_source_labels=False, + user_fields=["email"], + ) + def test_invalid_with_username(self): """Test invalid with username (user exists but stage only allows email)""" form_data = {"uid_field": self.user.username} diff --git a/blueprints/schema.json b/blueprints/schema.json index 9b3b91eb74..b0e7d140f1 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -6974,7 +6974,7 @@ "spnego_server_name": { "type": "string", "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": { "type": "string", @@ -10679,6 +10679,11 @@ "title": "Password stage", "description": "When set, shows a password field, instead of showing the password field as separate step." }, + "captcha_stage": { + "type": "integer", + "title": "Captcha stage", + "description": "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage." + }, "case_insensitive_matching": { "type": "boolean", "title": "Case insensitive matching", diff --git a/go.mod b/go.mod index 804d9d0dc5..dfa0cb5cb0 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/wwt/guac v1.3.2 - goauthentik.io/api/v3 v3.2024083.11 + goauthentik.io/api/v3 v3.2024083.13 golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 diff --git a/go.sum b/go.sum index 7c008d78ad..e062e8ba4f 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -goauthentik.io/api/v3 v3.2024083.11 h1:kF5WAnS0dB2cq9Uldqel8e8PDepJg/824JC3YFsQVHU= -goauthentik.io/api/v3 v3.2024083.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= +goauthentik.io/api/v3 v3.2024083.13 h1:xKh3feJYUeLw583zZ5ifgV0qjD37ZCOzgXPfbHQSbHM= +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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/internal/outpost/proxyv2/application/endpoint.go b/internal/outpost/proxyv2/application/endpoint.go index 9a91459182..c9cc50d40c 100644 --- a/internal/outpost/proxyv2/application/endpoint.go +++ b/internal/outpost/proxyv2/application/endpoint.go @@ -82,6 +82,9 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo if embedded { ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host) ep.JwksUri = updateURL(jwksUri, newHost.Scheme, newHost.Host) + } else { + // Fixes: https://github.com/goauthentik/authentik/issues/9622 / ep.Issuer must be the HostBrowser URL + ep.Issuer = updateURL(ep.Issuer, newBrowserHost.Scheme, newBrowserHost.Host) } return ep } diff --git a/internal/outpost/proxyv2/application/endpoint_test.go b/internal/outpost/proxyv2/application/endpoint_test.go index d3d0f74262..bd2be424d6 100644 --- a/internal/outpost/proxyv2/application/endpoint_test.go +++ b/internal/outpost/proxyv2/application/endpoint_test.go @@ -55,7 +55,7 @@ func TestEndpointAuthentikHostBrowser(t *testing.T) { assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL) assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) - assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer) + assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/", ep.Issuer) assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) } diff --git a/lifecycle/ak b/lifecycle/ak index 2033951557..15271b3103 100755 --- a/lifecycle/ak +++ b/lifecycle/ak @@ -54,6 +54,8 @@ function cleanup { } function prepare_debug { + export DEBIAN_FRONTEND=noninteractive + apt-get update 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 touch /unittest.xml diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 243070be90..7662522a26 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -19,7 +19,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-18 00:09+0000\n" +"POT-Creation-Date: 2024-10-23 16:39+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n" "Last-Translator: Marc Schmitt, 2024\n" "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" @@ -587,6 +587,30 @@ msgstr "Limite maximum de connection atteinte." msgid "(You are already connected in another tab/window)" msgstr "(Vous êtes déjà connecté dans un autre onglet/une autre fenêtre)" +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stage" +msgstr "" +"Étape d'authentificateur d'appareil du connecteur de confiance des appareils" +" Google" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stages" +msgstr "" +"Étapes d'authentificateur d'appareil du connecteur de confiance des " +"appareils Google" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Device" +msgstr "Appareil point de terminaison" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Devices" +msgstr "Appareils point de terminaison" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py +msgid "Verifying your browser..." +msgstr "Vérification de votre navigateur..." + #: authentik/enterprise/stages/source/models.py msgid "" "Amount of time a user can take to return from the source to continue the " @@ -2029,6 +2053,125 @@ msgstr "" msgid "Used recovery-link to authenticate." msgstr "Utiliser un lien de récupération pour se connecter." +#: authentik/sources/kerberos/models.py +msgid "Kerberos realm" +msgstr "Realm Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Custom krb5.conf to use. Uses the system one by default" +msgstr "" +"krb5.conf personnalisé à utiliser. Utilise celui du système par défault" + +#: authentik/sources/kerberos/models.py +msgid "Sync users from Kerberos into authentik" +msgstr "Synchroniser les utilisateurs Kerberos dans authentik" + +#: authentik/sources/kerberos/models.py +msgid "When a user changes their password, sync it back to Kerberos" +msgstr "" +"Lorsqu'un utilisateur change son mot de passe, le synchroniser à nouveau " +"vers Kerberos." + +#: authentik/sources/kerberos/models.py +msgid "Principal to authenticate to kadmin for sync." +msgstr "Principal pour s'authentifier à kadmin pour la synchronisation." + +#: authentik/sources/kerberos/models.py +msgid "Password to authenticate to kadmin for sync" +msgstr "Mot de passe pour s'authentifier à kadmin pour la synchronisation." + +#: authentik/sources/kerberos/models.py +msgid "" +"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the " +"form TYPE:residual" +msgstr "" +"Keytab pour s'authentifier à kadmin pour la synchronisation. Doit être " +"encodé en base64 ou de la forme TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Credentials cache to authenticate to kadmin for sync. Must be in the form " +"TYPE:residual" +msgstr "" +"Credentials cache pour s'authentifier à kadmin pour la synchronisation. Doit" +" être de la forme TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Force the use of a specific server name for SPNEGO. Must be in the form " +"HTTP@hostname" +msgstr "" +"Force l'utilisation d'un nom de serveur spécifique pour SPNEGO. Doit être de" +" la forme HTTP@hostname" + +#: authentik/sources/kerberos/models.py +msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path" +msgstr "" +"Keytab SPNEGO encodée en base64 ou chemin vers la keytab de la forme " +"FILE:path" + +#: authentik/sources/kerberos/models.py +msgid "Credential cache to use for SPNEGO in form type:residual" +msgstr "Credentials cache pour SPNEGO de la forme TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"If enabled, the authentik-stored password will be updated upon login with " +"the Kerberos password backend" +msgstr "" +"Si activé, le mot de passe stocké par authentik sera mis à jour à la " +"connexion avec le backend de mot de passe Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source" +msgstr "Source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Sources" +msgstr "Sources Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mapping" +msgstr "Mappage de propriété source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mappings" +msgstr "Mappages de propriété source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connection" +msgstr "Connexion de l'utilisateur à la source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connections" +msgstr "Connexions de l'utilisateur à la source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connection" +msgstr "Connexion du groupe à la source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connections" +msgstr "Connexions du groupe à la source Kerberos" + +#: authentik/sources/kerberos/views.py +msgid "SPNEGO authentication required" +msgstr "Authentification SPNEGO requise" + +#: authentik/sources/kerberos/views.py +msgid "" +"\n" +" Make sure you have valid tickets (obtainable via kinit)\n" +" and configured the browser correctly.\n" +" Please contact your administrator.\n" +" " +msgstr "" +"\n" +" Vérifiez que vous avez des tickets valides (qu'on peut obtenir via kinit)\n" +" et que le navigateur est configuré correctement.\n" +" Veuillez contacter votre administrateur.\n" +" " + #: authentik/sources/ldap/api.py msgid "Only a single LDAP Source with password synchronization is allowed" msgstr "" @@ -3121,6 +3264,10 @@ msgstr "Base de données utilisateurs + mots de passes applicatifs" msgid "User database + LDAP password" msgstr "Base de données utilisateurs + mot de passe LDAP" +#: authentik/stages/password/models.py +msgid "User database + Kerberos password" +msgstr "Base de données utilisateurs + mot de passe Kerberos" + #: authentik/stages/password/models.py msgid "Selection of backends to test the password against." msgstr "Sélection de backends pour tester le mot de passe." diff --git a/locale/zh-Hans/LC_MESSAGES/django.po b/locale/zh-Hans/LC_MESSAGES/django.po index d2b79c466b..4a8b2ddda9 100644 --- a/locale/zh-Hans/LC_MESSAGES/django.po +++ b/locale/zh-Hans/LC_MESSAGES/django.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-18 00:09+0000\n" +"POT-Creation-Date: 2024-10-23 16:39+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n" "Last-Translator: deluxghost, 2024\n" "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" @@ -540,6 +540,26 @@ msgstr "已达到最大连接数。" msgid "(You are already connected in another tab/window)" msgstr "(您已经在另一个标签页/窗口连接了)" +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stage" +msgstr "端点身份验证器 Google 设备信任连接器阶段" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stages" +msgstr "端点身份验证器 Google 设备信任连接器阶段" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Device" +msgstr "端点设备" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Devices" +msgstr "端点设备" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py +msgid "Verifying your browser..." +msgstr "正在验证您的浏览器…" + #: authentik/enterprise/stages/source/models.py msgid "" "Amount of time a user can take to return from the source to continue the " @@ -1848,6 +1868,112 @@ msgstr "创建一个密钥,可用于恢复对 authentik 的访问权限。" msgid "Used recovery-link to authenticate." msgstr "已使用恢复链接进行身份验证。" +#: authentik/sources/kerberos/models.py +msgid "Kerberos realm" +msgstr "Kerberos 领域" + +#: authentik/sources/kerberos/models.py +msgid "Custom krb5.conf to use. Uses the system one by default" +msgstr "要使用的自定义 krb5.conf。默认使用系统自带" + +#: authentik/sources/kerberos/models.py +msgid "Sync users from Kerberos into authentik" +msgstr "从 Kerberos 同步用户到 authentik" + +#: authentik/sources/kerberos/models.py +msgid "When a user changes their password, sync it back to Kerberos" +msgstr "当用户修改密码时,将其同步回 Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Principal to authenticate to kadmin for sync." +msgstr "向 kadmin 进行身份验证以进行同步的主体。" + +#: authentik/sources/kerberos/models.py +msgid "Password to authenticate to kadmin for sync" +msgstr "向 kadmin 进行身份验证以进行同步的密码" + +#: authentik/sources/kerberos/models.py +msgid "" +"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the " +"form TYPE:residual" +msgstr "向 kadmin 进行身份验证以进行同步的 Keytab。必须以 Base64 编码,或者形式为 TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Credentials cache to authenticate to kadmin for sync. Must be in the form " +"TYPE:residual" +msgstr "向 kadmin 进行身份验证以进行同步的凭据缓存。形式必须为 TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Force the use of a specific server name for SPNEGO. Must be in the form " +"HTTP@hostname" +msgstr "强制为 SPNEGO 使用特定服务器名称。形式必须为 HTTP@主机名" + +#: authentik/sources/kerberos/models.py +msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path" +msgstr "以 Base64 编码的 SPNEGO Keytab 或 FILE:path 形式的 Keytab 路径" + +#: authentik/sources/kerberos/models.py +msgid "Credential cache to use for SPNEGO in form type:residual" +msgstr "SPNEGO 使用的凭据缓存,形式为 type:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"If enabled, the authentik-stored password will be updated upon login with " +"the Kerberos password backend" +msgstr "启用时,authentik 存储的密码将会在使用 Kerberos 密码后端登录时更新" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source" +msgstr "Kerberos 源" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Sources" +msgstr "Kerberos 源" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mapping" +msgstr "Kerberos 源属性映射" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mappings" +msgstr "Kerberos 源属性映射" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connection" +msgstr "用户 Kerberos 源连接" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connections" +msgstr "用户 Kerberos 源连接" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connection" +msgstr "组 Kerberos 源连接" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connections" +msgstr "组 Kerberos 源连接" + +#: authentik/sources/kerberos/views.py +msgid "SPNEGO authentication required" +msgstr "需要 SPNEGO 身份验证" + +#: authentik/sources/kerberos/views.py +msgid "" +"\n" +" Make sure you have valid tickets (obtainable via kinit)\n" +" and configured the browser correctly.\n" +" Please contact your administrator.\n" +" " +msgstr "" +"\n" +" 请确认您拥有有效票据(通过 kinit 获得)\n" +" 并且已正确配置浏览器。\n" +" 请联系您的管理员。\n" +" " + #: authentik/sources/ldap/api.py msgid "Only a single LDAP Source with password synchronization is allowed" msgstr "仅允许使用密码同步的单个 LDAP 源" @@ -2876,6 +3002,10 @@ msgstr "用户数据库 + 应用程序密码" msgid "User database + LDAP password" msgstr "用户数据库 + LDAP 密码" +#: authentik/stages/password/models.py +msgid "User database + Kerberos password" +msgstr "用户数据库 + Kerberos 密码" + #: authentik/stages/password/models.py msgid "Selection of backends to test the password against." msgstr "选择用于测试密码的后端。" diff --git a/locale/zh_CN/LC_MESSAGES/django.po b/locale/zh_CN/LC_MESSAGES/django.po index f3a6d452c4..7564cada8f 100644 --- a/locale/zh_CN/LC_MESSAGES/django.po +++ b/locale/zh_CN/LC_MESSAGES/django.po @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-18 00:09+0000\n" +"POT-Creation-Date: 2024-10-23 16:39+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n" "Last-Translator: deluxghost, 2024\n" "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" @@ -539,6 +539,26 @@ msgstr "已达到最大连接数。" msgid "(You are already connected in another tab/window)" msgstr "(您已经在另一个标签页/窗口连接了)" +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stage" +msgstr "端点身份验证器 Google 设备信任连接器阶段" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stages" +msgstr "端点身份验证器 Google 设备信任连接器阶段" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Device" +msgstr "端点设备" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Devices" +msgstr "端点设备" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py +msgid "Verifying your browser..." +msgstr "正在验证您的浏览器…" + #: authentik/enterprise/stages/source/models.py msgid "" "Amount of time a user can take to return from the source to continue the " @@ -1847,6 +1867,112 @@ msgstr "创建一个密钥,可用于恢复对 authentik 的访问权限。" msgid "Used recovery-link to authenticate." msgstr "已使用恢复链接进行身份验证。" +#: authentik/sources/kerberos/models.py +msgid "Kerberos realm" +msgstr "Kerberos 领域" + +#: authentik/sources/kerberos/models.py +msgid "Custom krb5.conf to use. Uses the system one by default" +msgstr "要使用的自定义 krb5.conf。默认使用系统自带" + +#: authentik/sources/kerberos/models.py +msgid "Sync users from Kerberos into authentik" +msgstr "从 Kerberos 同步用户到 authentik" + +#: authentik/sources/kerberos/models.py +msgid "When a user changes their password, sync it back to Kerberos" +msgstr "当用户修改密码时,将其同步回 Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Principal to authenticate to kadmin for sync." +msgstr "向 kadmin 进行身份验证以进行同步的主体。" + +#: authentik/sources/kerberos/models.py +msgid "Password to authenticate to kadmin for sync" +msgstr "向 kadmin 进行身份验证以进行同步的密码" + +#: authentik/sources/kerberos/models.py +msgid "" +"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the " +"form TYPE:residual" +msgstr "向 kadmin 进行身份验证以进行同步的 Keytab。必须以 Base64 编码,或者形式为 TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Credentials cache to authenticate to kadmin for sync. Must be in the form " +"TYPE:residual" +msgstr "向 kadmin 进行身份验证以进行同步的凭据缓存。形式必须为 TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Force the use of a specific server name for SPNEGO. Must be in the form " +"HTTP@hostname" +msgstr "强制为 SPNEGO 使用特定服务器名称。形式必须为 HTTP@主机名" + +#: authentik/sources/kerberos/models.py +msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path" +msgstr "以 Base64 编码的 SPNEGO Keytab 或 FILE:path 形式的 Keytab 路径" + +#: authentik/sources/kerberos/models.py +msgid "Credential cache to use for SPNEGO in form type:residual" +msgstr "SPNEGO 使用的凭据缓存,形式为 type:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"If enabled, the authentik-stored password will be updated upon login with " +"the Kerberos password backend" +msgstr "启用时,authentik 存储的密码将会在使用 Kerberos 密码后端登录时更新" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source" +msgstr "Kerberos 源" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Sources" +msgstr "Kerberos 源" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mapping" +msgstr "Kerberos 源属性映射" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mappings" +msgstr "Kerberos 源属性映射" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connection" +msgstr "用户 Kerberos 源连接" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connections" +msgstr "用户 Kerberos 源连接" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connection" +msgstr "组 Kerberos 源连接" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connections" +msgstr "组 Kerberos 源连接" + +#: authentik/sources/kerberos/views.py +msgid "SPNEGO authentication required" +msgstr "需要 SPNEGO 身份验证" + +#: authentik/sources/kerberos/views.py +msgid "" +"\n" +" Make sure you have valid tickets (obtainable via kinit)\n" +" and configured the browser correctly.\n" +" Please contact your administrator.\n" +" " +msgstr "" +"\n" +" 请确认您拥有有效票据(通过 kinit 获得)\n" +" 并且已正确配置浏览器。\n" +" 请联系您的管理员。\n" +" " + #: authentik/sources/ldap/api.py msgid "Only a single LDAP Source with password synchronization is allowed" msgstr "仅允许使用密码同步的单个 LDAP 源" @@ -2875,6 +3001,10 @@ msgstr "用户数据库 + 应用程序密码" msgid "User database + LDAP password" msgstr "用户数据库 + LDAP 密码" +#: authentik/stages/password/models.py +msgid "User database + Kerberos password" +msgstr "用户数据库 + Kerberos 密码" + #: authentik/stages/password/models.py msgid "Selection of backends to test the password against." msgstr "选择用于测试密码的后端。" diff --git a/poetry.lock b/poetry.lock index 477f4034d8..ab70b55356 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1849,35 +1849,36 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "gssapi" -version = "1.8.3" +version = "1.9.0" description = "Python GSSAPI Wrapper" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "gssapi-1.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e4a83e9b275fe69b5d40be6d5479889866b80333a12c51a9243f2712d4f0554"}, - {file = "gssapi-1.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d57d67547e18f4e44a688bfb20abbf176d1b8df547da2b31c3f2df03cfdc269"}, - {file = "gssapi-1.8.3-cp310-cp310-win32.whl", hash = "sha256:3a3f63105f39c4af29ffc8f7b6542053d87fe9d63010c689dd9a9f5571facb8e"}, - {file = "gssapi-1.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:b031c0f186ab4275186da385b2c7470dd47c9b27522cb3b753757c9ac4bebf11"}, - {file = "gssapi-1.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b03d6b30f1fcd66d9a688b45a97e302e4dd3f1386d5c333442731aec73cdb409"}, - {file = "gssapi-1.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca6ceb17fc15eda2a69f2e8c6cf10d11e2edb32832255e5d4c65b21b6db4680a"}, - {file = "gssapi-1.8.3-cp311-cp311-win32.whl", hash = "sha256:edc8ef3a9e397dbe18bb6016f8e2209969677b534316d20bb139da2865a38efe"}, - {file = "gssapi-1.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8fdb1ff130cee49bc865ec1624dee8cf445cd6c6e93b04bffef2c6f363a60cb9"}, - {file = "gssapi-1.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:19c373b3ba63ce19cd3163aa1495635e3d01b0de6cc4ff1126095eded1df6e01"}, - {file = "gssapi-1.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f1a8046d695f2c9b8d640a6e385780d3945c0741571ed6fee6f94c31e431dc"}, - {file = "gssapi-1.8.3-cp312-cp312-win32.whl", hash = "sha256:338db18612e3e6ed64e92b6d849242a535fdc98b365f21122992fb8cae737617"}, - {file = "gssapi-1.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5731c5b40ecc3116cfe7fb7e1d1e128583ec8b3df1e68bf8cd12073160793acd"}, - {file = "gssapi-1.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e556878da197ad115a566d36e46a8082d0079731d9c24d1ace795132d725ff2a"}, - {file = "gssapi-1.8.3-cp37-cp37m-win32.whl", hash = "sha256:e2bb081f2db2111377effe7d40ba23f9a87359b9d2f4881552b731e9da88b36b"}, - {file = "gssapi-1.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4d9ed83f2064cda60aad90e6840ae282096801b2c814b8cbd390bf0df4635aab"}, - {file = "gssapi-1.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d91fe6e2a5c89b32102ea8e374b8ae13b9031d43d7b55f3abc1f194ddce820d"}, - {file = "gssapi-1.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5b28237afc0668046934792756dd4b6b7e957b0d95a608d02f296734a2819ad"}, - {file = "gssapi-1.8.3-cp38-cp38-win32.whl", hash = "sha256:791e44f7bea602b8e3da1ec56fbdb383b8ee3326fdeb736f904c2aa9af13a67d"}, - {file = "gssapi-1.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:5b4bf84d0a6d7779a4bf11dacfd3db57ae02dd53562e2aeadac4219a68eaee07"}, - {file = "gssapi-1.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e40efc88ccefefd6142f8c47b8af498731938958b808bad49990442a91f45160"}, - {file = "gssapi-1.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee74b9211c977b9181ff4652d886d7712c9a221560752a35393b58e5ea07887a"}, - {file = "gssapi-1.8.3-cp39-cp39-win32.whl", hash = "sha256:465c6788f2ac6ef7c738394ba8fde1ede6004e5721766f386add63891d8c90af"}, - {file = "gssapi-1.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:8fb8ee70458f47b51ed881a6881f30b187c987c02af16cc0fff0079255d4d465"}, - {file = "gssapi-1.8.3.tar.gz", hash = "sha256:aa3c8d0b1526f52559552bb2c9d2d6be013d76a8e5db00b39a1db5727e93b0b0"}, + {file = "gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1"}, + {file = "gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6"}, + {file = "gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e"}, + {file = "gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962"}, + {file = "gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560"}, + {file = "gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd"}, + {file = "gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe"}, + {file = "gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f"}, + {file = "gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f"}, + {file = "gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f"}, + {file = "gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec"}, + {file = "gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76"}, + {file = "gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c"}, + {file = "gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e"}, + {file = "gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28"}, + {file = "gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9"}, + {file = "gssapi-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0e378d62b2fc352ca0046030cda5911d808a965200f612fdd1d74501b83e98f"}, + {file = "gssapi-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b74031c70864d04864b7406c818f41be0c1637906fb9654b06823bcc79f151dc"}, + {file = "gssapi-1.9.0-cp38-cp38-win32.whl", hash = "sha256:f2f3a46784d8127cc7ef10d3367dedcbe82899ea296710378ccc9b7cefe96f4c"}, + {file = "gssapi-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:a81f30cde21031e7b1f8194a3eea7285e39e551265e7744edafd06eadc1c95bc"}, + {file = "gssapi-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbc93fdadd5aab9bae594538b2128044b8c5cdd1424fe015a465d8a8a587411a"}, + {file = "gssapi-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b2a3c0a9beb895942d4b8e31f515e52c17026e55aeaa81ee0df9bbfdac76098"}, + {file = "gssapi-1.9.0-cp39-cp39-win32.whl", hash = "sha256:060b58b455d29ab8aca74770e667dca746264bee660ac5b6a7a17476edc2c0b8"}, + {file = "gssapi-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:11c9fe066edb0fa0785697eb0cecf2719c7ad1d9f2bf27be57b647a617bcfaa5"}, + {file = "gssapi-1.9.0.tar.gz", hash = "sha256:f468fac8f3f5fca8f4d1ca19e3cd4d2e10bd91074e7285464b22715d13548afe"}, ] [package.dependencies] @@ -4292,29 +4293,29 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.7.0" +version = "0.7.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"}, - {file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"}, - {file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"}, - {file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"}, - {file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"}, - {file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"}, - {file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"}, - {file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"}, - {file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"}, - {file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"}, - {file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"}, - {file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"}, + {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"}, + {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"}, + {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"}, + {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"}, + {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"}, + {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"}, + {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"}, ] [[package]] @@ -4750,13 +4751,13 @@ wsproto = ">=0.14" [[package]] name = "twilio" -version = "9.3.4" +version = "9.3.5" description = "Twilio API client and TwiML generator" optional = false python-versions = ">=3.7.0" files = [ - {file = "twilio-9.3.4-py2.py3-none-any.whl", hash = "sha256:2cae99f0f7aecbd9da02fa59ad8f11b360db4a9281fc3fb3237ad50be21d8a9b"}, - {file = "twilio-9.3.4.tar.gz", hash = "sha256:38a6ab04752f44313dcf736eae45236a901528d3f53dfc21d3afd33539243c7f"}, + {file = "twilio-9.3.5-py2.py3-none-any.whl", hash = "sha256:d6a97a77b98cc176a61c960f11894af385bc1c11b93e2e8b79fdfb9601788fb0"}, + {file = "twilio-9.3.5.tar.gz", hash = "sha256:608d78a903d403465aac1840c58a6546a090b7e222d2bf539a93c3831072880c"}, ] [package.dependencies] diff --git a/schema.yml b/schema.yml index d4f3eb78ac..9028d7c5c9 100644 --- a/schema.yml +++ b/schema.yml @@ -33862,6 +33862,11 @@ paths: operationId: stages_identification_list description: IdentificationStage Viewset parameters: + - in: query + name: captcha_stage + schema: + type: string + format: uuid - in: query name: case_insensitive_matching schema: @@ -40204,10 +40209,15 @@ components: challenge: type: object additionalProperties: {} + last_used: + type: string + format: date-time + nullable: true required: - challenge - device_class - device_uid + - last_used DeviceChallengeRequest: type: object description: Single device challenge @@ -40221,10 +40231,15 @@ components: challenge: type: object additionalProperties: {} + last_used: + type: string + format: date-time + nullable: true required: - challenge - device_class - device_uid + - last_used DeviceClassesEnum: enum: - static @@ -42494,6 +42509,8 @@ components: type: string flow_designation: $ref: '#/components/schemas/FlowDesignationEnum' + captcha_stage: + $ref: '#/components/schemas/CaptchaChallenge' enroll_url: type: string recovery_url: @@ -42528,6 +42545,9 @@ components: password: type: string nullable: true + captcha_token: + type: string + nullable: true required: - uid_field IdentificationStage: @@ -42573,6 +42593,12 @@ components: nullable: true description: When set, shows a password field, instead of showing the password field as separate step. + captcha_stage: + type: string + format: uuid + nullable: true + description: When set, adds functionality exactly like a Captcha stage, + but baked into the Identification stage. case_insensitive_matching: type: boolean description: When enabled, user fields are matched regardless of their casing. @@ -42641,6 +42667,12 @@ components: nullable: true description: When set, shows a password field, instead of showing the password field as separate step. + captcha_stage: + type: string + format: uuid + nullable: true + description: When set, adds functionality exactly like a Captcha stage, + but baked into the Identification stage. case_insensitive_matching: type: boolean description: When enabled, user fields are matched regardless of their casing. @@ -42943,7 +42975,8 @@ components: readOnly: true spnego_server_name: 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: type: string description: Credential cache to use for SPNEGO in form type:residual @@ -43112,7 +43145,8 @@ components: be in the form TYPE:residual spnego_server_name: 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: type: string writeOnly: true @@ -48231,6 +48265,12 @@ components: nullable: true description: When set, shows a password field, instead of showing the password field as separate step. + captcha_stage: + type: string + format: uuid + nullable: true + description: When set, adds functionality exactly like a Captcha stage, + but baked into the Identification stage. case_insensitive_matching: type: boolean description: When enabled, user fields are matched regardless of their casing. @@ -48410,7 +48450,8 @@ components: be in the form TYPE:residual spnego_server_name: 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: type: string writeOnly: true diff --git a/web/package-lock.json b/web/package-lock.json index b300cf9690..869227879e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,7 +23,7 @@ "@floating-ui/dom": "^1.6.11", "@formatjs/intl-listformat": "^7.5.7", "@fortawesome/fontawesome-free": "^6.6.0", - "@goauthentik/api": "^2024.8.3-1729699127", + "@goauthentik/api": "^2024.8.3-1729836831", "@lit-labs/ssr": "^3.2.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.2", @@ -1775,9 +1775,9 @@ } }, "node_modules/@goauthentik/api": { - "version": "2024.8.3-1729699127", - "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729699127.tgz", - "integrity": "sha512-luo0SAASR6BTTtLszDgfdwofBejv4F3hCHgPxeSoTSFgE8/A2+zJD8EtWPZaa1udDkwPa9lbIeJSSmbgFke3jA==" + "version": "2024.8.3-1729836831", + "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729836831.tgz", + "integrity": "sha512-nOgvjYQiK+HhWuiZ635h/aSsq7Mfj5cDrIyBJt+IJRQuJFtnnHx8nscRXKK/8sBl9obH2zMCoZgeqytK8145bg==" }, "node_modules/@goauthentik/web": { "resolved": "", diff --git a/web/package.json b/web/package.json index ac5117f7b2..a51cd66952 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ "@floating-ui/dom": "^1.6.11", "@formatjs/intl-listformat": "^7.5.7", "@fortawesome/fontawesome-free": "^6.6.0", - "@goauthentik/api": "^2024.8.3-1729699127", + "@goauthentik/api": "^2024.8.3-1729836831", "@lit-labs/ssr": "^3.2.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.2", diff --git a/web/src/admin/applications/wizard/BasePanel.ts b/web/src/admin/applications/wizard/BasePanel.ts index 1f4bf37161..8e3c4cbe7a 100644 --- a/web/src/admin/applications/wizard/BasePanel.ts +++ b/web/src/admin/applications/wizard/BasePanel.ts @@ -29,7 +29,7 @@ export class ApplicationWizardPageBase return AwadStyles; } - @consume({ context: applicationWizardContext }) + @consume({ context: applicationWizardContext, subscribe: true }) public wizard!: ApplicationWizardState; @query("form") diff --git a/web/src/admin/applications/wizard/ContextIdentity.ts b/web/src/admin/applications/wizard/ContextIdentity.ts index ab71861b70..85b0275dfa 100644 --- a/web/src/admin/applications/wizard/ContextIdentity.ts +++ b/web/src/admin/applications/wizard/ContextIdentity.ts @@ -1,7 +1,12 @@ import { createContext } from "@lit/context"; +import { LocalTypeCreate } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js"; import { ApplicationWizardState } from "./types"; export const applicationWizardContext = createContext( Symbol("ak-application-wizard-state-context"), ); + +export const applicationWizardProvidersContext = createContext( + Symbol("ak-application-wizard-providers-context"), +); diff --git a/web/src/admin/applications/wizard/ak-application-wizard.ts b/web/src/admin/applications/wizard/ak-application-wizard.ts index 32ef5eff2c..757e571dfc 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard.ts +++ b/web/src/admin/applications/wizard/ak-application-wizard.ts @@ -1,3 +1,4 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; @@ -5,7 +6,10 @@ import { ContextProvider } from "@lit/context"; import { msg } from "@lit/localize"; 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 { ApplicationStep, @@ -19,6 +23,7 @@ const freshWizardState = (): ApplicationWizardState => ({ app: {}, provider: {}, errors: {}, + proxyMode: ProxyMode.Proxy, }); @customElement("ak-application-wizard") @@ -46,6 +51,11 @@ export class ApplicationWizard extends CustomListenerElement( 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 * 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 = 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... handleUpdate(detail: ApplicationWizardStateUpdate) { if (detail.status === "submitted") { diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts index fc7ed36597..c4b25a56c4 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts @@ -1,176 +1,28 @@ import "@goauthentik/admin/common/ak-license-notice"; -import { msg } from "@lit/localize"; import { TemplateResult, html } from "lit"; -import type { ProviderModelEnum as ProviderModelEnumType, 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"; +import type { TypeCreate } from "@goauthentik/api"; type ProviderRenderer = () => TemplateResult; -type ModelConverter = (provider: OneOfProvider) => ModelRequest; - -type ProviderNoteProvider = () => TemplateResult | undefined; -type ProviderNote = ProviderNoteProvider | undefined; - export type LocalTypeCreate = TypeCreate & { - formName: string; - modelName: ProviderModelEnumType; - converter: ModelConverter; - note?: ProviderNote; renderer: ProviderRenderer; }; -export const providerModelsList: LocalTypeCreate[] = [ - { - formName: "oauth2provider", - name: msg("OAuth2/OpenID Provider"), - description: msg("Modern applications, APIs and Single-page applications."), - renderer: () => - html``, - modelName: ProviderModelEnum.Oauth2Oauth2provider, - 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``, - modelName: ProviderModelEnum.LdapLdapprovider, - 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``, - modelName: ProviderModelEnum.ProxyProxyprovider, - 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``, - 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``, - 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``, - modelName: ProviderModelEnum.RacRacprovider, - converter: (provider: OneOfProvider) => ({ - providerModel: ProviderModelEnum.RacRacprovider, - ...(provider as RACProviderRequest), - }), - note: () => html``, - requiresEnterprise: true, - component: "", - iconUrl: "/static/authentik/sources/rac.svg", - }, - { - formName: "samlprovider", - name: msg("SAML Provider"), - description: msg("Configure SAML provider manually"), - renderer: () => - html``, - modelName: ProviderModelEnum.SamlSamlprovider, - 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``, - modelName: ProviderModelEnum.RadiusRadiusprovider, - 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``, - modelName: ProviderModelEnum.ScimScimprovider, - converter: (provider: OneOfProvider) => ({ - providerModel: ProviderModelEnum.ScimScimprovider, - ...(provider as SCIMProviderRequest), - }), - component: "", - iconUrl: "/static/authentik/sources/scim.png", - }, -]; - -export const providerRendererList = new Map( - providerModelsList.map((tc) => [tc.formName, tc.renderer]), -); - -export default providerModelsList; +export const providerTypeRenderers = { + oauth2provider: () => + html``, + ldapprovider: () => + html``, + proxyprovider: () => + html``, + racprovider: () => + html``, + samlprovider: () => + html``, + radiusprovider: () => + html``, + scimprovider: () => + html``, +}; diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts index eac762f3f9..f647b268d9 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts @@ -7,34 +7,29 @@ import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/wizard/TypeCreateWizardPage"; import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage"; +import { consume } from "@lit/context"; import { msg } from "@lit/localize"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { html } from "lit"; import BasePanel from "../BasePanel"; +import { applicationWizardProvidersContext } from "../ContextIdentity"; 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") export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) { + @consume({ context: applicationWizardProvidersContext }) + public providerModelsList: LocalTypeCreate[]; + render() { - const selectedTypes = providerModelsList.filter( - (t) => t.formName === this.wizard.providerModel, + const selectedTypes = this.providerModelsList.filter( + (t) => t.modelName === this.wizard.providerModel, ); - // As a hack, the Application wizard has separate provider paths for our three types of - // 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 + return this.providerModelsList.length > 0 ? html`
0 ? selectedTypes[0] : undefined} @@ -42,7 +37,7 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm this.dispatchWizardUpdate({ update: { ...this.wizard, - providerModel: ev.detail.formName, + providerModel: ev.detail.modelName, errors: {}, }, status: this.valid ? "valid" : "invalid", diff --git a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts index 087d05703a..1e032f2d7e 100644 --- a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts +++ b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts @@ -21,14 +21,14 @@ import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; import { type ApplicationRequest, CoreApi, - type ModelRequest, + ProviderModelEnum, + ProxyMode, type TransactionApplicationRequest, type TransactionApplicationResponse, ValidationError, } from "@goauthentik/api"; import BasePanel from "../BasePanel"; -import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices"; function cleanApplication(app: Partial): ApplicationRequest { return { @@ -38,14 +38,19 @@ function cleanApplication(app: Partial): ApplicationRequest }; } -type ProviderModelType = Exclude; - type State = { state: "idle" | "running" | "error" | "success"; label: string | TemplateResult; icon: string[]; }; +const providerMap: Map = Object.values(ProviderModelEnum) + .filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value)) + .reduce((acc: Map, value) => { + acc.set(value.split(".")[1], value); + return acc; + }, new Map()); + const idleState: State = { state: "idle", label: "", @@ -98,19 +103,25 @@ export class ApplicationWizardCommitApplication extends BasePanel { if (this.commitState === idleState) { this.response = undefined; this.commitState = runningState; - const providerModel = providerModelsList.find( - ({ formName }) => formName === this.wizard.providerModel, - ); - if (!providerModel) { - throw new Error( - `Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`, - ); + + // Stringly-based API. Not the best, but it works. Just be aware that it is + // stringly-based. + const providerModel = providerMap.get(this.wizard.providerModel); + const provider = this.wizard.provider; + 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 = { - providerModel: providerModel.modelName as ProviderModelType, app: cleanApplication(this.wizard.app), - provider: providerModel.converter(this.wizard.provider), + providerModel, + provider, }; this.send(request); diff --git a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts index 526d1e78ca..8ec5344bb8 100644 --- a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts +++ b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts @@ -1,12 +1,12 @@ +import { consume } from "@lit/context"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; 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 "./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-single-forward-proxy"; import "./rac/ak-application-wizard-authentication-for-rac"; import "./radius/ak-application-wizard-authentication-by-radius"; 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") export class ApplicationWizardApplicationDetails extends BasePanel { + @consume({ context: applicationWizardProvidersContext }) + public providerModelsList: LocalTypeCreate[]; + render() { - const handler = providerRendererList.get(this.wizard.providerModel); + const handler: LocalTypeCreate | undefined = this.providerModelsList.find( + ({ modelName }) => modelName === this.wizard.providerModel, + ); if (!handler) { throw new Error( "Unrecognized authentication method in ak-application-wizard-authentication-method", ); } - return handler(); + return handler.renderer(); } } diff --git a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts deleted file mode 100644 index e6d66aea6f..0000000000 --- a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts +++ /dev/null @@ -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` - - - - `; - } - - render() { - const errors = this.wizard.errors.provider; - - return html` ${msg("Configure Proxy Provider")} - - ${this.renderModeDescription()} - - - - -

- ${msg("Flow used when authorizing this provider.")} -

-
- - ${this.renderProxyMode()} - - - - - ${msg("Advanced protocol settings")} -
- - - - - -

- ${msg("Additional scope mappings, which are passed to the proxy.")} -

-
- - - ${msg( - "Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.", - )} -

-

- ${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.", - )} -

`} - > -
-
-
- - ${msg("Advanced flow settings")} - - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - -

- ${msg("Flow used when logging out of this provider.")} -

-
- -
- - ${msg("Authentication settings")} -
- - - { - 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.", - )} - > - - ${this.showHttpBasic ? this.renderHttpBasic() : html``} - - - -

- ${msg( - "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", - )} -

-
-
-
- `; - } -} - -export default AkTypeProxyApplicationWizardPage; diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-forward-domain-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-forward-domain-proxy.ts deleted file mode 100644 index acfbf4d281..0000000000 --- a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-forward-domain-proxy.ts +++ /dev/null @@ -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`

- ${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.", - )} -

-
- ${msg("An example setup can look like this:")} -
    -
  • ${msg("authentik running on auth.example.com")}
  • -
  • ${msg("app1 running on app1.example.com")}
  • -
- ${msg( - "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.", - )} -
`; - } - - renderProxyMode() { - const provider = this.wizard.provider as ProxyProvider | undefined; - const errors = this.wizard.errors.provider; - - return html` - - - - `; - } -} - -export default AkForwardDomainProxyApplicationWizardPage; - -declare global { - interface HTMLElementTagNameMap { - "ak-application-wizard-authentication-for-forward-proxy-domain": AkForwardDomainProxyApplicationWizardPage; - } -} diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts index b920254cc8..5487aa4802 100644 --- a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts +++ b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts @@ -1,55 +1,46 @@ -import { first } from "@goauthentik/common/utils"; -import "@goauthentik/components/ak-switch-input"; -import "@goauthentik/components/ak-text-input"; +import { + ProxyModeValue, + renderForm, +} from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js"; 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 { ifDefined } from "lit/directives/if-defined.js"; -import { ProxyProvider } from "@goauthentik/api"; - -import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage"; +import BaseProviderPanel from "../BaseProviderPanel.js"; @customElement("ak-application-wizard-authentication-for-reverse-proxy") -export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage { - renderModeDescription() { - return html`

- ${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.", - )} -

`; - } +export class AkReverseProxyApplicationWizardPage extends BaseProviderPanel { + @state() + showHttpBasic = true; - renderProxyMode() { - const provider = this.wizard.provider as ProxyProvider | undefined; - const errors = this.wizard.errors.provider; + render() { + const onSetMode: SetMode = (ev: CustomEvent) => { + 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` - - - `; + const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => { + const el = ev.target as HTMLInputElement; + this.showHttpBasic = el.checked; + }; + + return html` ${msg("Configure Proxy Provider")} +
+ ${renderForm(this.wizard.provider ?? {}, this.wizard.errors.provider ?? [], { + mode: this.wizard.proxyMode, + onSetMode, + showHttpBasic: this.showHttpBasic, + onSetShowHttpBasic, + })} +
`; } } diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-single-forward-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-single-forward-proxy.ts deleted file mode 100644 index 501bff8cce..0000000000 --- a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-single-forward-proxy.ts +++ /dev/null @@ -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`

- ${msg( - html`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).`, - )} -

`; - } - - renderProxyMode() { - const provider = this.wizard.provider as ProxyProvider | undefined; - const errors = this.wizard.errors.provider; - - return html``; - } -} - -export default AkForwardSingleProxyApplicationWizardPage; - -declare global { - interface HTMLElementTagNameMap { - "ak-application-wizard-authentication-for-single-forward-proxy": AkForwardSingleProxyApplicationWizardPage; - } -} diff --git a/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts b/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts index 75d8534084..00535fa7fa 100644 --- a/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts +++ b/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts @@ -1,3 +1,5 @@ +import { renderForm } from "@goauthentik/admin/providers/scim/SCIMProviderFormForm.js"; + import { msg } from "@lit/localize"; import { customElement, state } from "@lit/reactive-element/decorators.js"; import { html } from "lit"; diff --git a/web/src/admin/applications/wizard/types.ts b/web/src/admin/applications/wizard/types.ts index d36340c87c..122a7c6ead 100644 --- a/web/src/admin/applications/wizard/types.ts +++ b/web/src/admin/applications/wizard/types.ts @@ -5,6 +5,7 @@ import { type LDAPProviderRequest, type OAuth2ProviderRequest, type ProvidersSamlImportMetadataCreateRequest, + ProxyMode, type ProxyProviderRequest, type RACProviderRequest, type RadiusProviderRequest, @@ -27,6 +28,7 @@ export interface ApplicationWizardState { providerModel: string; app: Partial; provider: OneOfProvider; + proxyMode?: ProxyMode; errors: ValidationError; } diff --git a/web/src/admin/providers/ProviderWizard.ts b/web/src/admin/providers/ProviderWizard.ts index 2c7ba266b9..fd227851f7 100644 --- a/web/src/admin/providers/ProviderWizard.ts +++ b/web/src/admin/providers/ProviderWizard.ts @@ -43,8 +43,12 @@ export class ProviderWizard extends AKElement { @query("ak-wizard") wizard?: Wizard; - async firstUpdated(): Promise { - this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(); + connectedCallback() { + super.connectedCallback(); + new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => { + console.log(providerTypes); + this.providerTypes = providerTypes; + }); } render(): TemplateResult { diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index b39e010f18..4cdee45371 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -1,39 +1,18 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; 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 { 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, TemplateResult, html } from "lit"; +import { CSSResult } from "lit"; 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 PFList from "@patternfly/patternfly/components/List/list.css"; import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css"; -import { - FlowsInstancesListDesignationEnum, - ProvidersApi, - ProxyMode, - ProxyProvider, -} from "@goauthentik/api"; +import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api"; -import { - makeProxyPropertyMappingsSelector, - proxyPropertyMappingsProvider, -} from "./ProxyProviderPropertyMappings.js"; +import { SetMode, SetShowHttpBasic, renderForm } from "./ProxyProviderFormForm.js"; @customElement("ak-provider-proxy-form") export class ProxyProviderFormPage extends BaseProviderForm { @@ -73,376 +52,22 @@ export class ProxyProviderFormPage extends BaseProviderForm { } } - renderHttpBasic(): TemplateResult { - return html` - - - - `; - } - - renderModeSelector(): TemplateResult { - const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => { + renderForm() { + const onSetMode: SetMode = (ev) => { this.mode = ev.detail.value; }; - // prettier-ignore - return html` - - - - - - `; - } + const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => { + const el = ev.target as HTMLInputElement; + this.showHttpBasic = el.checked; + }; - renderSettings(): TemplateResult { - switch (this.mode) { - case ProxyMode.Proxy: - return html`

- ${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.", - )} -

- - -

- ${msg( - "The external URL you'll access the application at. Include any non-standard port.", - )} -

-
- - -

- ${msg("Upstream host that the requests are forwarded to.")} -

-
- - -

- ${msg("Validate SSL Certificates of upstream servers.")} -

-
`; - case ProxyMode.ForwardSingle: - return html`

- ${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).", - )} -

- - -

- ${msg( - "The external URL you'll access the application at. Include any non-standard port.", - )} -

-
`; - case ProxyMode.ForwardDomain: - return html`

- ${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.", - )} -

-
- ${msg("An example setup can look like this:")} -
    -
  • ${msg("authentik running on auth.example.com")}
  • -
  • ${msg("app1 running on app1.example.com")}
  • -
- ${msg( - "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.", - )} -
- - -

- ${msg( - "The external URL you'll authenticate at. The authentik core server should be reachable under this URL.", - )} -

-
- - -

- ${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'.", - )} -

-
`; - case ProxyMode.UnknownDefaultOpenApi: - return html`

${msg("Unknown proxy mode")}

`; - } - } - - renderForm(): TemplateResult { - return html` - - - - - -

- ${msg("Flow used when authorizing this provider.")} -

-
- -
-
${this.renderModeSelector()}
- -
- - -

- ${msg("Configure how long tokens are valid for.")} -

- -
- - - ${msg("Advanced protocol settings")} -
- - - - - -

- ${msg("Additional scope mappings, which are passed to the proxy.")} -

-
- - - -

- ${msg( - "Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.", - )} -

-

- ${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.", - )} -

-
-
-
- - ${msg("Authentication settings")} -
- - -

- ${msg( - "When enabled, authentik will intercept the Authorization header to authenticate the request.", - )} -

-
- - -

- ${msg( - "Send a custom HTTP-Basic Authentication header based on values from authentik.", - )} -

-
- ${this.showHttpBasic ? this.renderHttpBasic() : html``} - - -

- ${msg( - "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", - )} -

-
-
-
- - - ${msg("Advanced flow settings")} -
- - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - -

- ${msg("Flow used when logging out of this provider.")} -

-
-
-
- `; + return renderForm(this.instance ?? {}, [], { + mode: this.mode, + onSetMode, + showHttpBasic: this.showHttpBasic, + onSetShowHttpBasic, + }); } } diff --git a/web/src/admin/providers/proxy/ProxyProviderFormForm.ts b/web/src/admin/providers/proxy/ProxyProviderFormForm.ts new file mode 100644 index 0000000000..f27ea019a4 --- /dev/null +++ b/web/src/admin/providers/proxy/ProxyProviderFormForm.ts @@ -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) => void; +export type SetShowHttpBasic = (ev: Event) => void; + +export interface ProxyModeExtraArgs { + mode: ProxyMode; + onSetMode: SetMode; + showHttpBasic: boolean; + onSetShowHttpBasic: SetShowHttpBasic; +} + +function renderHttpBasic(provider: ProxyProvider) { + return html` + + + + `; +} + +function renderModeSelector(mode: ProxyMode, onSet: SetMode) { + // prettier-ignore + return html` + + + + `; +} + +function renderProxySettings(provider: ProxyProvider) { + return html`

+ ${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.", + )} +

+ + +

+ ${msg( + "The external URL you'll access the application at. Include any non-standard port.", + )} +

+
+ + +

+ ${msg("Upstream host that the requests are forwarded to.")} +

+
+ + +

+ ${msg("Validate SSL Certificates of upstream servers.")} +

+
`; +} + +function renderForwardSingleSettings(provider: ProxyProvider) { + return html`

+ ${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).", + )} +

+ + +

+ ${msg( + "The external URL you'll access the application at. Include any non-standard port.", + )} +

+
`; +} + +function renderForwardDomainSettings(provider: ProxyProvider) { + return html`

+ ${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.", + )} +

+
+ ${msg("An example setup can look like this:")} +
    +
  • ${msg("authentik running on auth.example.com")}
  • +
  • ${msg("app1 running on app1.example.com")}
  • +
+ ${msg( + "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.", + )} +
+ + +

+ ${msg( + "The external URL you'll authenticate at. The authentik core server should be reachable under this URL.", + )} +

+
+ + +

+ ${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'.", + )} +

+
`; +} + +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, + errors: ValidationError, + args: ProxyModeExtraArgs, +) { + const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args; + + return html` + + + + + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ +
+
${renderModeSelector(mode, onSetMode)}
+ +
+ + +

${msg("Configure how long tokens are valid for.")}

+ +
+ + + ${msg("Advanced protocol settings")} +
+ + + + + +

+ ${msg("Additional scope mappings, which are passed to the proxy.")} +

+
+ + + +

+ ${msg( + "Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.", + )} +

+

+ ${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.", + )} +

+
+
+
+ + ${msg("Authentication settings")} +
+ + +

+ ${msg( + "When enabled, authentik will intercept the Authorization header to authenticate the request.", + )} +

+
+ + +

+ ${msg( + "Send a custom HTTP-Basic Authentication header based on values from authentik.", + )} +

+
+ ${showHttpBasic ? renderHttpBasic(provider) : nothing} + + +

+ ${msg( + "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", + )} +

+
+
+
+ + + ${msg("Advanced flow settings")} +
+ + +

+ ${msg( + "Flow used when a user access this provider and is not authenticated.", + )} +

+
+ + +

+ ${msg("Flow used when logging out of this provider.")} +

+
+
+
+ `; +} diff --git a/web/src/admin/providers/saml/SAMLProviderFormForm.ts b/web/src/admin/providers/saml/SAMLProviderFormForm.ts index 5fe9d9b873..760ddf24e8 100644 --- a/web/src/admin/providers/saml/SAMLProviderFormForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderFormForm.ts @@ -350,14 +350,12 @@ export function renderForm(

${msg( diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts index 6a9c65de08..7a20af84d6 100644 --- a/web/src/admin/stages/identification/IdentificationStageForm.ts +++ b/web/src/admin/stages/identification/IdentificationStageForm.ts @@ -21,6 +21,7 @@ import { SourcesApi, Stage, StagesApi, + StagesCaptchaListRequest, StagesPasswordListRequest, UserFieldsEnum, } from "@goauthentik/api"; @@ -140,19 +141,13 @@ export class IdentificationStageForm extends BaseStageForm ).stagesPasswordList(args); return stages.results; }} - .groupBy=${(items: Stage[]) => { - return groupBy(items, (stage) => stage.verboseNamePlural); - }} - .renderElement=${(stage: Stage): string => { - return stage.name; - }} - .value=${(stage: Stage | undefined): string | undefined => { - return stage?.pk; - }} - .selected=${(stage: Stage): boolean => { - return stage.pk === this.instance?.passwordStage; - }} - ?blankable=${true} + .groupBy=${(items: Stage[]) => + groupBy(items, (stage) => stage.verboseNamePlural)} + .renderElement=${(stage: Stage): string => stage.name} + .value=${(stage: Stage | undefined): string | undefined => stage?.pk} + .selected=${(stage: Stage): boolean => + stage.pk === this.instance?.passwordStage} + blankable >

@@ -161,6 +156,35 @@ export class IdentificationStageForm extends BaseStageForm )}

+ + => { + const args: StagesCaptchaListRequest = { + ordering: "name", + }; + if (query !== undefined) { + args.search = query; + } + const stages = await new StagesApi( + DEFAULT_CONFIG, + ).stagesCaptchaList(args); + return stages.results; + }} + .groupBy=${(items: Stage[]) => + groupBy(items, (stage) => stage.verboseNamePlural)} + .renderElement=${(stage: Stage): string => stage.name} + .value=${(stage: Stage | undefined): string | undefined => stage?.pk} + .selected=${(stage: Stage): boolean => + stage.pk === this.instance?.captchaStage} + blankable + > + +

+ ${msg( + "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.", + )} +

+