diff --git a/authentik/stages/captcha/api.py b/authentik/stages/captcha/api.py
index 8d833197cd..bd83c59f74 100644
--- a/authentik/stages/captcha/api.py
+++ b/authentik/stages/captcha/api.py
@@ -12,7 +12,15 @@ class CaptchaStageSerializer(StageSerializer):
class Meta:
model = CaptchaStage
- fields = StageSerializer.Meta.fields + ["public_key", "private_key", "js_url", "api_url"]
+ fields = StageSerializer.Meta.fields + [
+ "public_key",
+ "private_key",
+ "js_url",
+ "api_url",
+ "score_min_threshold",
+ "score_max_threshold",
+ "error_on_invalid_score",
+ ]
extra_kwargs = {"private_key": {"write_only": True}}
diff --git a/authentik/stages/captcha/migrations/0003_captchastage_error_on_invalid_score_and_more.py b/authentik/stages/captcha/migrations/0003_captchastage_error_on_invalid_score_and_more.py
new file mode 100644
index 0000000000..c9a673abe9
--- /dev/null
+++ b/authentik/stages/captcha/migrations/0003_captchastage_error_on_invalid_score_and_more.py
@@ -0,0 +1,31 @@
+# Generated by Django 5.0.6 on 2024-06-03 15:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_captcha", "0002_captchastage_api_url_captchastage_js_url_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="captchastage",
+ name="error_on_invalid_score",
+ field=models.BooleanField(
+ default=True,
+ help_text="When enabled and the received captcha score is outside of the given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions",
+ ),
+ ),
+ migrations.AddField(
+ model_name="captchastage",
+ name="score_max_threshold",
+ field=models.FloatField(default=1.0),
+ ),
+ migrations.AddField(
+ model_name="captchastage",
+ name="score_min_threshold",
+ field=models.FloatField(default=0.5),
+ ),
+ ]
diff --git a/authentik/stages/captcha/models.py b/authentik/stages/captcha/models.py
index e0e126b056..02f20882f1 100644
--- a/authentik/stages/captcha/models.py
+++ b/authentik/stages/captcha/models.py
@@ -14,6 +14,18 @@ class CaptchaStage(Stage):
public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider."))
private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider."))
+ score_min_threshold = models.FloatField(default=0.5) # Default values for reCaptcha
+ score_max_threshold = models.FloatField(default=1.0) # Default values for reCaptcha
+
+ error_on_invalid_score = models.BooleanField(
+ default=True,
+ help_text=_(
+ "When enabled and the received captcha score is outside of the given threshold, "
+ "the stage will show an error message. When not enabled, the flow will continue, "
+ "but the data from the captcha will be available in the context for policy decisions"
+ ),
+ )
+
js_url = models.TextField(default="https://www.recaptcha.net/recaptcha/api.js")
api_url = models.TextField(default="https://www.recaptcha.net/recaptcha/api/siteverify")
diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py
index e0a8c20d6a..e770119c5a 100644
--- a/authentik/stages/captcha/stage.py
+++ b/authentik/stages/captcha/stage.py
@@ -1,6 +1,7 @@
"""authentik captcha stage"""
from django.http.response import HttpResponse
+from django.utils.translation import gettext_lazy as _
from requests import RequestException
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
@@ -16,6 +17,8 @@ from authentik.lib.utils.http import get_http_session
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.captcha.models import CaptchaStage
+PLAN_CONTEXT_CAPTCHA = "captcha"
+
class CaptchaChallenge(WithUserInfoChallenge):
"""Site public key"""
@@ -48,11 +51,24 @@ class CaptchaChallengeResponse(ChallengeResponse):
)
response.raise_for_status()
data = response.json()
- if not data.get("success", False):
- raise ValidationError(f"Failed to validate token: {data.get('error-codes', '')}")
- except RequestException as exc:
- raise ValidationError("Failed to validate token") from exc
- return token
+ if stage.error_on_invalid_score:
+ if not data.get("success", False):
+ raise ValidationError(
+ _(
+ "Failed to validate token: {error}".format(
+ error=data.get("error-codes", _("Unknown error"))
+ )
+ )
+ )
+ if "score" in data:
+ score = float(data.get("score"))
+ if stage.score_max_threshold > -1 and score > stage.score_max_threshold:
+ raise ValidationError(_("Invalid captcha response"))
+ if stage.score_min_threshold > -1 and score < stage.score_min_threshold:
+ raise ValidationError(_("Invalid captcha response"))
+ except (RequestException, TypeError) as exc:
+ raise ValidationError(_("Failed to validate token")) from exc
+ return data
class CaptchaStageView(ChallengeStageView):
@@ -69,5 +85,10 @@ class CaptchaStageView(ChallengeStageView):
}
)
- def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
+ def challenge_valid(self, response: CaptchaChallengeResponse) -> HttpResponse:
+ response = response.validated_data["token"]
+ self.executor.plan.context[PLAN_CONTEXT_CAPTCHA] = {
+ "response": response,
+ "stage": self.executor.current_stage,
+ }
return self.executor.stage_ok()
diff --git a/authentik/stages/captcha/tests.py b/authentik/stages/captcha/tests.py
index d5d63d8c20..c03384803b 100644
--- a/authentik/stages/captcha/tests.py
+++ b/authentik/stages/captcha/tests.py
@@ -1,6 +1,7 @@
"""captcha tests"""
from django.urls import reverse
+from requests_mock import Mocker
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
@@ -30,8 +31,89 @@ class TestCaptchaStage(FlowTestCase):
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
- def test_valid(self):
+ @Mocker()
+ def test_valid(self, mock: Mocker):
"""Test valid captcha"""
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": True,
+ "score": 0.5,
+ },
+ )
+ plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
+ session = self.client.session
+ session[SESSION_KEY_PLAN] = plan
+ session.save()
+ response = self.client.post(
+ reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
+ {"token": "PASSED"},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
+
+ @Mocker()
+ def test_invalid_score_high(self, mock: Mocker):
+ """Test invalid captcha (score too high)"""
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": True,
+ "score": 99,
+ },
+ )
+ plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
+ session = self.client.session
+ session[SESSION_KEY_PLAN] = plan
+ session.save()
+ response = self.client.post(
+ reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
+ {"token": "PASSED"},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertStageResponse(
+ response,
+ component="ak-stage-captcha",
+ response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
+ )
+
+ @Mocker()
+ def test_invalid_score_low(self, mock: Mocker):
+ """Test invalid captcha (score too low)"""
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": True,
+ "score": -3,
+ },
+ )
+ plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
+ session = self.client.session
+ session[SESSION_KEY_PLAN] = plan
+ session.save()
+ response = self.client.post(
+ reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
+ {"token": "PASSED"},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertStageResponse(
+ response,
+ component="ak-stage-captcha",
+ response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
+ )
+
+ @Mocker()
+ def test_invalid_score_low_continue(self, mock: Mocker):
+ """Test invalid captcha (score too low, but continue)"""
+ self.stage.error_on_invalid_score = False
+ self.stage.save()
+ mock.post(
+ "https://www.recaptcha.net/recaptcha/api/siteverify",
+ json={
+ "success": True,
+ "score": -3,
+ },
+ )
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 32d40e4d4e..d32ba34e50 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -6164,6 +6164,19 @@
"type": "string",
"minLength": 1,
"title": "Api url"
+ },
+ "score_min_threshold": {
+ "type": "number",
+ "title": "Score min threshold"
+ },
+ "score_max_threshold": {
+ "type": "number",
+ "title": "Score max threshold"
+ },
+ "error_on_invalid_score": {
+ "type": "boolean",
+ "title": "Error on invalid score",
+ "description": "When enabled and the received captcha score is outside of the given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions"
}
},
"required": []
diff --git a/poetry.lock b/poetry.lock
index 9430fc338a..06c8104999 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "aiohttp"
@@ -3376,19 +3376,19 @@ files = [
[[package]]
name = "pydantic"
-version = "2.7.2"
+version = "2.7.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.7.2-py3-none-any.whl", hash = "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7"},
- {file = "pydantic-2.7.2.tar.gz", hash = "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7"},
+ {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"},
+ {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
-pydantic-core = "2.18.3"
+pydantic-core = "2.18.4"
typing-extensions = ">=4.6.1"
[package.extras]
@@ -3396,90 +3396,90 @@ email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
-version = "2.18.3"
+version = "2.18.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.18.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c"},
- {file = "pydantic_core-2.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7"},
- {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c"},
- {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2"},
- {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34"},
- {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a"},
- {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b"},
- {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6"},
- {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426"},
- {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812"},
- {file = "pydantic_core-2.18.3-cp310-none-win32.whl", hash = "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779"},
- {file = "pydantic_core-2.18.3-cp310-none-win_amd64.whl", hash = "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0"},
- {file = "pydantic_core-2.18.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab"},
- {file = "pydantic_core-2.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2"},
- {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb"},
- {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231"},
- {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be"},
- {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd"},
- {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106"},
- {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4"},
- {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe"},
- {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d"},
- {file = "pydantic_core-2.18.3-cp311-none-win32.whl", hash = "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7"},
- {file = "pydantic_core-2.18.3-cp311-none-win_amd64.whl", hash = "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7"},
- {file = "pydantic_core-2.18.3-cp311-none-win_arm64.whl", hash = "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4"},
- {file = "pydantic_core-2.18.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f"},
- {file = "pydantic_core-2.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9"},
- {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c"},
- {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7"},
- {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048"},
- {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326"},
- {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4"},
- {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022"},
- {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd"},
- {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be"},
- {file = "pydantic_core-2.18.3-cp312-none-win32.whl", hash = "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5"},
- {file = "pydantic_core-2.18.3-cp312-none-win_amd64.whl", hash = "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6"},
- {file = "pydantic_core-2.18.3-cp312-none-win_arm64.whl", hash = "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417"},
- {file = "pydantic_core-2.18.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522"},
- {file = "pydantic_core-2.18.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702"},
- {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2"},
- {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5"},
- {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361"},
- {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60"},
- {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044"},
- {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc"},
- {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4"},
- {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0"},
- {file = "pydantic_core-2.18.3-cp38-none-win32.whl", hash = "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558"},
- {file = "pydantic_core-2.18.3-cp38-none-win_amd64.whl", hash = "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc"},
- {file = "pydantic_core-2.18.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083"},
- {file = "pydantic_core-2.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef"},
- {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1"},
- {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442"},
- {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8"},
- {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9"},
- {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943"},
- {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06"},
- {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6"},
- {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af"},
- {file = "pydantic_core-2.18.3-cp39-none-win32.whl", hash = "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78"},
- {file = "pydantic_core-2.18.3-cp39-none-win_amd64.whl", hash = "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb"},
- {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1"},
- {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a"},
- {file = "pydantic_core-2.18.3.tar.gz", hash = "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"},
+ {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"},
+ {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"},
+ {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"},
+ {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"},
+ {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"},
+ {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"},
+ {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"},
+ {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"},
+ {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"},
+ {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"},
+ {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"},
+ {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"},
+ {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"},
+ {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"},
+ {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"},
+ {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"},
+ {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"},
+ {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"},
+ {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"},
+ {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"},
]
[package.dependencies]
diff --git a/schema.yml b/schema.yml
index 0fc89940d8..1f47eccfcf 100644
--- a/schema.yml
+++ b/schema.yml
@@ -34067,6 +34067,18 @@ components:
type: string
api_url:
type: string
+ score_min_threshold:
+ type: number
+ format: double
+ score_max_threshold:
+ type: number
+ format: double
+ error_on_invalid_score:
+ type: boolean
+ description: When enabled and the received captcha score is outside of the
+ given threshold, the stage will show an error message. When not enabled,
+ the flow will continue, but the data from the captcha will be available
+ in the context for policy decisions
required:
- component
- meta_model_name
@@ -34101,6 +34113,18 @@ components:
api_url:
type: string
minLength: 1
+ score_min_threshold:
+ type: number
+ format: double
+ score_max_threshold:
+ type: number
+ format: double
+ error_on_invalid_score:
+ type: boolean
+ description: When enabled and the received captcha score is outside of the
+ given threshold, the stage will show an error message. When not enabled,
+ the flow will continue, but the data from the captcha will be available
+ in the context for policy decisions
required:
- name
- private_key
@@ -41182,6 +41206,18 @@ components:
api_url:
type: string
minLength: 1
+ score_min_threshold:
+ type: number
+ format: double
+ score_max_threshold:
+ type: number
+ format: double
+ error_on_invalid_score:
+ type: boolean
+ description: When enabled and the received captcha score is outside of the
+ given threshold, the stage will show an error message. When not enabled,
+ the flow will continue, but the data from the captcha will be available
+ in the context for policy decisions
PatchedCertificateKeyPairRequest:
type: object
description: CertificateKeyPair Serializer
diff --git a/tests/wdio/package-lock.json b/tests/wdio/package-lock.json
index f68d58529d..0e90610654 100644
--- a/tests/wdio/package-lock.json
+++ b/tests/wdio/package-lock.json
@@ -12,10 +12,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
- "@wdio/cli": "^8.38.1",
- "@wdio/local-runner": "^8.38.0",
- "@wdio/mocha-framework": "^8.38.0",
- "@wdio/spec-reporter": "^8.38.0",
+ "@wdio/cli": "^8.38.2",
+ "@wdio/local-runner": "^8.38.2",
+ "@wdio/mocha-framework": "^8.38.2",
+ "@wdio/spec-reporter": "^8.38.2",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.1",
@@ -1189,19 +1189,19 @@
}
},
"node_modules/@wdio/cli": {
- "version": "8.38.1",
- "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.38.1.tgz",
- "integrity": "sha512-xif0RvJX+saXVdzrhlEafbEdptNyeyqm0pyilT4H7OI7OMa+23rWB67r2SPoIZbBrVeFv1iqtUx9hR73RRio2A==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.38.2.tgz",
+ "integrity": "sha512-p9y6jxmpmw53OoB9v/uTLwMetmz7Q0K7NewdVONgmeTY/ERpkU15qL3fMw1rXb+E+vrV8dlce4srnXroec6SFA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.1",
"@vitest/snapshot": "^1.2.1",
- "@wdio/config": "8.38.0",
- "@wdio/globals": "8.38.0",
+ "@wdio/config": "8.38.2",
+ "@wdio/globals": "8.38.2",
"@wdio/logger": "8.38.0",
"@wdio/protocols": "8.38.0",
- "@wdio/types": "8.37.0",
- "@wdio/utils": "8.38.0",
+ "@wdio/types": "8.38.2",
+ "@wdio/utils": "8.38.2",
"async-exit-hook": "^2.0.1",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
@@ -1216,7 +1216,7 @@
"lodash.union": "^4.6.0",
"read-pkg-up": "10.0.0",
"recursive-readdir": "^2.2.3",
- "webdriverio": "8.38.0",
+ "webdriverio": "8.38.2",
"yargs": "^17.7.2"
},
"bin": {
@@ -1239,14 +1239,14 @@
}
},
"node_modules/@wdio/config": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.38.0.tgz",
- "integrity": "sha512-9eMmHYkXw/0htj8Nok2vBa8Q+IS/wj7HXbLczKb5rEmDb57SW5iPMpZutFywGki1D/GIDLvejWaZvlxmS/yfBA==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.38.2.tgz",
+ "integrity": "sha512-xlnapTr1vOA0h5HsHTIqj47729FbG3WjxmgHweDEQvcT4C1g9l+WKf+N3FM7DNNoIsAqxKi6rOHG02rJADQJtw==",
"dev": true,
"dependencies": {
"@wdio/logger": "8.38.0",
- "@wdio/types": "8.37.0",
- "@wdio/utils": "8.38.0",
+ "@wdio/types": "8.38.2",
+ "@wdio/utils": "8.38.2",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.0.0",
"glob": "^10.2.2",
@@ -1257,29 +1257,29 @@
}
},
"node_modules/@wdio/globals": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.38.0.tgz",
- "integrity": "sha512-3Lo7R305gwJrloUolrgSRaMbIFKwnj/SPoYUt94dsTF1oM3TpyWpomcVokElNQiOFx9WYjMmBla2M+YcnoGmgw==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.38.2.tgz",
+ "integrity": "sha512-iIrUF1EODfHLh3V/CSNCqbNNxUTe3ND+c86zDjzJcPFjawLX1plvAApsU/eDmtsFShcOS2KHbfSjiydFoqQG1Q==",
"dev": true,
"engines": {
"node": "^16.13 || >=18"
},
"optionalDependencies": {
"expect-webdriverio": "^4.11.2",
- "webdriverio": "8.38.0"
+ "webdriverio": "8.38.2"
}
},
"node_modules/@wdio/local-runner": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.38.0.tgz",
- "integrity": "sha512-++eVI+EQapBMRxtLSbeooK02uELAhIXNLzgBlC03s23OTZUUOxzl4WxFuGqaG8gpNHer5Bjg+uAy0rIbSMnklA==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.38.2.tgz",
+ "integrity": "sha512-syW+R5VUHJ3GBkQGFcNYe6MYwWRgklc9W7A83xQDTvKWFNHCetLvc8AtKZ54vs8MItBejjU+Oh94ZNbNX1pBcg==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.38.0",
"@wdio/repl": "8.24.12",
- "@wdio/runner": "8.38.0",
- "@wdio/types": "8.37.0",
+ "@wdio/runner": "8.38.2",
+ "@wdio/types": "8.38.2",
"async-exit-hook": "^2.0.1",
"split2": "^4.1.0",
"stream-buffers": "^3.0.2"
@@ -1316,16 +1316,16 @@
}
},
"node_modules/@wdio/mocha-framework": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.38.0.tgz",
- "integrity": "sha512-LWwKslKIxqhQNskdffcS6QbxN45PzJN21b5zByELbEYd9PIGNbuXXqueVE09RwEAzBHtL86ZhSpFclAB+ulsBw==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.38.2.tgz",
+ "integrity": "sha512-qJmRL5E6/ypjCUACH4hvCAAmTdU4YUrUlp9o/IKvTIAHMnZPE0/HgUFixCeu8Mop+rdzTPVBrbqxpRDdSnraYA==",
"dev": true,
"dependencies": {
"@types/mocha": "^10.0.0",
"@types/node": "^20.1.0",
"@wdio/logger": "8.38.0",
- "@wdio/types": "8.37.0",
- "@wdio/utils": "8.38.0",
+ "@wdio/types": "8.38.2",
+ "@wdio/utils": "8.38.2",
"mocha": "^10.0.0"
},
"engines": {
@@ -1351,14 +1351,14 @@
}
},
"node_modules/@wdio/reporter": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.38.0.tgz",
- "integrity": "sha512-7eyBM06j0PAu2g14wOwJbSmUOaT8q1pmMukzeRl1YDoW2sLpQG0zMZNzQNsQyi7+1KgltTJJnvHCec5MBZrjpw==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.38.2.tgz",
+ "integrity": "sha512-R78UdAtAnkaV22NYlCCcbPPhmYweiDURiw64LYhlVIQrKNuXUQcafR2kRlWKy31rZc9thSLs5LzrZDReENUlFQ==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.38.0",
- "@wdio/types": "8.37.0",
+ "@wdio/types": "8.38.2",
"diff": "^5.0.0",
"object-inspect": "^1.12.0"
},
@@ -1367,35 +1367,35 @@
}
},
"node_modules/@wdio/runner": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.38.0.tgz",
- "integrity": "sha512-pmoXwRMUxWXhqce64Y3gqoMqJn0YknJ1U9aHOMxp6uB3iHfzoPA+7Mf5ziSc0uDBcEyjXtau9qMfu7iBHUz62Q==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.38.2.tgz",
+ "integrity": "sha512-5lPnKSX2BBLI2AbYW+hoGPiEUAJXj8F8I6NC2LaBVzf1CLN+w2HWZ7lUiqS14XT0b5/hlSUX6+JYwUXlDbpuuw==",
"dev": true,
"dependencies": {
"@types/node": "^20.11.28",
- "@wdio/config": "8.38.0",
- "@wdio/globals": "8.38.0",
+ "@wdio/config": "8.38.2",
+ "@wdio/globals": "8.38.2",
"@wdio/logger": "8.38.0",
- "@wdio/types": "8.37.0",
- "@wdio/utils": "8.38.0",
+ "@wdio/types": "8.38.2",
+ "@wdio/utils": "8.38.2",
"deepmerge-ts": "^5.1.0",
"expect-webdriverio": "^4.12.0",
"gaze": "^1.1.3",
- "webdriver": "8.38.0",
- "webdriverio": "8.38.0"
+ "webdriver": "8.38.2",
+ "webdriverio": "8.38.2"
},
"engines": {
"node": "^16.13 || >=18"
}
},
"node_modules/@wdio/spec-reporter": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.38.0.tgz",
- "integrity": "sha512-Cuk/mqsBa+YgAL3OCtsYLUxRkGSYeaXM3LSE5muzY1mSqQlK3a/5GdAXlaOosuhYpUgI0A9xTsMiq7PQd75qHA==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.38.2.tgz",
+ "integrity": "sha512-Dntk+lmrp+0I3HRRWkkXED+smshvgsW5cdLKwJhEJ1liI48MdBpdNGf9IHTVckE6nfxcWDyFI4icD9qYv/5bFA==",
"dev": true,
"dependencies": {
- "@wdio/reporter": "8.38.0",
- "@wdio/types": "8.37.0",
+ "@wdio/reporter": "8.38.2",
+ "@wdio/types": "8.38.2",
"chalk": "^5.1.2",
"easy-table": "^1.2.0",
"pretty-ms": "^7.0.0"
@@ -1417,9 +1417,9 @@
}
},
"node_modules/@wdio/types": {
- "version": "8.37.0",
- "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.37.0.tgz",
- "integrity": "sha512-36kmSlZcVhsMlbhaSCQUfL51iG81FlbzW4Dfkz4903cDkxmh64bgxydZbRB5aPLnJzzR7tI3chIME8zSVZFR8w==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.38.2.tgz",
+ "integrity": "sha512-+wj1c1OSLdnN4WO5b44Ih4263dTl/eSwMGSI4/pCgIyXIuYQH38JQ+6WRa+c8vJEskUzboq2cSgEQumVZ39ozQ==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0"
@@ -1429,14 +1429,14 @@
}
},
"node_modules/@wdio/utils": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.38.0.tgz",
- "integrity": "sha512-ios7MpyJk4kGW9ZOYxbPpdwVZBI7SzccIgiirqSf8rvJi62VpDA2nfa7i7BY1rs9p7lnenF8phwnuVFGMCoL0w==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.38.2.tgz",
+ "integrity": "sha512-y5AnBwsGcu/XuCBGCgKmlvKdwEIFyzLA+Cr+denySxY3jbWDONtPUcGaVdFALwsIa5jcIjcATqGmZcCPGnkd7g==",
"dev": true,
"dependencies": {
"@puppeteer/browsers": "^1.6.0",
"@wdio/logger": "8.38.0",
- "@wdio/types": "8.37.0",
+ "@wdio/types": "8.38.2",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.1.0",
"edgedriver": "^5.5.0",
@@ -8969,18 +8969,18 @@
}
},
"node_modules/webdriver": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.38.0.tgz",
- "integrity": "sha512-BT3sd667AqcZ1lzaOd7lphjnc7MNG3WgduAq4vUUDYlJAbs6SyjYd0EZPIPv9KUUYr0BoSJTDa0Xuyl/6PPW0Q==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.38.2.tgz",
+ "integrity": "sha512-NGfjW0BDYwFgOIzeojOcWGn3tYloQdvHr+Y2xKKYVqa9Rs0x1mzlTjU1kWtC4DaV8DltskwaPa7o+s8hTNpuyA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@types/ws": "^8.5.3",
- "@wdio/config": "8.38.0",
+ "@wdio/config": "8.38.2",
"@wdio/logger": "8.38.0",
"@wdio/protocols": "8.38.0",
- "@wdio/types": "8.37.0",
- "@wdio/utils": "8.38.0",
+ "@wdio/types": "8.38.2",
+ "@wdio/utils": "8.38.2",
"deepmerge-ts": "^5.1.0",
"got": "^12.6.1",
"ky": "^0.33.0",
@@ -8991,18 +8991,18 @@
}
},
"node_modules/webdriverio": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.38.0.tgz",
- "integrity": "sha512-PxLtJPK8Aa+f/G/P8YGUwGH80uHowMA8cDHshhog6sKbp1BXEVB8x6PyC1AIswRlTWVijoes7cD0cUoVs6C87A==",
+ "version": "8.38.2",
+ "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.38.2.tgz",
+ "integrity": "sha512-r09y5UfivyYh5JOzT2SpJJ1zDmQl/R4OTH12opUqkjvp21BibCQm/uu1mrxGy4lzSHljrvqSVrrcGI+6UA1O8w==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
- "@wdio/config": "8.38.0",
+ "@wdio/config": "8.38.2",
"@wdio/logger": "8.38.0",
"@wdio/protocols": "8.38.0",
"@wdio/repl": "8.24.12",
- "@wdio/types": "8.37.0",
- "@wdio/utils": "8.38.0",
+ "@wdio/types": "8.38.2",
+ "@wdio/utils": "8.38.2",
"archiver": "^7.0.0",
"aria-query": "^5.0.0",
"css-shorthand-properties": "^1.1.1",
@@ -9020,7 +9020,7 @@
"resq": "^1.9.1",
"rgb2hex": "0.2.5",
"serialize-error": "^11.0.1",
- "webdriver": "8.38.0"
+ "webdriver": "8.38.2"
},
"engines": {
"node": "^16.13 || >=18"
diff --git a/tests/wdio/package.json b/tests/wdio/package.json
index 6e89fb4c4c..7b03b56dcf 100644
--- a/tests/wdio/package.json
+++ b/tests/wdio/package.json
@@ -6,10 +6,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
- "@wdio/cli": "^8.38.1",
- "@wdio/local-runner": "^8.38.0",
- "@wdio/mocha-framework": "^8.38.0",
- "@wdio/spec-reporter": "^8.38.0",
+ "@wdio/cli": "^8.38.2",
+ "@wdio/local-runner": "^8.38.2",
+ "@wdio/mocha-framework": "^8.38.2",
+ "@wdio/spec-reporter": "^8.38.2",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.1",
diff --git a/web/package-lock.json b/web/package-lock.json
index 0cfffe19bf..4d3830ac55 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -17,7 +17,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
- "@goauthentik/api": "^2024.4.2-1717033226",
+ "@goauthentik/api": "^2024.4.2-1717493155",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.1",
"@lit/localize": "^0.12.1",
@@ -2968,9 +2968,9 @@
}
},
"node_modules/@goauthentik/api": {
- "version": "2024.4.2-1717033226",
- "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.4.2-1717033226.tgz",
- "integrity": "sha512-XMAY7k+cOGZF4hZMG01VtYXqnSH29wDT7qQ8R1Wg7WfDv2zo/LmwtDY4f/HGjbDo/6mxTo9WGl7wXTSCwgJqWQ=="
+ "version": "2024.4.2-1717493155",
+ "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.4.2-1717493155.tgz",
+ "integrity": "sha512-+3jX2BUJLIlYVmUPtz4gQxRNd+k+IaGlc87t0JJPF3k9Zb4ZWFSp+garoHUkMtwz+6obCqbVaH/MH3Lbu6J4zw=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
diff --git a/web/package.json b/web/package.json
index 92956ec911..079eec86b1 100644
--- a/web/package.json
+++ b/web/package.json
@@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
- "@goauthentik/api": "^2024.4.2-1717033226",
+ "@goauthentik/api": "^2024.4.2-1717493155",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.1",
"@lit/localize": "^0.12.1",
diff --git a/web/src/admin/stages/captcha/CaptchaStageForm.ts b/web/src/admin/stages/captcha/CaptchaStageForm.ts
index cacdee5dea..27a9470989 100644
--- a/web/src/admin/stages/captcha/CaptchaStageForm.ts
+++ b/web/src/admin/stages/captcha/CaptchaStageForm.ts
@@ -1,5 +1,7 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import { first } from "@goauthentik/common/utils";
+import "@goauthentik/components/ak-number-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@@ -78,6 +80,40 @@ export class CaptchaStageForm extends BaseStageForm
+ ${msg( + "When enabled and the resultant score is outside the threshold, the user will not be able to continue. When disabled, the user will be able to continue and the score can be used in policies to customize further stages.", + )} +
+