Compare commits
	
		
			6 Commits
		
	
	
		
			better-ver
			...
			stages/ide
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ad652bde38 | |||
| 9e813bf404 | |||
| 11e708a45a | |||
| 1e6e4a0bbc | |||
| 2149e81d8f | |||
| 98dc794597 | 
| @ -69,8 +69,8 @@ class MessageStage(StageView): | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         """Show a pre-configured message after the flow is done""" | ||||
|         message = getattr(self.executor.current_stage, "message", "") | ||||
|         level = getattr(self.executor.current_stage, "level", messages.SUCCESS) | ||||
|         message = getattr(self.current_stage, "message", "") | ||||
|         level = getattr(self.current_stage, "level", messages.SUCCESS) | ||||
|         messages.add_message( | ||||
|             self.request, | ||||
|             level, | ||||
| @ -486,9 +486,7 @@ class GroupUpdateStage(StageView): | ||||
|     def handle_groups(self) -> bool: | ||||
|         self.source: Source = self.executor.plan.context[PLAN_CONTEXT_SOURCE] | ||||
|         self.user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] | ||||
|         self.group_connection_type: GroupSourceConnection = ( | ||||
|             self.executor.current_stage.group_connection_type | ||||
|         ) | ||||
|         self.group_connection_type: GroupSourceConnection = self.current_stage.group_connection_type | ||||
|  | ||||
|         raw_groups: dict[str, dict[str, Any | dict[str, Any]]] = self.executor.plan.context[ | ||||
|             PLAN_CONTEXT_SOURCE_GROUPS | ||||
|  | ||||
| @ -17,7 +17,7 @@ from authentik.flows.challenge import RedirectChallenge | ||||
| from authentik.flows.exceptions import FlowNonApplicableException | ||||
| from authentik.flows.models import in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner | ||||
| from authentik.flows.stage import RedirectStage | ||||
| from authentik.flows.stage import RedirectStageChallengeView | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| @ -83,7 +83,7 @@ class RACInterface(InterfaceView): | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
|  | ||||
| class RACFinalStage(RedirectStage): | ||||
| class RACFinalStage(RedirectStageChallengeView): | ||||
|     """RAC Connection final stage, set the connection token in the stage""" | ||||
|  | ||||
|     endpoint: Endpoint | ||||
| @ -91,9 +91,9 @@ class RACFinalStage(RedirectStage): | ||||
|     application: Application | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|         self.endpoint = self.executor.current_stage.endpoint | ||||
|         self.provider = self.executor.current_stage.provider | ||||
|         self.application = self.executor.current_stage.application | ||||
|         self.endpoint = self.current_stage.endpoint | ||||
|         self.provider = self.current_stage.provider | ||||
|         self.application = self.current_stage.application | ||||
|         # Check policies bound to endpoint directly | ||||
|         engine = PolicyEngine(self.endpoint, self.request.user, self.request) | ||||
|         engine.use_cache = False | ||||
| @ -132,7 +132,7 @@ class RACFinalStage(RedirectStage): | ||||
|             flow=self.executor.plan.flow_pk, | ||||
|             endpoint=self.endpoint.name, | ||||
|         ).from_http(self.request) | ||||
|         self.executor.current_stage.destination = self.request.build_absolute_uri( | ||||
|         self.current_stage.destination = self.request.build_absolute_uri( | ||||
|             reverse("authentik_providers_rac:if-rac", kwargs={"token": str(token.token)}) | ||||
|         ) | ||||
|         return super().get_challenge(*args, **kwargs) | ||||
|  | ||||
| @ -21,16 +21,15 @@ from authentik.lib.utils.time import timedelta_from_string | ||||
| PLAN_CONTEXT_RESUME_TOKEN = "resume_token"  # nosec | ||||
|  | ||||
|  | ||||
| class SourceStageView(ChallengeStageView): | ||||
| class SourceStageView(ChallengeStageView[SourceStage]): | ||||
|     """Suspend the current flow execution and send the user to a source, | ||||
|     after which this flow execution is resumed.""" | ||||
|  | ||||
|     login_button: UILoginButton | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|         current_stage: SourceStage = self.executor.current_stage | ||||
|         source: Source = ( | ||||
|             Source.objects.filter(pk=current_stage.source_id).select_subclasses().first() | ||||
|             Source.objects.filter(pk=self.current_stage.source_id).select_subclasses().first() | ||||
|         ) | ||||
|         if not source: | ||||
|             self.logger.warning("Source does not exist") | ||||
| @ -56,11 +55,10 @@ class SourceStageView(ChallengeStageView): | ||||
|         pending_user: User = self.get_pending_user() | ||||
|         if pending_user.is_anonymous or not pending_user.pk: | ||||
|             pending_user = get_anonymous_user() | ||||
|         current_stage: SourceStage = self.executor.current_stage | ||||
|         identifier = slugify(f"ak-source-stage-{current_stage.name}-{str(uuid4())}") | ||||
|         identifier = slugify(f"ak-source-stage-{self.current_stage.name}-{str(uuid4())}") | ||||
|         # Don't check for validity here, we only care if the token exists | ||||
|         tokens = FlowToken.objects.filter(identifier=identifier) | ||||
|         valid_delta = timedelta_from_string(current_stage.resume_timeout) | ||||
|         valid_delta = timedelta_from_string(self.current_stage.resume_timeout) | ||||
|         if not tokens.exists(): | ||||
|             return FlowToken.objects.create( | ||||
|                 expires=now() + valid_delta, | ||||
|  | ||||
| @ -74,9 +74,9 @@ class FlowPlan: | ||||
|  | ||||
|     def redirect(self, destination: str): | ||||
|         """Insert a redirect stage as next stage""" | ||||
|         from authentik.flows.stage import RedirectStage | ||||
|         from authentik.flows.stage import RedirectStageChallengeView | ||||
|  | ||||
|         self.insert_stage(in_memory_stage(RedirectStage, destination=destination)) | ||||
|         self.insert_stage(in_memory_stage(RedirectStageChallengeView, destination=destination)) | ||||
|  | ||||
|     def next(self, http_request: HttpRequest | None) -> FlowStageBinding | None: | ||||
|         """Return next pending stage from the bottom of the list""" | ||||
|  | ||||
| @ -30,6 +30,7 @@ from authentik.lib.avatars import DEFAULT_AVATAR, get_avatar | ||||
| from authentik.lib.utils.reflection import class_to_path | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from authentik.flows.models import Stage | ||||
|     from authentik.flows.views.executor import FlowExecutorView | ||||
|  | ||||
| PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier" | ||||
| @ -40,20 +41,21 @@ HIST_FLOWS_STAGE_TIME = Histogram( | ||||
| ) | ||||
|  | ||||
|  | ||||
| class StageView(View): | ||||
| class StageView[TStage: "Stage"](View): | ||||
|     """Abstract Stage""" | ||||
|  | ||||
|     executor: "FlowExecutorView" | ||||
|     current_stage: TStage | ||||
|  | ||||
|     request: HttpRequest = None | ||||
|  | ||||
|     logger: BoundLogger | ||||
|  | ||||
|     def __init__(self, executor: "FlowExecutorView", **kwargs): | ||||
|     def __init__(self, executor: "FlowExecutorView", current_stage: TStage | None = None, **kwargs): | ||||
|         self.executor = executor | ||||
|         current_stage = getattr(self.executor, "current_stage", None) | ||||
|         self.current_stage = current_stage or executor.current_stage | ||||
|         self.logger = get_logger().bind( | ||||
|             stage=getattr(current_stage, "name", None), | ||||
|             stage=getattr(self.current_stage, "name", None), | ||||
|             stage_view=class_to_path(type(self)), | ||||
|         ) | ||||
|         super().__init__(**kwargs) | ||||
| @ -80,7 +82,7 @@ class StageView(View): | ||||
|         """Cleanup session""" | ||||
|  | ||||
|  | ||||
| class ChallengeStageView(StageView): | ||||
| class ChallengeStageView[TStage: "Stage"](StageView[TStage]): | ||||
|     """Stage view which response with a challenge""" | ||||
|  | ||||
|     response_class = ChallengeResponse | ||||
| @ -253,12 +255,12 @@ class AccessDeniedChallengeView(ChallengeStageView): | ||||
|         return self.executor.cancel() | ||||
|  | ||||
|  | ||||
| class RedirectStage(ChallengeStageView): | ||||
| class RedirectStageChallengeView(ChallengeStageView): | ||||
|     """Redirect to any URL""" | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> RedirectChallenge: | ||||
|         destination = getattr( | ||||
|             self.executor.current_stage, "destination", reverse("authentik_core:root-redirect") | ||||
|             self.current_stage, "destination", reverse("authentik_core:root-redirect") | ||||
|         ) | ||||
|         return RedirectChallenge( | ||||
|             data={ | ||||
|  | ||||
| @ -164,7 +164,7 @@ class SAMLProvider(Provider): | ||||
|     ) | ||||
|  | ||||
|     sign_assertion = models.BooleanField(default=True) | ||||
|     sign_response = models.BooleanField(default=True) | ||||
|     sign_response = models.BooleanField(default=False) | ||||
|  | ||||
|     @property | ||||
|     def launch_url(self) -> str | None: | ||||
|  | ||||
| @ -32,7 +32,7 @@ class AuthenticatorDuoChallengeResponse(ChallengeResponse): | ||||
|     component = CharField(default="ak-stage-authenticator-duo") | ||||
|  | ||||
|  | ||||
| class AuthenticatorDuoStageView(ChallengeStageView): | ||||
| class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]): | ||||
|     """Duo stage""" | ||||
|  | ||||
|     response_class = AuthenticatorDuoChallengeResponse | ||||
| @ -40,9 +40,8 @@ class AuthenticatorDuoStageView(ChallengeStageView): | ||||
|     def duo_enroll(self): | ||||
|         """Enroll User with Duo API and save results""" | ||||
|         user = self.get_pending_user() | ||||
|         stage: AuthenticatorDuoStage = self.executor.current_stage | ||||
|         try: | ||||
|             enroll = stage.auth_client().enroll(user.username) | ||||
|             enroll = self.current_stage.auth_client().enroll(user.username) | ||||
|         except RuntimeError as exc: | ||||
|             Event.new( | ||||
|                 EventAction.CONFIGURATION_ERROR, | ||||
| @ -54,7 +53,6 @@ class AuthenticatorDuoStageView(ChallengeStageView): | ||||
|         return enroll | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         stage: AuthenticatorDuoStage = self.executor.current_stage | ||||
|         if SESSION_KEY_DUO_ENROLL not in self.request.session: | ||||
|             self.duo_enroll() | ||||
|         enroll = self.request.session[SESSION_KEY_DUO_ENROLL] | ||||
| @ -62,15 +60,14 @@ class AuthenticatorDuoStageView(ChallengeStageView): | ||||
|             data={ | ||||
|                 "activation_barcode": enroll["activation_barcode"], | ||||
|                 "activation_code": enroll["activation_code"], | ||||
|                 "stage_uuid": str(stage.stage_uuid), | ||||
|                 "stage_uuid": str(self.current_stage.stage_uuid), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||
|         # Duo Challenge has already been validated | ||||
|         stage: AuthenticatorDuoStage = self.executor.current_stage | ||||
|         enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL) | ||||
|         enroll_status = stage.auth_client().enroll_status( | ||||
|         enroll_status = self.current_stage.auth_client().enroll_status( | ||||
|             enroll["user_id"], enroll["activation_code"] | ||||
|         ) | ||||
|         if enroll_status != "success": | ||||
| @ -82,7 +79,7 @@ class AuthenticatorDuoStageView(ChallengeStageView): | ||||
|                 name="Duo Authenticator", | ||||
|                 user=self.get_pending_user(), | ||||
|                 duo_user_id=enroll["user_id"], | ||||
|                 stage=stage, | ||||
|                 stage=self.current_stage, | ||||
|                 last_t=now(), | ||||
|             ) | ||||
|         else: | ||||
|  | ||||
| @ -57,21 +57,20 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse): | ||||
|         return super().validate(attrs) | ||||
|  | ||||
|  | ||||
| class AuthenticatorSMSStageView(ChallengeStageView): | ||||
| class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]): | ||||
|     """OTP sms Setup stage""" | ||||
|  | ||||
|     response_class = AuthenticatorSMSChallengeResponse | ||||
|  | ||||
|     def validate_and_send(self, phone_number: str): | ||||
|         """Validate phone number and send message""" | ||||
|         stage: AuthenticatorSMSStage = self.executor.current_stage | ||||
|         hashed_number = hash_phone_number(phone_number) | ||||
|         query = Q(phone_number=hashed_number) | Q(phone_number=phone_number) | ||||
|         if SMSDevice.objects.filter(query, stage=stage.pk).exists(): | ||||
|         if SMSDevice.objects.filter(query, stage=self.current_stage.pk).exists(): | ||||
|             raise ValidationError(_("Invalid phone number")) | ||||
|         # No code yet, but we have a phone number, so send a verification message | ||||
|         device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE] | ||||
|         stage.send(device.token, device) | ||||
|         self.current_stage.send(device.token, device) | ||||
|  | ||||
|     def _has_phone_number(self) -> str | None: | ||||
|         context = self.executor.plan.context | ||||
| @ -101,10 +100,10 @@ class AuthenticatorSMSStageView(ChallengeStageView): | ||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         user = self.get_pending_user() | ||||
|  | ||||
|         stage: AuthenticatorSMSStage = self.executor.current_stage | ||||
|  | ||||
|         if SESSION_KEY_SMS_DEVICE not in self.request.session: | ||||
|             device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device") | ||||
|             device = SMSDevice( | ||||
|                 user=user, confirmed=False, stage=self.current_stage, name="SMS Device" | ||||
|             ) | ||||
|             device.generate_token(commit=False) | ||||
|             self.request.session[SESSION_KEY_SMS_DEVICE] = device | ||||
|             if phone_number := self._has_phone_number(): | ||||
| @ -130,8 +129,7 @@ class AuthenticatorSMSStageView(ChallengeStageView): | ||||
|         device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE] | ||||
|         if not device.confirmed: | ||||
|             return self.challenge_invalid(response) | ||||
|         stage: AuthenticatorSMSStage = self.executor.current_stage | ||||
|         if stage.verify_only: | ||||
|         if self.current_stage.verify_only: | ||||
|             self.logger.debug("Hashing number on device") | ||||
|             device.set_hashed_number() | ||||
|         device.save() | ||||
|  | ||||
| @ -29,7 +29,7 @@ class AuthenticatorStaticChallengeResponse(ChallengeResponse): | ||||
|     component = CharField(default="ak-stage-authenticator-static") | ||||
|  | ||||
|  | ||||
| class AuthenticatorStaticStageView(ChallengeStageView): | ||||
| class AuthenticatorStaticStageView(ChallengeStageView[AuthenticatorStaticStage]): | ||||
|     """Static OTP Setup stage""" | ||||
|  | ||||
|     response_class = AuthenticatorStaticChallengeResponse | ||||
| @ -48,14 +48,14 @@ class AuthenticatorStaticStageView(ChallengeStageView): | ||||
|             self.logger.debug("No pending user, continuing") | ||||
|             return self.executor.stage_ok() | ||||
|  | ||||
|         stage: AuthenticatorStaticStage = self.executor.current_stage | ||||
|  | ||||
|         if SESSION_STATIC_DEVICE not in self.request.session: | ||||
|             device = StaticDevice(user=user, confirmed=False, name="Static Token") | ||||
|             tokens = [] | ||||
|             for _ in range(0, stage.token_count): | ||||
|             for _ in range(0, self.current_stage.token_count): | ||||
|                 tokens.append( | ||||
|                     StaticToken(device=device, token=generate_id(length=stage.token_length)) | ||||
|                     StaticToken( | ||||
|                         device=device, token=generate_id(length=self.current_stage.token_length) | ||||
|                     ) | ||||
|                 ) | ||||
|             self.request.session[SESSION_STATIC_DEVICE] = device | ||||
|             self.request.session[SESSION_STATIC_TOKENS] = tokens | ||||
|  | ||||
| @ -45,7 +45,7 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse): | ||||
|         return code | ||||
|  | ||||
|  | ||||
| class AuthenticatorTOTPStageView(ChallengeStageView): | ||||
| class AuthenticatorTOTPStageView(ChallengeStageView[AuthenticatorTOTPStage]): | ||||
|     """OTP totp Setup stage""" | ||||
|  | ||||
|     response_class = AuthenticatorTOTPChallengeResponse | ||||
| @ -71,11 +71,12 @@ class AuthenticatorTOTPStageView(ChallengeStageView): | ||||
|             self.logger.debug("No pending user, continuing") | ||||
|             return self.executor.stage_ok() | ||||
|  | ||||
|         stage: AuthenticatorTOTPStage = self.executor.current_stage | ||||
|  | ||||
|         if SESSION_TOTP_DEVICE not in self.request.session: | ||||
|             device = TOTPDevice( | ||||
|                 user=user, confirmed=False, digits=stage.digits, name="TOTP Authenticator" | ||||
|                 user=user, | ||||
|                 confirmed=False, | ||||
|                 digits=self.current_stage.digits, | ||||
|                 name="TOTP Authenticator", | ||||
|             ) | ||||
|  | ||||
|             self.request.session[SESSION_TOTP_DEVICE] = device | ||||
|  | ||||
| @ -151,7 +151,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
| class AuthenticatorValidateStageView(ChallengeStageView): | ||||
| class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateStage]): | ||||
|     """Authenticator Validation""" | ||||
|  | ||||
|     response_class = AuthenticatorValidationChallengeResponse | ||||
| @ -177,16 +177,14 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         # since their challenges are device-independent | ||||
|         seen_classes = [] | ||||
|  | ||||
|         stage: AuthenticatorValidateStage = self.executor.current_stage | ||||
|  | ||||
|         threshold = timedelta_from_string(stage.last_auth_threshold) | ||||
|         threshold = timedelta_from_string(self.current_stage.last_auth_threshold) | ||||
|         allowed_devices = [] | ||||
|  | ||||
|         has_webauthn_filters_set = stage.webauthn_allowed_device_types.exists() | ||||
|         has_webauthn_filters_set = self.current_stage.webauthn_allowed_device_types.exists() | ||||
|  | ||||
|         for device in user_devices: | ||||
|             device_class = device.__class__.__name__.lower().replace("device", "") | ||||
|             if device_class not in stage.device_classes: | ||||
|             if device_class not in self.current_stage.device_classes: | ||||
|                 self.logger.debug("device class not allowed", device_class=device_class) | ||||
|                 continue | ||||
|             if isinstance(device, SMSDevice) and device.is_hashed: | ||||
| @ -199,7 +197,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 and device.device_type | ||||
|                 and has_webauthn_filters_set | ||||
|             ): | ||||
|                 if not stage.webauthn_allowed_device_types.filter( | ||||
|                 if not self.current_stage.webauthn_allowed_device_types.filter( | ||||
|                     pk=device.device_type.pk | ||||
|                 ).exists(): | ||||
|                     self.logger.debug( | ||||
| @ -216,7 +214,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 data={ | ||||
|                     "device_class": device_class, | ||||
|                     "device_uid": device.pk, | ||||
|                     "challenge": get_challenge_for_device(self.request, stage, device), | ||||
|                     "challenge": get_challenge_for_device(self.request, self.current_stage, device), | ||||
|                 } | ||||
|             ) | ||||
|             challenge.is_valid() | ||||
| @ -235,7 +233,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 "device_uid": -1, | ||||
|                 "challenge": get_webauthn_challenge_without_user( | ||||
|                     self.request, | ||||
|                     self.executor.current_stage, | ||||
|                     self.current_stage, | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
| @ -246,7 +244,6 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         """Check if a user is set, and check if the user has any devices | ||||
|         if not, we can skip this entire stage""" | ||||
|         user = self.get_pending_user() | ||||
|         stage: AuthenticatorValidateStage = self.executor.current_stage | ||||
|         if user and not user.is_anonymous: | ||||
|             try: | ||||
|                 challenges = self.get_device_challenges() | ||||
| @ -257,7 +254,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 self.logger.debug("Refusing passwordless flow in non-authentication flow") | ||||
|                 return self.executor.stage_ok() | ||||
|             # Passwordless auth, with just webauthn | ||||
|             if DeviceClasses.WEBAUTHN in stage.device_classes: | ||||
|             if DeviceClasses.WEBAUTHN in self.current_stage.device_classes: | ||||
|                 self.logger.debug("Flow without user, getting generic webauthn challenge") | ||||
|                 challenges = self.get_webauthn_challenge_without_user() | ||||
|             else: | ||||
| @ -267,13 +264,13 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|  | ||||
|         # No allowed devices | ||||
|         if len(challenges) < 1: | ||||
|             if stage.not_configured_action == NotConfiguredAction.SKIP: | ||||
|             if self.current_stage.not_configured_action == NotConfiguredAction.SKIP: | ||||
|                 self.logger.debug("Authenticator not configured, skipping stage") | ||||
|                 return self.executor.stage_ok() | ||||
|             if stage.not_configured_action == NotConfiguredAction.DENY: | ||||
|             if self.current_stage.not_configured_action == NotConfiguredAction.DENY: | ||||
|                 self.logger.debug("Authenticator not configured, denying") | ||||
|                 return self.executor.stage_invalid(_("No (allowed) MFA authenticator configured.")) | ||||
|             if stage.not_configured_action == NotConfiguredAction.CONFIGURE: | ||||
|             if self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE: | ||||
|                 self.logger.debug("Authenticator not configured, forcing configure") | ||||
|                 return self.prepare_stages(user) | ||||
|         return super().get(request, *args, **kwargs) | ||||
| @ -282,8 +279,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         """Check how the user can configure themselves. If no stages are set, return an error. | ||||
|         If a single stage is set, insert that stage directly. If multiple are selected, include | ||||
|         them in the challenge.""" | ||||
|         stage: AuthenticatorValidateStage = self.executor.current_stage | ||||
|         if not stage.configuration_stages.exists(): | ||||
|         if not self.current_stage.configuration_stages.exists(): | ||||
|             Event.new( | ||||
|                 EventAction.CONFIGURATION_ERROR, | ||||
|                 message=( | ||||
| @ -293,15 +289,19 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 stage=self, | ||||
|             ).from_http(self.request).set_user(user).save() | ||||
|             return self.executor.stage_invalid() | ||||
|         if stage.configuration_stages.count() == 1: | ||||
|             next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk) | ||||
|         if self.current_stage.configuration_stages.count() == 1: | ||||
|             next_stage = Stage.objects.get_subclass( | ||||
|                 pk=self.current_stage.configuration_stages.first().pk | ||||
|             ) | ||||
|             self.logger.debug("Single stage configured, auto-selecting", stage=next_stage) | ||||
|             self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage | ||||
|             # Because that normal execution only happens on post, we directly inject it here and | ||||
|             # return it | ||||
|             self.executor.plan.insert_stage(next_stage) | ||||
|             return self.executor.stage_ok() | ||||
|         stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses() | ||||
|         stages = Stage.objects.filter( | ||||
|             pk__in=self.current_stage.configuration_stages.all() | ||||
|         ).select_subclasses() | ||||
|         self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages | ||||
|         return super().get(self.request, *args, **kwargs) | ||||
|  | ||||
| @ -309,7 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         res = super().post(request, *args, **kwargs) | ||||
|         if ( | ||||
|             PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context | ||||
|             and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE | ||||
|             and self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE | ||||
|         ): | ||||
|             self.logger.debug("Got selected stage in context, running that") | ||||
|             stage_pk = self.executor.plan.context.get(PLAN_CONTEXT_SELECTED_STAGE) | ||||
| @ -351,7 +351,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|     def cookie_jwt_key(self) -> str: | ||||
|         """Signing key for MFA Cookie for this stage""" | ||||
|         return sha256( | ||||
|             f"{get_unique_identifier()}:{self.executor.current_stage.pk.hex}".encode("ascii") | ||||
|             f"{get_unique_identifier()}:{self.current_stage.pk.hex}".encode("ascii") | ||||
|         ).hexdigest() | ||||
|  | ||||
|     def check_mfa_cookie(self, allowed_devices: list[Device]): | ||||
| @ -362,12 +362,11 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         correct user and with an allowed class""" | ||||
|         if COOKIE_NAME_MFA not in self.request.COOKIES: | ||||
|             return | ||||
|         stage: AuthenticatorValidateStage = self.executor.current_stage | ||||
|         threshold = timedelta_from_string(stage.last_auth_threshold) | ||||
|         threshold = timedelta_from_string(self.current_stage.last_auth_threshold) | ||||
|         latest_allowed = datetime.now() + threshold | ||||
|         try: | ||||
|             payload = decode(self.request.COOKIES[COOKIE_NAME_MFA], self.cookie_jwt_key, ["HS256"]) | ||||
|             if payload["stage"] != stage.pk.hex: | ||||
|             if payload["stage"] != self.current_stage.pk.hex: | ||||
|                 self.logger.warning("Invalid stage PK") | ||||
|                 return | ||||
|             if datetime.fromtimestamp(payload["exp"]) > latest_allowed: | ||||
| @ -385,15 +384,14 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         """Set an MFA cookie to allow users to skip MFA validation in this context (browser) | ||||
|  | ||||
|         The cookie is JWT which is signed with a hash of the secret key and the UID of the stage""" | ||||
|         stage: AuthenticatorValidateStage = self.executor.current_stage | ||||
|         delta = timedelta_from_string(stage.last_auth_threshold) | ||||
|         delta = timedelta_from_string(self.current_stage.last_auth_threshold) | ||||
|         if delta.total_seconds() < 1: | ||||
|             self.logger.info("Not setting MFA cookie since threshold is not set.") | ||||
|             return self.executor.stage_ok() | ||||
|         expiry = datetime.now() + delta | ||||
|         cookie_payload = { | ||||
|             "device": device.pk, | ||||
|             "stage": stage.pk.hex, | ||||
|             "stage": self.current_stage.pk.hex, | ||||
|             "exp": expiry.timestamp(), | ||||
|         } | ||||
|         response = self.executor.stage_ok() | ||||
|  | ||||
| @ -108,7 +108,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): | ||||
|         return registration | ||||
|  | ||||
|  | ||||
| class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
| class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnStage]): | ||||
|     """WebAuthn stage""" | ||||
|  | ||||
|     response_class = AuthenticatorWebAuthnChallengeResponse | ||||
| @ -116,12 +116,11 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         # clear session variables prior to starting a new registration | ||||
|         self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None) | ||||
|         stage: AuthenticatorWebAuthnStage = self.executor.current_stage | ||||
|         user = self.get_pending_user() | ||||
|  | ||||
|         # library accepts none so we store null in the database, but if there is a value | ||||
|         # set, cast it to string to ensure it's not a django class | ||||
|         authenticator_attachment = stage.authenticator_attachment | ||||
|         authenticator_attachment = self.current_stage.authenticator_attachment | ||||
|         if authenticator_attachment: | ||||
|             authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment)) | ||||
|  | ||||
| @ -132,8 +131,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|             user_name=user.username, | ||||
|             user_display_name=user.name, | ||||
|             authenticator_selection=AuthenticatorSelectionCriteria( | ||||
|                 resident_key=ResidentKeyRequirement(str(stage.resident_key_requirement)), | ||||
|                 user_verification=UserVerificationRequirement(str(stage.user_verification)), | ||||
|                 resident_key=ResidentKeyRequirement( | ||||
|                     str(self.current_stage.resident_key_requirement) | ||||
|                 ), | ||||
|                 user_verification=UserVerificationRequirement( | ||||
|                     str(self.current_stage.user_verification) | ||||
|                 ), | ||||
|                 authenticator_attachment=authenticator_attachment, | ||||
|             ), | ||||
|             attestation=AttestationConveyancePreference.DIRECT, | ||||
|  | ||||
| @ -70,7 +70,7 @@ class CaptchaChallengeResponse(ChallengeResponse): | ||||
|         return data | ||||
|  | ||||
|  | ||||
| class CaptchaStageView(ChallengeStageView): | ||||
| class CaptchaStageView(ChallengeStageView[CaptchaChallenge]): | ||||
|     """Simple captcha checker, logic is handled in django-captcha module""" | ||||
|  | ||||
|     response_class = CaptchaChallengeResponse | ||||
| @ -78,8 +78,8 @@ class CaptchaStageView(ChallengeStageView): | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         return CaptchaChallenge( | ||||
|             data={ | ||||
|                 "js_url": self.executor.current_stage.js_url, | ||||
|                 "site_key": self.executor.current_stage.public_key, | ||||
|                 "js_url": self.current_stage.js_url, | ||||
|                 "site_key": self.current_stage.public_key, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @ -87,6 +87,6 @@ class CaptchaStageView(ChallengeStageView): | ||||
|         response = response.validated_data["token"] | ||||
|         self.executor.plan.context[PLAN_CONTEXT_CAPTCHA] = { | ||||
|             "response": response, | ||||
|             "stage": self.executor.current_stage, | ||||
|             "stage": self.current_stage, | ||||
|         } | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
| @ -48,7 +48,7 @@ class ConsentChallengeResponse(ChallengeResponse): | ||||
|     token = CharField(required=True) | ||||
|  | ||||
|  | ||||
| class ConsentStageView(ChallengeStageView): | ||||
| class ConsentStageView(ChallengeStageView[ConsentStage]): | ||||
|     """Simple consent checker.""" | ||||
|  | ||||
|     response_class = ConsentChallengeResponse | ||||
| @ -72,14 +72,13 @@ class ConsentStageView(ChallengeStageView): | ||||
|         """Check if the current request should require a prompt for non consent reasons, | ||||
|         i.e. this stage injected from another stage, mode is always requireed or no application | ||||
|         is set.""" | ||||
|         current_stage: ConsentStage = self.executor.current_stage | ||||
|         # Make this StageView work when injected, in which case `current_stage` is an instance | ||||
|         # of the base class, and we don't save any consent, as it is assumed to be a one-time | ||||
|         # prompt | ||||
|         if not isinstance(current_stage, ConsentStage): | ||||
|         if not isinstance(self.current_stage, ConsentStage): | ||||
|             return True | ||||
|         # For always require, we always return the challenge | ||||
|         if current_stage.mode == ConsentMode.ALWAYS_REQUIRE: | ||||
|         if self.current_stage.mode == ConsentMode.ALWAYS_REQUIRE: | ||||
|             return True | ||||
|         # at this point we need to check consent from database | ||||
|         if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context: | ||||
| @ -125,7 +124,6 @@ class ConsentStageView(ChallengeStageView): | ||||
|             return self.get(self.request) | ||||
|         if self.should_always_prompt(): | ||||
|             return self.executor.stage_ok() | ||||
|         current_stage: ConsentStage = self.executor.current_stage | ||||
|         application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION] | ||||
|         permissions = self.executor.plan.context.get( | ||||
|             PLAN_CONTEXT_CONSENT_PERMISSIONS, [] | ||||
| @ -139,9 +137,9 @@ class ConsentStageView(ChallengeStageView): | ||||
|             ) | ||||
|         consent: UserConsent = self.executor.plan.context[PLAN_CONTEXT_CONSENT] | ||||
|         consent.permissions = permissions_string | ||||
|         if current_stage.mode == ConsentMode.PERMANENT: | ||||
|         if self.current_stage.mode == ConsentMode.PERMANENT: | ||||
|             consent.expiring = False | ||||
|         if current_stage.mode == ConsentMode.EXPIRING: | ||||
|             consent.expires = now() + timedelta_from_string(current_stage.consent_expire_in) | ||||
|         if self.current_stage.mode == ConsentMode.EXPIRING: | ||||
|             consent.expires = now() + timedelta_from_string(self.current_stage.consent_expire_in) | ||||
|         consent.save() | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
| @ -6,11 +6,10 @@ from authentik.flows.stage import StageView | ||||
| from authentik.stages.deny.models import DenyStage | ||||
|  | ||||
|  | ||||
| class DenyStageView(StageView): | ||||
| class DenyStageView(StageView[DenyStage]): | ||||
|     """Cancels the current flow""" | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Cancels the current flow""" | ||||
|         stage: DenyStage = self.executor.current_stage | ||||
|         message = self.executor.plan.context.get("deny_message", stage.deny_message) | ||||
|         message = self.executor.plan.context.get("deny_message", self.current_stage.deny_message) | ||||
|         return self.executor.stage_invalid(message) | ||||
|  | ||||
| @ -30,11 +30,11 @@ class DummyStageView(ChallengeStageView): | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         if self.executor.current_stage.throw_error: | ||||
|         if self.current_stage.throw_error: | ||||
|             raise SentryIgnoredException("Test error") | ||||
|         return DummyChallenge( | ||||
|             data={ | ||||
|                 "title": self.executor.current_stage.name, | ||||
|                 "name": self.executor.current_stage.name, | ||||
|                 "title": self.current_stage.name, | ||||
|                 "name": self.current_stage.name, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @ -46,7 +46,7 @@ class EmailChallengeResponse(ChallengeResponse): | ||||
|         raise ValidationError(detail="email-sent", code="email-sent") | ||||
|  | ||||
|  | ||||
| class EmailStageView(ChallengeStageView): | ||||
| class EmailStageView(ChallengeStageView[EmailStage]): | ||||
|     """Email stage which sends Email for verification""" | ||||
|  | ||||
|     response_class = EmailChallengeResponse | ||||
| @ -72,11 +72,10 @@ class EmailStageView(ChallengeStageView): | ||||
|     def get_token(self) -> FlowToken: | ||||
|         """Get token""" | ||||
|         pending_user = self.get_pending_user() | ||||
|         current_stage: EmailStage = self.executor.current_stage | ||||
|         valid_delta = timedelta( | ||||
|             minutes=current_stage.token_expiry + 1 | ||||
|             minutes=self.current_stage.token_expiry + 1 | ||||
|         )  # + 1 because django timesince always rounds down | ||||
|         identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}") | ||||
|         identifier = slugify(f"ak-email-stage-{self.current_stage.name}-{str(uuid4())}") | ||||
|         # Don't check for validity here, we only care if the token exists | ||||
|         tokens = FlowToken.objects.filter(identifier=identifier) | ||||
|         if not tokens.exists(): | ||||
| @ -105,15 +104,14 @@ class EmailStageView(ChallengeStageView): | ||||
|         email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None) | ||||
|         if not email: | ||||
|             email = pending_user.email | ||||
|         current_stage: EmailStage = self.executor.current_stage | ||||
|         token = self.get_token() | ||||
|         # Send mail to user | ||||
|         try: | ||||
|             message = TemplateEmailMessage( | ||||
|                 subject=_(current_stage.subject), | ||||
|                 subject=_(self.current_stage.subject), | ||||
|                 to=[(pending_user.name, email)], | ||||
|                 language=pending_user.locale(self.request), | ||||
|                 template_name=current_stage.template, | ||||
|                 template_name=self.current_stage.template, | ||||
|                 template_context={ | ||||
|                     "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), | ||||
|                     "user": pending_user, | ||||
| @ -121,26 +119,28 @@ class EmailStageView(ChallengeStageView): | ||||
|                     "token": token.key, | ||||
|                 }, | ||||
|             ) | ||||
|             send_mails(current_stage, message) | ||||
|             send_mails(self.current_stage, message) | ||||
|         except TemplateSyntaxError as exc: | ||||
|             Event.new( | ||||
|                 EventAction.CONFIGURATION_ERROR, | ||||
|                 message=_("Exception occurred while rendering E-mail template"), | ||||
|                 error=exception_to_string(exc), | ||||
|                 template=current_stage.template, | ||||
|                 template=self.current_stage.template, | ||||
|             ).from_http(self.request) | ||||
|             raise StageInvalidException from exc | ||||
|  | ||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         # Check if the user came back from the email link to verify | ||||
|         restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None) | ||||
|         restore_token: FlowToken | None = self.executor.plan.context.get( | ||||
|             PLAN_CONTEXT_IS_RESTORED, None | ||||
|         ) | ||||
|         user = self.get_pending_user() | ||||
|         if restore_token: | ||||
|             if restore_token.user != user: | ||||
|                 self.logger.warning("Flow token for non-matching user, denying request") | ||||
|                 return self.executor.stage_invalid() | ||||
|             messages.success(request, _("Successfully verified Email.")) | ||||
|             if self.executor.current_stage.activate_user_on_success: | ||||
|             if self.current_stage.activate_user_on_success: | ||||
|                 user.is_active = True | ||||
|                 user.save() | ||||
|             return self.executor.stage_ok() | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -0,0 +1,26 @@ | ||||
| # Generated by Django 5.0.8 on 2024-08-24 12:58 | ||||
|  | ||||
| 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, the captcha element is shown on the identification stage.", | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 to="authentik_stages_captcha.captchastage", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -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 | ||||
|  | ||||
|  | ||||
| @ -42,6 +43,15 @@ class IdentificationStage(Stage): | ||||
|             ), | ||||
|         ), | ||||
|     ) | ||||
|     captcha_stage = models.ForeignKey( | ||||
|         CaptchaStage, | ||||
|         null=True, | ||||
|         default=None, | ||||
|         on_delete=models.SET_NULL, | ||||
|         help_text=_( | ||||
|             ("When set, the captcha element is shown on the identification stage."), | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     case_insensitive_matching = models.BooleanField( | ||||
|         default=True, | ||||
|  | ||||
| @ -30,9 +30,14 @@ from authentik.lib.utils.urls import reverse_with_qs | ||||
| from authentik.root.middleware import ClientIPMiddleware | ||||
| from authentik.sources.oauth.types.apple import AppleLoginChallenge | ||||
| from authentik.sources.plex.models import PlexAuthenticationChallenge | ||||
| from authentik.stages.captcha.stage import ( | ||||
|     CaptchaChallenge, | ||||
|     CaptchaChallengeResponse, | ||||
|     CaptchaStageView, | ||||
| ) | ||||
| from authentik.stages.identification.models import IdentificationStage | ||||
| from authentik.stages.identification.signals import identification_failed | ||||
| from authentik.stages.password.stage import authenticate | ||||
| from authentik.stages.password.stage import PasswordChallenge, PasswordStageView, authenticate | ||||
|  | ||||
|  | ||||
| @extend_schema_field( | ||||
| @ -63,8 +68,8 @@ class IdentificationChallenge(Challenge): | ||||
|     """Identification challenges with all UI elements""" | ||||
|  | ||||
|     user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True) | ||||
|     password_fields = BooleanField() | ||||
|     allow_show_password = BooleanField(default=False) | ||||
|     password_stage = PasswordChallenge(required=False) | ||||
|     captcha_stage = CaptchaChallenge(required=False) | ||||
|     application_pre = CharField(required=False) | ||||
|     flow_designation = ChoiceField(FlowDesignation.choices) | ||||
|  | ||||
| @ -84,6 +89,7 @@ class IdentificationChallengeResponse(ChallengeResponse): | ||||
|     uid_field = CharField() | ||||
|     password = CharField(required=False, allow_blank=True, allow_null=True) | ||||
|     component = CharField(default="ak-stage-identification") | ||||
|     captcha = CaptchaChallengeResponse(required=False) | ||||
|  | ||||
|     pre_user: User | None = None | ||||
|  | ||||
| @ -128,49 +134,50 @@ class IdentificationChallengeResponse(ChallengeResponse): | ||||
|                 return attrs | ||||
|             raise ValidationError("Failed to authenticate.") | ||||
|         self.pre_user = pre_user | ||||
|         if not current_stage.password_stage: | ||||
|             # No password stage select, don't validate the password | ||||
|             return attrs | ||||
|  | ||||
|         password = attrs.get("password", None) | ||||
|         if not password: | ||||
|             self.stage.logger.warning("Password not set for ident+auth attempt") | ||||
|         try: | ||||
|             with start_span( | ||||
|                 op="authentik.stages.identification.authenticate", | ||||
|                 description="User authenticate call (combo stage)", | ||||
|             ): | ||||
|                 user = authenticate( | ||||
|                     self.stage.request, | ||||
|                     current_stage.password_stage.backends, | ||||
|                     current_stage, | ||||
|                     username=self.pre_user.username, | ||||
|                     password=password, | ||||
|                 ) | ||||
|             if not user: | ||||
|                 raise ValidationError("Failed to authenticate.") | ||||
|             self.pre_user = user | ||||
|         except PermissionDenied as exc: | ||||
|             raise ValidationError(str(exc)) from exc | ||||
|         if current_stage.password_stage: | ||||
|             password = attrs.get("password", None) | ||||
|             if not password: | ||||
|                 self.stage.logger.warning("Password not set for ident+auth attempt") | ||||
|             try: | ||||
|                 with start_span( | ||||
|                     op="authentik.stages.identification.authenticate", | ||||
|                     description="User authenticate call (combo stage)", | ||||
|                 ): | ||||
|                     user = authenticate( | ||||
|                         self.stage.request, | ||||
|                         current_stage.password_stage.backends, | ||||
|                         current_stage, | ||||
|                         username=self.pre_user.username, | ||||
|                         password=password, | ||||
|                     ) | ||||
|                 if not user: | ||||
|                     raise ValidationError("Failed to authenticate.") | ||||
|                 self.pre_user = user | ||||
|             except PermissionDenied as exc: | ||||
|                 raise ValidationError(str(exc)) from exc | ||||
|         print(attrs) | ||||
|         # if current_stage.captcha_stage: | ||||
|         #     captcha = CaptchaStageView(self.stage.executor) | ||||
|         #     captcha.stage = current_stage.captcha_stage | ||||
|         #     captcha.challenge_valid(attrs.get("captcha")) | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
| class IdentificationStageView(ChallengeStageView): | ||||
| class IdentificationStageView(ChallengeStageView[IdentificationStage]): | ||||
|     """Form to identify the user""" | ||||
|  | ||||
|     response_class = IdentificationChallengeResponse | ||||
|  | ||||
|     def get_user(self, uid_value: str) -> User | None: | ||||
|         """Find user instance. Returns None if no user was found.""" | ||||
|         current_stage: IdentificationStage = self.executor.current_stage | ||||
|         query = Q() | ||||
|         for search_field in current_stage.user_fields: | ||||
|         for search_field in self.current_stage.user_fields: | ||||
|             model_field = { | ||||
|                 "email": "email", | ||||
|                 "username": "username", | ||||
|                 "upn": "attributes__upn", | ||||
|             }[search_field] | ||||
|             if current_stage.case_insensitive_matching: | ||||
|             if self.current_stage.case_insensitive_matching: | ||||
|                 model_field += "__iexact" | ||||
|             else: | ||||
|                 model_field += "__exact" | ||||
| @ -191,16 +198,12 @@ class IdentificationStageView(ChallengeStageView): | ||||
|         return _("Continue") | ||||
|  | ||||
|     def get_challenge(self) -> Challenge: | ||||
|         current_stage: IdentificationStage = self.executor.current_stage | ||||
|         challenge = IdentificationChallenge( | ||||
|             data={ | ||||
|                 "component": "ak-stage-identification", | ||||
|                 "primary_action": self.get_primary_action(), | ||||
|                 "user_fields": current_stage.user_fields, | ||||
|                 "password_fields": bool(current_stage.password_stage), | ||||
|                 "allow_show_password": bool(current_stage.password_stage) | ||||
|                 and current_stage.password_stage.allow_show_password, | ||||
|                 "show_source_labels": current_stage.show_source_labels, | ||||
|                 "user_fields": self.current_stage.user_fields, | ||||
|                 "show_source_labels": self.current_stage.show_source_labels, | ||||
|                 "flow_designation": self.executor.flow.designation, | ||||
|             } | ||||
|         ) | ||||
| @ -212,29 +215,39 @@ class IdentificationStageView(ChallengeStageView): | ||||
|             ).name | ||||
|         get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET) | ||||
|         # Check for related enrollment and recovery flow, add URL to view | ||||
|         if current_stage.enrollment_flow: | ||||
|         if self.current_stage.enrollment_flow: | ||||
|             challenge.initial_data["enroll_url"] = reverse_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|                 query=get_qs, | ||||
|                 kwargs={"flow_slug": current_stage.enrollment_flow.slug}, | ||||
|                 kwargs={"flow_slug": self.current_stage.enrollment_flow.slug}, | ||||
|             ) | ||||
|         if current_stage.recovery_flow: | ||||
|         if self.current_stage.recovery_flow: | ||||
|             challenge.initial_data["recovery_url"] = reverse_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|                 query=get_qs, | ||||
|                 kwargs={"flow_slug": current_stage.recovery_flow.slug}, | ||||
|                 kwargs={"flow_slug": self.current_stage.recovery_flow.slug}, | ||||
|             ) | ||||
|         if current_stage.passwordless_flow: | ||||
|         if self.current_stage.passwordless_flow: | ||||
|             challenge.initial_data["passwordless_url"] = reverse_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|                 query=get_qs, | ||||
|                 kwargs={"flow_slug": current_stage.passwordless_flow.slug}, | ||||
|                 kwargs={"flow_slug": self.current_stage.passwordless_flow.slug}, | ||||
|             ) | ||||
|         if self.current_stage.password_stage: | ||||
|             password = PasswordStageView(self.executor, self.current_stage.captcha_stage) | ||||
|             password_challenge = password.get_challenge() | ||||
|             password_challenge.is_valid() | ||||
|             challenge.initial_data["password_stage"] = password_challenge.data | ||||
|         if self.current_stage.captcha_stage: | ||||
|             captcha = CaptchaStageView(self.executor, self.current_stage.captcha_stage) | ||||
|             captcha_challenge = captcha.get_challenge() | ||||
|             captcha_challenge.is_valid() | ||||
|             challenge.initial_data["captcha_stage"] = captcha_challenge.data | ||||
|  | ||||
|         # Check all enabled source, add them if they have a UI Login button. | ||||
|         ui_sources = [] | ||||
|         sources: list[Source] = ( | ||||
|             current_stage.sources.filter(enabled=True).order_by("name").select_subclasses() | ||||
|             self.current_stage.sources.filter(enabled=True).order_by("name").select_subclasses() | ||||
|         ) | ||||
|         for source in sources: | ||||
|             ui_login_button = source.ui_login_button(self.request) | ||||
| @ -249,8 +262,7 @@ class IdentificationStageView(ChallengeStageView): | ||||
|  | ||||
|     def challenge_valid(self, response: IdentificationChallengeResponse) -> HttpResponse: | ||||
|         self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = response.pre_user | ||||
|         current_stage: IdentificationStage = self.executor.current_stage | ||||
|         if not current_stage.show_matched_user: | ||||
|         if not self.current_stage.show_matched_user: | ||||
|             self.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = ( | ||||
|                 response.validated_data.get("uid_field") | ||||
|             ) | ||||
|  | ||||
| @ -17,7 +17,7 @@ INVITATION_IN_EFFECT = "invitation_in_effect" | ||||
| INVITATION = "invitation" | ||||
|  | ||||
|  | ||||
| class InvitationStageView(StageView): | ||||
| class InvitationStageView(StageView[InvitationStage]): | ||||
|     """Finalise Authentication flow by logging the user in""" | ||||
|  | ||||
|     def get_token(self) -> str | None: | ||||
| @ -52,11 +52,10 @@ class InvitationStageView(StageView): | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Apply data to the current flow based on a URL""" | ||||
|         stage: InvitationStage = self.executor.current_stage | ||||
|  | ||||
|         invite = self.get_invite() | ||||
|         if not invite: | ||||
|             if stage.continue_flow_without_invitation: | ||||
|             if self.current_stage.continue_flow_without_invitation: | ||||
|                 return self.executor.stage_ok() | ||||
|             return self.executor.stage_invalid(_("Invalid invite/invite not found")) | ||||
|  | ||||
|  | ||||
| @ -130,7 +130,7 @@ class PasswordChallengeResponse(ChallengeResponse): | ||||
|         return password | ||||
|  | ||||
|  | ||||
| class PasswordStageView(ChallengeStageView): | ||||
| class PasswordStageView(ChallengeStageView[PasswordStage]): | ||||
|     """Authentication stage which authenticates against django's AuthBackend""" | ||||
|  | ||||
|     response_class = PasswordChallengeResponse | ||||
| @ -138,7 +138,7 @@ class PasswordStageView(ChallengeStageView): | ||||
|     def get_challenge(self) -> Challenge: | ||||
|         challenge = PasswordChallenge( | ||||
|             data={ | ||||
|                 "allow_show_password": self.executor.current_stage.allow_show_password, | ||||
|                 "allow_show_password": self.current_stage.allow_show_password, | ||||
|             } | ||||
|         ) | ||||
|         recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) | ||||
| @ -154,10 +154,9 @@ class PasswordStageView(ChallengeStageView): | ||||
|         if SESSION_KEY_INVALID_TRIES not in self.request.session: | ||||
|             self.request.session[SESSION_KEY_INVALID_TRIES] = 0 | ||||
|         self.request.session[SESSION_KEY_INVALID_TRIES] += 1 | ||||
|         current_stage: PasswordStage = self.executor.current_stage | ||||
|         if ( | ||||
|             self.request.session[SESSION_KEY_INVALID_TRIES] | ||||
|             >= current_stage.failed_attempts_before_cancel | ||||
|             >= self.current_stage.failed_attempts_before_cancel | ||||
|         ): | ||||
|             self.logger.debug("User has exceeded maximum tries") | ||||
|             del self.request.session[SESSION_KEY_INVALID_TRIES] | ||||
|  | ||||
| @ -222,7 +222,7 @@ class PromptStageView(ChallengeStageView): | ||||
|         return serializers | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         fields: list[Prompt] = list(self.executor.current_stage.fields.all().order_by("order")) | ||||
|         fields: list[Prompt] = list(self.current_stage.fields.all().order_by("order")) | ||||
|         context_prompt = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}) | ||||
|         serializers = self.get_prompt_challenge_fields(fields, context_prompt) | ||||
|         challenge = PromptChallenge( | ||||
| @ -239,7 +239,7 @@ class PromptStageView(ChallengeStageView): | ||||
|             instance=None, | ||||
|             data=data, | ||||
|             request=self.request, | ||||
|             stage_instance=self.executor.current_stage, | ||||
|             stage_instance=self.current_stage, | ||||
|             stage=self, | ||||
|             plan=self.executor.plan, | ||||
|             user=self.get_pending_user(), | ||||
|  | ||||
| @ -7,9 +7,10 @@ from django.utils.translation import gettext as _ | ||||
|  | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.stages.user_delete.models import UserDeleteStage | ||||
|  | ||||
|  | ||||
| class UserDeleteStageView(StageView): | ||||
| class UserDeleteStageView(StageView[UserDeleteStage]): | ||||
|     """Finalise unenrollment flow by deleting the user object.""" | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|  | ||||
| @ -39,7 +39,7 @@ class UserLoginChallengeResponse(ChallengeResponse): | ||||
|     remember_me = BooleanField(required=True) | ||||
|  | ||||
|  | ||||
| class UserLoginStageView(ChallengeStageView): | ||||
| class UserLoginStageView(ChallengeStageView[UserLoginStage]): | ||||
|     """Finalise Authentication flow by logging the user in""" | ||||
|  | ||||
|     response_class = UserLoginChallengeResponse | ||||
| @ -49,8 +49,7 @@ class UserLoginStageView(ChallengeStageView): | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Check for remember_me, and do login""" | ||||
|         stage: UserLoginStage = self.executor.current_stage | ||||
|         if timedelta_from_string(stage.remember_me_offset).total_seconds() > 0: | ||||
|         if timedelta_from_string(self.current_stage.remember_me_offset).total_seconds() > 0: | ||||
|             return super().dispatch(request) | ||||
|         return self.do_login(request) | ||||
|  | ||||
| @ -59,9 +58,9 @@ class UserLoginStageView(ChallengeStageView): | ||||
|  | ||||
|     def set_session_duration(self, remember: bool) -> timedelta: | ||||
|         """Update the sessions' expiry""" | ||||
|         delta = timedelta_from_string(self.executor.current_stage.session_duration) | ||||
|         delta = timedelta_from_string(self.current_stage.session_duration) | ||||
|         if remember: | ||||
|             offset = timedelta_from_string(self.executor.current_stage.remember_me_offset) | ||||
|             offset = timedelta_from_string(self.current_stage.remember_me_offset) | ||||
|             delta = delta + offset | ||||
|         if delta.total_seconds() == 0: | ||||
|             self.request.session.set_expiry(0) | ||||
| @ -71,11 +70,9 @@ class UserLoginStageView(ChallengeStageView): | ||||
|  | ||||
|     def set_session_ip(self): | ||||
|         """Set the sessions' last IP and session bindings""" | ||||
|         stage: UserLoginStage = self.executor.current_stage | ||||
|  | ||||
|         self.request.session[SESSION_KEY_LAST_IP] = ClientIPMiddleware.get_client_ip(self.request) | ||||
|         self.request.session[SESSION_KEY_BINDING_NET] = stage.network_binding | ||||
|         self.request.session[SESSION_KEY_BINDING_GEO] = stage.geoip_binding | ||||
|         self.request.session[SESSION_KEY_BINDING_NET] = self.current_stage.network_binding | ||||
|         self.request.session[SESSION_KEY_BINDING_GEO] = self.current_stage.geoip_binding | ||||
|  | ||||
|     def do_login(self, request: HttpRequest, remember: bool = False) -> HttpResponse: | ||||
|         """Attach the currently pending user to the current session""" | ||||
| @ -111,7 +108,7 @@ class UserLoginStageView(ChallengeStageView): | ||||
|         # as sources show their own success messages | ||||
|         if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None): | ||||
|             messages.success(self.request, _("Successfully logged in!")) | ||||
|         if self.executor.current_stage.terminate_other_sessions: | ||||
|         if self.current_stage.terminate_other_sessions: | ||||
|             AuthenticatedSession.objects.filter( | ||||
|                 user=user, | ||||
|             ).exclude(session_key=self.request.session.session_key).delete() | ||||
|  | ||||
| @ -4,9 +4,10 @@ from django.contrib.auth import logout | ||||
| from django.http import HttpRequest, HttpResponse | ||||
|  | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.stages.user_logout.models import UserLogoutStage | ||||
|  | ||||
|  | ||||
| class UserLogoutStageView(StageView): | ||||
| class UserLogoutStageView(StageView[UserLogoutStage]): | ||||
|     """Finalise Authentication flow by logging the user in""" | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|  | ||||
| @ -55,7 +55,7 @@ class UserWriteStageView(StageView): | ||||
|         """Ensure a user exists""" | ||||
|         user_created = False | ||||
|         path = self.executor.plan.context.get( | ||||
|             PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template | ||||
|             PLAN_CONTEXT_USER_PATH, self.current_stage.user_path_template | ||||
|         ) | ||||
|         if path == "": | ||||
|             path = User.default_path() | ||||
| @ -64,11 +64,11 @@ class UserWriteStageView(StageView): | ||||
|             user_type = UserTypes( | ||||
|                 self.executor.plan.context.get( | ||||
|                     PLAN_CONTEXT_USER_TYPE, | ||||
|                     self.executor.current_stage.user_type, | ||||
|                     self.current_stage.user_type, | ||||
|                 ) | ||||
|             ) | ||||
|         except ValueError: | ||||
|             user_type = self.executor.current_stage.user_type | ||||
|             user_type = self.current_stage.user_type | ||||
|         if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT: | ||||
|             user_type = UserTypes.SERVICE_ACCOUNT | ||||
|  | ||||
| @ -76,12 +76,12 @@ class UserWriteStageView(StageView): | ||||
|             self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user) | ||||
|         if ( | ||||
|             PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context | ||||
|             or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE | ||||
|             or self.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE | ||||
|         ): | ||||
|             if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE: | ||||
|             if self.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE: | ||||
|                 return None, False | ||||
|             self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User( | ||||
|                 is_active=not self.executor.current_stage.create_users_as_inactive, | ||||
|                 is_active=not self.current_stage.create_users_as_inactive, | ||||
|                 path=path, | ||||
|                 type=user_type, | ||||
|             ) | ||||
| @ -180,8 +180,8 @@ class UserWriteStageView(StageView): | ||||
|         try: | ||||
|             with transaction.atomic(): | ||||
|                 user.save() | ||||
|                 if self.executor.current_stage.create_users_group: | ||||
|                     user.ak_groups.add(self.executor.current_stage.create_users_group) | ||||
|                 if self.current_stage.create_users_group: | ||||
|                     user.ak_groups.add(self.current_stage.create_users_group) | ||||
|                 if PLAN_CONTEXT_GROUPS in self.executor.plan.context: | ||||
|                     user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS]) | ||||
|         except (IntegrityError, ValueError, TypeError, InternalError) as exc: | ||||
|  | ||||
| @ -10091,6 +10091,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, the captcha element is shown on the identification stage." | ||||
|                 }, | ||||
|                 "case_insensitive_matching": { | ||||
|                     "type": "boolean", | ||||
|                     "title": "Case insensitive matching", | ||||
|  | ||||
							
								
								
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							| @ -40457,11 +40457,10 @@ components: | ||||
|           items: | ||||
|             type: string | ||||
|           nullable: true | ||||
|         password_fields: | ||||
|           type: boolean | ||||
|         allow_show_password: | ||||
|           type: boolean | ||||
|           default: false | ||||
|         password_stage: | ||||
|           $ref: '#/components/schemas/PasswordChallenge' | ||||
|         captcha_stage: | ||||
|           $ref: '#/components/schemas/CaptchaChallenge' | ||||
|         application_pre: | ||||
|           type: string | ||||
|         flow_designation: | ||||
| @ -40482,7 +40481,6 @@ components: | ||||
|           type: boolean | ||||
|       required: | ||||
|       - flow_designation | ||||
|       - password_fields | ||||
|       - primary_action | ||||
|       - show_source_labels | ||||
|       - user_fields | ||||
| @ -40500,6 +40498,8 @@ components: | ||||
|         password: | ||||
|           type: string | ||||
|           nullable: true | ||||
|         captcha: | ||||
|           $ref: '#/components/schemas/CaptchaChallengeResponseRequest' | ||||
|       required: | ||||
|       - uid_field | ||||
|     IdentificationStage: | ||||
| @ -40545,6 +40545,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, the captcha element is shown on the identification | ||||
|             stage. | ||||
|         case_insensitive_matching: | ||||
|           type: boolean | ||||
|           description: When enabled, user fields are matched regardless of their casing. | ||||
| @ -40613,6 +40619,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, the captcha element is shown on the identification | ||||
|             stage. | ||||
|         case_insensitive_matching: | ||||
|           type: boolean | ||||
|           description: When enabled, user fields are matched regardless of their casing. | ||||
| @ -45745,6 +45757,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, the captcha element is shown on the identification | ||||
|             stage. | ||||
|         case_insensitive_matching: | ||||
|           type: boolean | ||||
|           description: When enabled, user fields are matched regardless of their casing. | ||||
|  | ||||
| @ -21,6 +21,7 @@ import { | ||||
|     SourcesApi, | ||||
|     Stage, | ||||
|     StagesApi, | ||||
|     StagesCaptchaListRequest, | ||||
|     StagesPasswordListRequest, | ||||
|     UserFieldsEnum, | ||||
| } from "@goauthentik/api"; | ||||
| @ -160,6 +161,37 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage> | ||||
|                             )} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${msg("Captcha stage")} name="captchaStage"> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<Stage[]> => { | ||||
|                                 const args: StagesCaptchaListRequest = { | ||||
|                                     ordering: "name", | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const stages = await new StagesApi( | ||||
|                                     DEFAULT_CONFIG, | ||||
|                                 ).stagesCaptchaList(args); | ||||
|                                 return stages.results; | ||||
|                             }} | ||||
|                             .groupBy=${(items: Stage[]) => { | ||||
|                                 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?.captchaStage; | ||||
|                             }} | ||||
|                             ?blankable=${true} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         <p class="pf-c-form__helper-text">${msg("TODO.")}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal name="caseInsensitiveMatching"> | ||||
|                         <label class="pf-c-switch"> | ||||
|                             <input | ||||
|  | ||||
| @ -5,7 +5,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, html, nothing } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| @ -33,17 +32,7 @@ export class AccessDeniedStage extends BaseStage< | ||||
|             </header> | ||||
|             <div class="pf-c-login__main-body"> | ||||
|                 <form class="pf-c-form"> | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <ak-empty-state icon="fa-times" header=${msg("Request has been denied.")}> | ||||
|                         ${this.challenge.errorMessage | ||||
|                             ? html` | ||||
|  | ||||
| @ -7,7 +7,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -77,17 +76,7 @@ export class AuthenticatorDuoStage extends BaseStage< | ||||
|                         this.submitForm(e); | ||||
|                     }} | ||||
|                 > | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <img | ||||
|                         src=${this.challenge.activationBarcode} | ||||
|                         alt=${msg("Duo activation QR code")} | ||||
|  | ||||
| @ -41,17 +41,7 @@ export class AuthenticatorSMSStage extends BaseStage< | ||||
|                         this.submitForm(e); | ||||
|                     }} | ||||
|                 > | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <ak-form-element | ||||
|                         label="${msg("Phone number")}" | ||||
|                         required | ||||
|  | ||||
| @ -6,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -66,17 +65,7 @@ export class AuthenticatorStaticStage extends BaseStage< | ||||
|                         this.submitForm(e); | ||||
|                     }} | ||||
|                 > | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <ak-form-element label="" class="pf-c-form__group"> | ||||
|                         <ul> | ||||
|                             ${this.challenge.codes.map((token) => { | ||||
|  | ||||
| @ -9,7 +9,6 @@ import "webcomponent-qr-code"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -60,17 +59,7 @@ export class AuthenticatorTOTPStage extends BaseStage< | ||||
|                         this.submitForm(e); | ||||
|                     }} | ||||
|                 > | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <input type="hidden" name="otp_uri" value=${this.challenge.configUrl} /> | ||||
|                     <ak-form-element> | ||||
|                         <div class="qr-container"> | ||||
|  | ||||
| @ -10,7 +10,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -133,17 +132,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage< | ||||
|             </header> | ||||
|             <div class="pf-c-login__main-body"> | ||||
|                 <form class="pf-c-form"> | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <ak-empty-state | ||||
|                         ?loading="${this.registerRunning}" | ||||
|                         header=${this.registerRunning | ||||
|  | ||||
| @ -7,8 +7,7 @@ import type { TurnstileObject } from "turnstile-types"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; | ||||
| import { customElement, state } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { customElement, property, state } from "lit/decorators.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -45,6 +44,9 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | ||||
|     @state() | ||||
|     scriptElement?: HTMLScriptElement; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     embedded = false; | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.captchaContainer = document.createElement("div"); | ||||
| @ -161,6 +163,9 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (this.embedded) { | ||||
|             return this.renderBody(); | ||||
|         } | ||||
|         if (!this.challenge) { | ||||
|             return html`<ak-empty-state loading> </ak-empty-state>`; | ||||
|         } | ||||
| @ -168,18 +173,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | ||||
|                 <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1> | ||||
|             </header> | ||||
|             <div class="pf-c-login__main-body"> | ||||
|                 <form class="pf-c-form"> | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     ${this.renderBody()} | ||||
|                 </form> | ||||
|             </div> | ||||
|  | ||||
| @ -5,7 +5,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, html, nothing } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -109,17 +108,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe | ||||
|                         }); | ||||
|                     }} | ||||
|                 > | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     ${this.challenge.additionalPermissions.length > 0 | ||||
|                         ? this.renderAdditional() | ||||
|                         : this.renderNoPrevious()} | ||||
|  | ||||
| @ -4,6 +4,7 @@ import "@goauthentik/elements/EmptyState"; | ||||
| import "@goauthentik/elements/forms/FormElement"; | ||||
| import "@goauthentik/flow/components/ak-flow-password-input.js"; | ||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import "@goauthentik/flow/stages/captcha/CaptchaStage"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | ||||
| @ -123,7 +124,7 @@ export class IdentificationStage extends BaseStage< | ||||
|             this.form.appendChild(username); | ||||
|         } | ||||
|         // Only add the password field when we don't already show a password field | ||||
|         if (!compatMode && !this.challenge.passwordFields) { | ||||
|         if (!compatMode && !this.challenge.passwordStage) { | ||||
|             const password = document.createElement("input"); | ||||
|             password.setAttribute("type", "password"); | ||||
|             password.setAttribute("name", "password"); | ||||
| @ -260,7 +261,7 @@ export class IdentificationStage extends BaseStage< | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element> | ||||
|             ${this.challenge.passwordFields | ||||
|             ${this.challenge.passwordStage | ||||
|                 ? html` | ||||
|                       <ak-flow-input-password | ||||
|                           label=${msg("Password")} | ||||
| @ -268,12 +269,20 @@ export class IdentificationStage extends BaseStage< | ||||
|                           required | ||||
|                           class="pf-c-form__group" | ||||
|                           .errors=${(this.challenge?.responseErrors || {})["password"]} | ||||
|                           ?allow-show-password=${this.challenge.allowShowPassword} | ||||
|                           ?allow-show-password=${this.challenge.passwordStage.allowShowPassword} | ||||
|                           prefill=${PasswordManagerPrefill["password"] ?? ""} | ||||
|                       ></ak-flow-input-password> | ||||
|                   ` | ||||
|                 : nothing} | ||||
|             ${this.renderNonFieldErrors()} | ||||
|             ${this.challenge.captchaStage | ||||
|                 ? html` | ||||
|                       <ak-stage-captcha | ||||
|                           .challenge=${this.challenge.captchaStage} | ||||
|                           embedded | ||||
|                       ></ak-stage-captcha> | ||||
|                   ` | ||||
|                 : nothing} | ||||
|             <div class="pf-c-form__group pf-m-action"> | ||||
|                 <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> | ||||
|                     ${this.challenge.primaryAction} | ||||
| @ -284,6 +293,13 @@ export class IdentificationStage extends BaseStage< | ||||
|                 : nothing}`; | ||||
|     } | ||||
|  | ||||
|     submitForm( | ||||
|         e: Event, | ||||
|         defaults?: IdentificationChallengeResponseRequest | undefined, | ||||
|     ): Promise<boolean> { | ||||
|         return super.submitForm(e, defaults); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.challenge) { | ||||
|             return html`<ak-empty-state loading> </ak-empty-state>`; | ||||
|  | ||||
| @ -8,7 +8,6 @@ import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/ | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -45,17 +44,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng | ||||
|                         this.submitForm(e); | ||||
|                     }} | ||||
|                 > | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <input | ||||
|                         name="username" | ||||
|                         autocomplete="username" | ||||
|  | ||||
| @ -6,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| @ -36,17 +35,7 @@ export class PasswordStage extends BaseStage< | ||||
|             </header> | ||||
|             <div class="pf-c-login__main-body"> | ||||
|                 <form class="pf-c-form"> | ||||
|                     <ak-form-static | ||||
|                         class="pf-c-form__group" | ||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" | ||||
|                         user=${this.challenge.pendingUser} | ||||
|                     > | ||||
|                         <div slot="link"> | ||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||
|                                 >${msg("Not you?")}</a | ||||
|                             > | ||||
|                         </div> | ||||
|                     </ak-form-static> | ||||
|                     ${this.renderUserInfo()} | ||||
|                     <div class="pf-c-form__group"> | ||||
|                         <h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl"> | ||||
|                             ${msg("Stay signed in?")} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	