diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py
index 9ad97320e8..b3ceb4cf8e 100644
--- a/authentik/stages/identification/api.py
+++ b/authentik/stages/identification/api.py
@@ -27,6 +27,7 @@ class IdentificationStageSerializer(StageSerializer):
         fields = StageSerializer.Meta.fields + [
             "user_fields",
             "password_stage",
+            "captcha_stage",
             "case_insensitive_matching",
             "show_matched_user",
             "enrollment_flow",
diff --git a/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py
new file mode 100644
index 0000000000..ca8a02ffd8
--- /dev/null
+++ b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.0.8 on 2024-08-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",
+            ),
+        ),
+    ]
diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py
index 27cfcb92f1..fd2faa0e2a 100644
--- a/authentik/stages/identification/models.py
+++ b/authentik/stages/identification/models.py
@@ -8,6 +8,7 @@ from rest_framework.serializers import BaseSerializer
 
 from authentik.core.models import Source
 from authentik.flows.models import Flow, Stage
+from authentik.stages.captcha.models import CaptchaStage
 from authentik.stages.password.models import PasswordStage
 
 
@@ -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,
diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py
index 3cdccf3c00..1591949741 100644
--- a/authentik/stages/identification/stage.py
+++ b/authentik/stages/identification/stage.py
@@ -30,6 +30,11 @@ 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
@@ -64,6 +69,7 @@ class IdentificationChallenge(Challenge):
 
     user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
     password_fields = BooleanField()
+    captcha_stage = CaptchaChallenge(required=False)
     allow_show_password = BooleanField(default=False)
     application_pre = CharField(required=False)
     flow_designation = ChoiceField(FlowDesignation.choices)
@@ -84,6 +90,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,30 +135,32 @@ 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
 
 
@@ -230,6 +239,12 @@ class IdentificationStageView(ChallengeStageView):
                 query=get_qs,
                 kwargs={"flow_slug": current_stage.passwordless_flow.slug},
             )
+        if current_stage.captcha_stage:
+            captcha = CaptchaStageView(self.executor)
+            captcha.stage = 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 = []
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 890022991c..8fb64613d8 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -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",
diff --git a/schema.yml b/schema.yml
index 6c21371d7b..7918d71ada 100644
--- a/schema.yml
+++ b/schema.yml
@@ -40459,6 +40459,8 @@ components:
           nullable: true
         password_fields:
           type: boolean
+        captcha_stage:
+          $ref: '#/components/schemas/CaptchaChallenge'
         allow_show_password:
           type: boolean
           default: false
@@ -40500,6 +40502,8 @@ components:
         password:
           type: string
           nullable: true
+        captcha:
+          $ref: '#/components/schemas/CaptchaChallengeResponseRequest'
       required:
       - uid_field
     IdentificationStage:
@@ -40545,6 +40549,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 +40623,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 +45761,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.
diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts
index 8b4c553c96..18fc1ae1a5 100644
--- a/web/src/admin/stages/identification/IdentificationStageForm.ts
+++ b/web/src/admin/stages/identification/IdentificationStageForm.ts
@@ -21,6 +21,7 @@ import {
     SourcesApi,
     Stage,
     StagesApi,
+    StagesCaptchaListRequest,
     StagesPasswordListRequest,
     UserFieldsEnum,
 } from "@goauthentik/api";
@@ -160,6 +161,41 @@ export class IdentificationStageForm extends BaseStageForm
+ ${msg( + "TODO.", + )} +
+