diff --git a/authentik/brands/middleware.py b/authentik/brands/middleware.py
index 71650cc621cf..52af854e332f 100644
--- a/authentik/brands/middleware.py
+++ b/authentik/brands/middleware.py
@@ -4,7 +4,7 @@
from django.http.request import HttpRequest
from django.http.response import HttpResponse
-from django.utils.translation import activate
+from django.utils.translation import override
from authentik.brands.utils import get_brand_for_request
@@ -18,10 +18,12 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
+ locale_to_set = None
if not hasattr(request, "brand"):
brand = get_brand_for_request(request)
request.brand = brand
locale = brand.default_locale
if locale != "":
- activate(locale)
- return self.get_response(request)
+ locale_to_set = locale
+ with override(locale_to_set):
+ return self.get_response(request)
diff --git a/authentik/core/middleware.py b/authentik/core/middleware.py
index f59b9aa6b785..1d20455a1ba9 100644
--- a/authentik/core/middleware.py
+++ b/authentik/core/middleware.py
@@ -5,7 +5,7 @@
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
-from django.utils.translation import activate
+from django.utils.translation import override
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
@@ -31,17 +31,19 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
def __call__(self, request: HttpRequest) -> HttpResponse:
# No permission checks are done here, they need to be checked before
# SESSION_KEY_IMPERSONATE_USER is set.
+ locale_to_set = None
if request.user.is_authenticated:
locale = request.user.locale(request)
if locale != "":
- activate(locale)
+ locale_to_set = locale
if SESSION_KEY_IMPERSONATE_USER in request.session:
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
# Ensure that the user is active, otherwise nothing will work
request.user.is_active = True
- return self.get_response(request)
+ with override(locale_to_set):
+ return self.get_response(request)
class RequestIDMiddleware:
diff --git a/authentik/core/tests/test_applications_api.py b/authentik/core/tests/test_applications_api.py
index 1244776b2af8..192adc458b90 100644
--- a/authentik/core/tests/test_applications_api.py
+++ b/authentik/core/tests/test_applications_api.py
@@ -12,7 +12,7 @@
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
-from authentik.providers.oauth2.models import OAuth2Provider
+from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.saml.models import SAMLProvider
@@ -24,7 +24,7 @@ def setUp(self) -> None:
self.user = create_test_admin_user()
self.provider = OAuth2Provider.objects.create(
name="test",
- redirect_uris="http://some-other-domain",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
authorization_flow=create_test_flow(),
)
self.allowed: Application = Application.objects.create(
diff --git a/authentik/core/tests/test_transactional_applications_api.py b/authentik/core/tests/test_transactional_applications_api.py
index ad122096b1b7..c6fcfb194674 100644
--- a/authentik/core/tests/test_transactional_applications_api.py
+++ b/authentik/core/tests/test_transactional_applications_api.py
@@ -35,6 +35,7 @@ def test_create_transactional(self):
"name": uid,
"authorization_flow": str(create_test_flow().pk),
"invalidation_flow": str(create_test_flow().pk),
+ "redirect_uris": [],
},
},
)
@@ -89,6 +90,7 @@ def test_create_transactional_bindings(self):
"name": uid,
"authorization_flow": str(authorization_flow.pk),
"invalidation_flow": str(authorization_flow.pk),
+ "redirect_uris": [],
},
"policy_bindings": [{"group": group.pk, "order": 0}],
},
@@ -120,6 +122,7 @@ def test_create_transactional_invalid(self):
"name": uid,
"authorization_flow": "",
"invalidation_flow": "",
+ "redirect_uris": [],
},
},
)
diff --git a/authentik/crypto/tests.py b/authentik/crypto/tests.py
index bb0c1b4e31b6..0e3c886d1120 100644
--- a/authentik/crypto/tests.py
+++ b/authentik/crypto/tests.py
@@ -18,7 +18,7 @@
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id, generate_key
-from authentik.providers.oauth2.models import OAuth2Provider
+from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
class TestCrypto(APITestCase):
@@ -274,7 +274,7 @@ def test_used_by(self):
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=keypair,
)
response = self.client.get(
@@ -306,7 +306,7 @@ def test_used_by_denied(self):
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=keypair,
)
response = self.client.get(
diff --git a/authentik/providers/oauth2/api/providers.py b/authentik/providers/oauth2/api/providers.py
index 69879fefdbcc..83d1cba2857a 100644
--- a/authentik/providers/oauth2/api/providers.py
+++ b/authentik/providers/oauth2/api/providers.py
@@ -1,15 +1,18 @@
"""OAuth2Provider API Views"""
from copy import copy
+from re import compile
+from re import error as RegexError
from django.urls import reverse
from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
-from rest_framework.fields import CharField
+from rest_framework.fields import CharField, ChoiceField
from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response
@@ -20,13 +23,39 @@
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
from authentik.core.models import Provider
from authentik.providers.oauth2.id_token import IDToken
-from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ AccessToken,
+ OAuth2Provider,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.rbac.decorators import permission_required
+class RedirectURISerializer(PassiveSerializer):
+ """A single allowed redirect URI entry"""
+
+ matching_mode = ChoiceField(choices=RedirectURIMatchingMode.choices)
+ url = CharField()
+
+
class OAuth2ProviderSerializer(ProviderSerializer):
"""OAuth2Provider Serializer"""
+ redirect_uris = RedirectURISerializer(many=True, source="_redirect_uris")
+
+ def validate_redirect_uris(self, data: list) -> list:
+ for entry in data:
+ if entry.get("matching_mode") == RedirectURIMatchingMode.REGEX:
+ url = entry.get("url")
+ try:
+ compile(url)
+ except RegexError:
+ raise ValidationError(
+ _("Invalid Regex Pattern: {url}".format(url=url))
+ ) from None
+ return data
+
class Meta:
model = OAuth2Provider
fields = ProviderSerializer.Meta.fields + [
@@ -79,7 +108,6 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
"refresh_token_validity",
"include_claims_in_id_token",
"signing_key",
- "redirect_uris",
"sub_mode",
"property_mappings",
"issuer_mode",
diff --git a/authentik/providers/oauth2/errors.py b/authentik/providers/oauth2/errors.py
index e8c5fd9ed828..479eae16f2d6 100644
--- a/authentik/providers/oauth2/errors.py
+++ b/authentik/providers/oauth2/errors.py
@@ -7,7 +7,7 @@
from authentik.events.models import Event, EventAction
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.views import bad_request_message
-from authentik.providers.oauth2.models import GrantTypes
+from authentik.providers.oauth2.models import GrantTypes, RedirectURI
class OAuth2Error(SentryIgnoredException):
@@ -46,9 +46,9 @@ class RedirectUriError(OAuth2Error):
)
provided_uri: str
- allowed_uris: list[str]
+ allowed_uris: list[RedirectURI]
- def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None:
+ def __init__(self, provided_uri: str, allowed_uris: list[RedirectURI]) -> None:
super().__init__()
self.provided_uri = provided_uri
self.allowed_uris = allowed_uris
diff --git a/authentik/providers/oauth2/migrations/0022_remove_accesstoken_session_id_and_more.py b/authentik/providers/oauth2/migrations/0022_remove_accesstoken_session_id_and_more.py
index 081f45962c44..82c5e3ed3aaa 100644
--- a/authentik/providers/oauth2/migrations/0022_remove_accesstoken_session_id_and_more.py
+++ b/authentik/providers/oauth2/migrations/0022_remove_accesstoken_session_id_and_more.py
@@ -37,7 +37,7 @@ def migrate_session(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
class Migration(migrations.Migration):
dependencies = [
- ("authentik_core", "0040_provider_invalidation_flow"),
+ ("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
("authentik_providers_oauth2", "0021_oauth2provider_encryption_key_and_more"),
]
diff --git a/authentik/providers/oauth2/migrations/0023_alter_accesstoken_refreshtoken_use_hash_index.py b/authentik/providers/oauth2/migrations/0023_alter_accesstoken_refreshtoken_use_hash_index.py
index e17440bc3cdf..9e232a90bf93 100644
--- a/authentik/providers/oauth2/migrations/0023_alter_accesstoken_refreshtoken_use_hash_index.py
+++ b/authentik/providers/oauth2/migrations/0023_alter_accesstoken_refreshtoken_use_hash_index.py
@@ -8,7 +8,7 @@
class Migration(migrations.Migration):
dependencies = [
- ("authentik_core", "0040_provider_invalidation_flow"),
+ ("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
("authentik_providers_oauth2", "0022_remove_accesstoken_session_id_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
diff --git a/authentik/providers/oauth2/migrations/0024_remove_oauth2provider_redirect_uris_and_more.py b/authentik/providers/oauth2/migrations/0024_remove_oauth2provider_redirect_uris_and_more.py
new file mode 100644
index 000000000000..6f9dc199f085
--- /dev/null
+++ b/authentik/providers/oauth2/migrations/0024_remove_oauth2provider_redirect_uris_and_more.py
@@ -0,0 +1,49 @@
+# Generated by Django 5.0.9 on 2024-11-04 12:56
+from dataclasses import asdict
+from django.apps.registry import Apps
+
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+from django.db import migrations, models
+
+
+def migrate_redirect_uris(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
+ from authentik.providers.oauth2.models import RedirectURI, RedirectURIMatchingMode
+
+ OAuth2Provider = apps.get_model("authentik_providers_oauth2", "oauth2provider")
+
+ db_alias = schema_editor.connection.alias
+ for provider in OAuth2Provider.objects.using(db_alias).all():
+ uris = []
+ for old in provider.old_redirect_uris.split("\n"):
+ mode = RedirectURIMatchingMode.STRICT
+ if old == "*" or old == ".*":
+ mode = RedirectURIMatchingMode.REGEX
+ uris.append(asdict(RedirectURI(mode, url=old)))
+ provider._redirect_uris = uris
+ provider.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_providers_oauth2", "0023_alter_accesstoken_refreshtoken_use_hash_index"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="oauth2provider",
+ old_name="redirect_uris",
+ new_name="old_redirect_uris",
+ ),
+ migrations.AddField(
+ model_name="oauth2provider",
+ name="_redirect_uris",
+ field=models.JSONField(default=dict, verbose_name="Redirect URIs"),
+ ),
+ migrations.RunPython(migrate_redirect_uris, lambda *args: ...),
+ migrations.RemoveField(
+ model_name="oauth2provider",
+ name="old_redirect_uris",
+ ),
+ ]
diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py
index be2047e33da8..7e9ee3276f79 100644
--- a/authentik/providers/oauth2/models.py
+++ b/authentik/providers/oauth2/models.py
@@ -3,7 +3,7 @@
import base64
import binascii
import json
-from dataclasses import asdict
+from dataclasses import asdict, dataclass
from functools import cached_property
from hashlib import sha256
from typing import Any
@@ -12,6 +12,7 @@
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
+from dacite import Config
from dacite.core import from_dict
from django.contrib.postgres.indexes import HashIndex
from django.db import models
@@ -77,11 +78,25 @@ class IssuerMode(models.TextChoices):
"""Configure how the `iss` field is created."""
GLOBAL = "global", _("Same identifier is used for all providers")
- PER_PROVIDER = "per_provider", _(
- "Each provider has a different issuer, based on the application slug."
+ PER_PROVIDER = (
+ "per_provider",
+ _("Each provider has a different issuer, based on the application slug."),
)
+class RedirectURIMatchingMode(models.TextChoices):
+ STRICT = "strict", _("Strict URL comparison")
+ REGEX = "regex", _("Regular Expression URL matching")
+
+
+@dataclass
+class RedirectURI:
+ """A single redirect URI entry"""
+
+ matching_mode: RedirectURIMatchingMode
+ url: str
+
+
class ResponseTypes(models.TextChoices):
"""Response Type required by the client."""
@@ -156,11 +171,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
verbose_name=_("Client Secret"),
default=generate_client_secret,
)
- redirect_uris = models.TextField(
- default="",
- blank=True,
+ _redirect_uris = models.JSONField(
+ default=dict,
verbose_name=_("Redirect URIs"),
- help_text=_("Enter each URI on a new line."),
)
include_claims_in_id_token = models.BooleanField(
@@ -271,12 +284,33 @@ def get_issuer(self, request: HttpRequest) -> str | None:
except Provider.application.RelatedObjectDoesNotExist:
return None
+ @property
+ def redirect_uris(self) -> list[RedirectURI]:
+ uris = []
+ for entry in self._redirect_uris:
+ uris.append(
+ from_dict(
+ RedirectURI,
+ entry,
+ config=Config(type_hooks={RedirectURIMatchingMode: RedirectURIMatchingMode}),
+ )
+ )
+ return uris
+
+ @redirect_uris.setter
+ def redirect_uris(self, value: list[RedirectURI]):
+ cleansed = []
+ for entry in value:
+ cleansed.append(asdict(entry))
+ self._redirect_uris = cleansed
+
@property
def launch_url(self) -> str | None:
"""Guess launch_url based on first redirect_uri"""
- if self.redirect_uris == "":
+ redirects = self.redirect_uris
+ if len(redirects) < 1:
return None
- main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
+ main_url = redirects[0].url
try:
launch_url = urlparse(main_url)._replace(path="")
return urlunparse(launch_url)
diff --git a/authentik/providers/oauth2/tests/test_api.py b/authentik/providers/oauth2/tests/test_api.py
index 827b68b0c4e9..47ea8ac8df1a 100644
--- a/authentik/providers/oauth2/tests/test_api.py
+++ b/authentik/providers/oauth2/tests/test_api.py
@@ -10,7 +10,13 @@
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
-from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.lib.generators import generate_id
+from authentik.providers.oauth2.models import (
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
class TestAPI(APITestCase):
@@ -21,7 +27,7 @@ def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
@@ -50,9 +56,29 @@ def test_setup_urls(self):
@skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up")
def test_launch_url(self):
"""Test launch_url"""
- self.provider.redirect_uris = (
- "https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/\n"
- )
+ self.provider.redirect_uris = [
+ RedirectURI(
+ RedirectURIMatchingMode.REGEX,
+ "https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/",
+ ),
+ ]
self.provider.save()
self.provider.refresh_from_db()
self.assertIsNone(self.provider.launch_url)
+
+ def test_validate_redirect_uris(self):
+ """Test redirect_uris API"""
+ response = self.client.post(
+ reverse("authentik_api:oauth2provider-list"),
+ data={
+ "name": generate_id(),
+ "authorization_flow": create_test_flow().pk,
+ "invalidation_flow": create_test_flow().pk,
+ "redirect_uris": [
+ {"matching_mode": "strict", "url": "http://goauthentik.io"},
+ {"matching_mode": "regex", "url": "**"},
+ ],
+ },
+ )
+ self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]})
+ self.assertEqual(response.status_code, 400)
diff --git a/authentik/providers/oauth2/tests/test_authorize.py b/authentik/providers/oauth2/tests/test_authorize.py
index 9b614477df5a..032289c0b10f 100644
--- a/authentik/providers/oauth2/tests/test_authorize.py
+++ b/authentik/providers/oauth2/tests/test_authorize.py
@@ -19,6 +19,8 @@
AuthorizationCode,
GrantTypes,
OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
ScopeMapping,
)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -39,7 +41,7 @@ def test_invalid_grant_type(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid/Foo",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
)
with self.assertRaises(AuthorizeError):
request = self.factory.get(
@@ -64,7 +66,7 @@ def test_request(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid/Foo",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
)
with self.assertRaises(AuthorizeError):
request = self.factory.get(
@@ -84,7 +86,7 @@ def test_invalid_redirect_uri(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
)
with self.assertRaises(RedirectUriError):
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
@@ -106,7 +108,7 @@ def test_blocked_redirect_uri(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="data:local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")],
)
with self.assertRaises(RedirectUriError):
request = self.factory.get(
@@ -125,7 +127,7 @@ def test_invalid_redirect_uri_empty(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="",
+ redirect_uris=[],
)
with self.assertRaises(RedirectUriError):
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
@@ -140,7 +142,7 @@ def test_invalid_redirect_uri_empty(self):
)
OAuthAuthorizationParams.from_request(request)
provider.refresh_from_db()
- self.assertEqual(provider.redirect_uris, "+")
+ self.assertEqual(provider.redirect_uris, [RedirectURI(RedirectURIMatchingMode.STRICT, "+")])
def test_invalid_redirect_uri_regex(self):
"""test missing/invalid redirect URI"""
@@ -148,7 +150,7 @@ def test_invalid_redirect_uri_regex(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid?",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")],
)
with self.assertRaises(RedirectUriError):
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
@@ -170,7 +172,7 @@ def test_redirect_uri_invalid_regex(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="+",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")],
)
with self.assertRaises(RedirectUriError):
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
@@ -213,7 +215,7 @@ def test_response_type(self):
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid/Foo",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
@@ -301,7 +303,7 @@ def test_full_code(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="foo://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
@@ -343,7 +345,7 @@ def test_full_implicit(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=self.keypair,
)
provider.property_mappings.set(
@@ -420,7 +422,7 @@ def test_full_implicit_enc(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=self.keypair,
encryption_key=self.keypair,
)
@@ -486,7 +488,7 @@ def test_full_fragment_code(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=self.keypair,
)
Application.objects.create(name="app", slug="app", provider=provider)
@@ -541,7 +543,7 @@ def test_full_form_post_id_token(self):
name=generate_id(),
client_id=generate_id(),
authorization_flow=flow,
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=self.keypair,
)
provider.property_mappings.set(
@@ -599,7 +601,7 @@ def test_full_form_post_code(self):
name=generate_id(),
client_id=generate_id(),
authorization_flow=flow,
- redirect_uris="http://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=self.keypair,
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
diff --git a/authentik/providers/oauth2/tests/test_introspect.py b/authentik/providers/oauth2/tests/test_introspect.py
index 374260a52793..f3f2a0324332 100644
--- a/authentik/providers/oauth2/tests/test_introspect.py
+++ b/authentik/providers/oauth2/tests/test_introspect.py
@@ -11,7 +11,14 @@
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
-from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
+from authentik.providers.oauth2.models import (
+ AccessToken,
+ IDToken,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ RefreshToken,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -23,7 +30,7 @@ def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
signing_key=create_test_cert(),
)
self.app = Application.objects.create(
@@ -118,7 +125,7 @@ def test_introspect_invalid_provider(self):
provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
signing_key=create_test_cert(),
)
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
diff --git a/authentik/providers/oauth2/tests/test_jwks.py b/authentik/providers/oauth2/tests/test_jwks.py
index 8d572125850d..87723fff9f80 100644
--- a/authentik/providers/oauth2/tests/test_jwks.py
+++ b/authentik/providers/oauth2/tests/test_jwks.py
@@ -13,7 +13,7 @@
from authentik.crypto.builder import PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_id
-from authentik.providers.oauth2.models import OAuth2Provider
+from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
from authentik.providers.oauth2.tests.utils import OAuthTestCase
TEST_CORDS_CERT = """
@@ -49,7 +49,7 @@ def test_rs256(self):
name="test",
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=create_test_cert(),
)
app = Application.objects.create(name="test", slug="test", provider=provider)
@@ -68,7 +68,7 @@ def test_hs256(self):
name="test",
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
)
app = Application.objects.create(name="test", slug="test", provider=provider)
response = self.client.get(
@@ -82,7 +82,7 @@ def test_es256(self):
name="test",
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
)
app = Application.objects.create(name="test", slug="test", provider=provider)
@@ -99,7 +99,7 @@ def test_enc(self):
name="test",
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
encryption_key=create_test_cert(PrivateKeyAlg.ECDSA),
)
@@ -122,7 +122,7 @@ def test_ecdsa_coords_mismatched(self):
name="test",
client_id="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=cert,
)
app = Application.objects.create(name="test", slug="test", provider=provider)
diff --git a/authentik/providers/oauth2/tests/test_revoke.py b/authentik/providers/oauth2/tests/test_revoke.py
index a0312ef71a95..3b08688d3e25 100644
--- a/authentik/providers/oauth2/tests/test_revoke.py
+++ b/authentik/providers/oauth2/tests/test_revoke.py
@@ -10,7 +10,14 @@
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
-from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
+from authentik.providers.oauth2.models import (
+ AccessToken,
+ IDToken,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ RefreshToken,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -22,7 +29,7 @@ def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
signing_key=create_test_cert(),
)
self.app = Application.objects.create(
diff --git a/authentik/providers/oauth2/tests/test_token.py b/authentik/providers/oauth2/tests/test_token.py
index 214b6fe22d01..c2e897182ab9 100644
--- a/authentik/providers/oauth2/tests/test_token.py
+++ b/authentik/providers/oauth2/tests/test_token.py
@@ -22,6 +22,8 @@
AccessToken,
AuthorizationCode,
OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
RefreshToken,
ScopeMapping,
)
@@ -42,7 +44,7 @@ def test_request_auth_code(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://TestServer",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")],
signing_key=self.keypair,
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
@@ -69,7 +71,7 @@ def test_request_auth_code_invalid(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=self.keypair,
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
@@ -90,7 +92,7 @@ def test_request_refresh_token(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
@@ -118,7 +120,7 @@ def test_auth_code_view(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
)
# Needs to be assigned to an application for iss to be set
@@ -157,7 +159,7 @@ def test_auth_code_enc(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
encryption_key=self.keypair,
)
@@ -188,7 +190,7 @@ def test_refresh_token_view(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
)
provider.property_mappings.set(
@@ -250,7 +252,7 @@ def test_refresh_token_view_invalid_origin(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://local.invalid",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
)
provider.property_mappings.set(
@@ -308,7 +310,7 @@ def test_refresh_token_revoke(self):
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=self.keypair,
)
provider.property_mappings.set(
diff --git a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py
index 29df22f91b5b..d52a2ed020de 100644
--- a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py
+++ b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py
@@ -19,7 +19,12 @@
SCOPE_OPENID_PROFILE,
TOKEN_TYPE,
)
-from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.jwks import JWKSView
from authentik.sources.oauth.models import OAuthSource
@@ -54,7 +59,7 @@ def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=self.cert,
)
self.provider.jwks_sources.add(self.source)
diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard.py b/authentik/providers/oauth2/tests/test_token_cc_standard.py
index 7b233794cdb1..f917c96617c2 100644
--- a/authentik/providers/oauth2/tests/test_token_cc_standard.py
+++ b/authentik/providers/oauth2/tests/test_token_cc_standard.py
@@ -19,7 +19,13 @@
TOKEN_TYPE,
)
from authentik.providers.oauth2.errors import TokenError
-from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ AccessToken,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -33,7 +39,7 @@ def setUp(self) -> None:
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
@@ -107,6 +113,48 @@ def test_permission_denied(self):
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
+ def test_incorrect_scopes(self):
+ """test scope that isn't configured"""
+ response = self.client.post(
+ reverse("authentik_providers_oauth2:token"),
+ {
+ "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+ "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE} extra_scope",
+ "client_id": self.provider.client_id,
+ "client_secret": self.provider.client_secret,
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ body = loads(response.content.decode())
+ self.assertEqual(body["token_type"], TOKEN_TYPE)
+ token = AccessToken.objects.filter(
+ provider=self.provider, token=body["access_token"]
+ ).first()
+ self.assertSetEqual(
+ set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE}
+ )
+ _, alg = self.provider.jwt_key
+ jwt = decode(
+ body["access_token"],
+ key=self.provider.signing_key.public_key,
+ algorithms=[alg],
+ audience=self.provider.client_id,
+ )
+ self.assertEqual(
+ jwt["given_name"], "Autogenerated user from application test (client credentials)"
+ )
+ self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
+ jwt = decode(
+ body["id_token"],
+ key=self.provider.signing_key.public_key,
+ algorithms=[alg],
+ audience=self.provider.client_id,
+ )
+ self.assertEqual(
+ jwt["given_name"], "Autogenerated user from application test (client credentials)"
+ )
+ self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
+
def test_successful(self):
"""test successful"""
response = self.client.post(
diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py b/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py
index 1c54ad38f1ed..8e4b1bbfe2a1 100644
--- a/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py
+++ b/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py
@@ -20,7 +20,12 @@
TOKEN_TYPE,
)
from authentik.providers.oauth2.errors import TokenError
-from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -34,7 +39,7 @@ def setUp(self) -> None:
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
diff --git a/authentik/providers/oauth2/tests/test_token_cc_user_pw.py b/authentik/providers/oauth2/tests/test_token_cc_user_pw.py
index 0af554c2b27f..bf57eca32b30 100644
--- a/authentik/providers/oauth2/tests/test_token_cc_user_pw.py
+++ b/authentik/providers/oauth2/tests/test_token_cc_user_pw.py
@@ -19,7 +19,12 @@
TOKEN_TYPE,
)
from authentik.providers.oauth2.errors import TokenError
-from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -33,7 +38,7 @@ def setUp(self) -> None:
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
diff --git a/authentik/providers/oauth2/tests/test_token_device.py b/authentik/providers/oauth2/tests/test_token_device.py
index 308b8d2d281a..212828897e7e 100644
--- a/authentik/providers/oauth2/tests/test_token_device.py
+++ b/authentik/providers/oauth2/tests/test_token_device.py
@@ -9,8 +9,19 @@
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_code_fixed_length, generate_id
-from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE
-from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.constants import (
+ GRANT_TYPE_DEVICE_CODE,
+ SCOPE_OPENID,
+ SCOPE_OPENID_EMAIL,
+)
+from authentik.providers.oauth2.models import (
+ AccessToken,
+ DeviceToken,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -24,7 +35,7 @@ def setUp(self) -> None:
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
- redirect_uris="http://testserver",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
@@ -80,3 +91,28 @@ def test_code(self):
},
)
self.assertEqual(res.status_code, 200)
+
+ def test_code_mismatched_scope(self):
+ """Test code with user (mismatched scopes)"""
+ device_token = DeviceToken.objects.create(
+ provider=self.provider,
+ user_code=generate_code_fixed_length(),
+ device_code=generate_id(),
+ user=self.user,
+ scope=[SCOPE_OPENID, SCOPE_OPENID_EMAIL],
+ )
+ res = self.client.post(
+ reverse("authentik_providers_oauth2:token"),
+ data={
+ "client_id": self.provider.client_id,
+ "grant_type": GRANT_TYPE_DEVICE_CODE,
+ "device_code": device_token.device_code,
+ "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} invalid",
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ body = loads(res.content)
+ token = AccessToken.objects.filter(
+ provider=self.provider, token=body["access_token"]
+ ).first()
+ self.assertSetEqual(set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL})
diff --git a/authentik/providers/oauth2/tests/test_token_pkce.py b/authentik/providers/oauth2/tests/test_token_pkce.py
index 1f64476a9eb9..b296eac65e43 100644
--- a/authentik/providers/oauth2/tests/test_token_pkce.py
+++ b/authentik/providers/oauth2/tests/test_token_pkce.py
@@ -10,7 +10,12 @@
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
-from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider
+from authentik.providers.oauth2.models import (
+ AuthorizationCode,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -30,7 +35,7 @@ def test_pkce_missing_in_authorize(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="foo://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
@@ -93,7 +98,7 @@ def test_pkce_missing_in_token(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="foo://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
@@ -154,7 +159,7 @@ def test_pkce_correct_s256(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="foo://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
@@ -210,7 +215,7 @@ def test_pkce_correct_plain(self):
name=generate_id(),
client_id="test",
authorization_flow=flow,
- redirect_uris="foo://localhost",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
diff --git a/authentik/providers/oauth2/tests/test_userinfo.py b/authentik/providers/oauth2/tests/test_userinfo.py
index 96e48754f7c4..e7cd42326659 100644
--- a/authentik/providers/oauth2/tests/test_userinfo.py
+++ b/authentik/providers/oauth2/tests/test_userinfo.py
@@ -11,7 +11,14 @@
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
-from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ AccessToken,
+ IDToken,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -25,7 +32,7 @@ def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
- redirect_uris="",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py
index 49f785d002f3..156f634f2676 100644
--- a/authentik/providers/oauth2/views/authorize.py
+++ b/authentik/providers/oauth2/views/authorize.py
@@ -56,6 +56,8 @@
AuthorizationCode,
GrantTypes,
OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
ResponseMode,
ResponseTypes,
ScopeMapping,
@@ -187,40 +189,39 @@ def check_grant(self):
def check_redirect_uri(self):
"""Redirect URI validation."""
- allowed_redirect_urls = self.provider.redirect_uris.split()
+ allowed_redirect_urls = self.provider.redirect_uris
if not self.redirect_uri:
LOGGER.warning("Missing redirect uri.")
raise RedirectUriError("", allowed_redirect_urls)
- if self.provider.redirect_uris == "":
+ if len(allowed_redirect_urls) < 1:
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
- self.provider.redirect_uris = self.redirect_uri
+ self.provider.redirect_uris = [
+ RedirectURI(RedirectURIMatchingMode.STRICT, self.redirect_uri)
+ ]
self.provider.save()
- allowed_redirect_urls = self.provider.redirect_uris.split()
-
- if self.provider.redirect_uris == "*":
- LOGGER.info("Converting redirect_uris to regex", redirect=self.redirect_uri)
- self.provider.redirect_uris = ".*"
- self.provider.save()
- allowed_redirect_urls = self.provider.redirect_uris.split()
-
- try:
- if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
- LOGGER.warning(
- "Invalid redirect uri (regex comparison)",
- redirect_uri_given=self.redirect_uri,
- redirect_uri_expected=allowed_redirect_urls,
- )
- raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
- except RegexError as exc:
- LOGGER.info("Failed to parse regular expression, checking directly", exc=exc)
- if not any(x == self.redirect_uri for x in allowed_redirect_urls):
- LOGGER.warning(
- "Invalid redirect uri (strict comparison)",
- redirect_uri_given=self.redirect_uri,
- redirect_uri_expected=allowed_redirect_urls,
- )
- raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) from None
+ allowed_redirect_urls = self.provider.redirect_uris
+
+ match_found = False
+ for allowed in allowed_redirect_urls:
+ if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
+ if self.redirect_uri == allowed.url:
+ match_found = True
+ break
+ if allowed.matching_mode == RedirectURIMatchingMode.REGEX:
+ try:
+ if fullmatch(allowed.url, self.redirect_uri):
+ match_found = True
+ break
+ except RegexError as exc:
+ LOGGER.warning(
+ "Failed to parse regular expression",
+ exc=exc,
+ url=allowed.url,
+ provider=self.provider,
+ )
+ if not match_found:
+ raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
# Check against forbidden schemes
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
diff --git a/authentik/providers/oauth2/views/provider.py b/authentik/providers/oauth2/views/provider.py
index 6c28298d2e96..eab3e62be04a 100644
--- a/authentik/providers/oauth2/views/provider.py
+++ b/authentik/providers/oauth2/views/provider.py
@@ -162,5 +162,5 @@ def dispatch(
OAuth2Provider, pk=application.provider_id
)
response = super().dispatch(request, *args, **kwargs)
- cors_allow(request, response, *self.provider.redirect_uris.split("\n"))
+ cors_allow(request, response, *[x.url for x in self.provider.redirect_uris])
return response
diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py
index cfb75fedd9b3..aa3a9fcc2657 100644
--- a/authentik/providers/oauth2/views/token.py
+++ b/authentik/providers/oauth2/views/token.py
@@ -58,7 +58,9 @@
ClientTypes,
DeviceToken,
OAuth2Provider,
+ RedirectURIMatchingMode,
RefreshToken,
+ ScopeMapping,
)
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
@@ -77,7 +79,7 @@ class TokenParams:
redirect_uri: str
grant_type: str
state: str
- scope: list[str]
+ scope: set[str]
provider: OAuth2Provider
@@ -112,11 +114,26 @@ def parse(
redirect_uri=request.POST.get("redirect_uri", ""),
grant_type=request.POST.get("grant_type", ""),
state=request.POST.get("state", ""),
- scope=request.POST.get("scope", "").split(),
+ scope=set(request.POST.get("scope", "").split()),
# PKCE parameter.
code_verifier=request.POST.get("code_verifier"),
)
+ def __check_scopes(self):
+ allowed_scope_names = set(
+ ScopeMapping.objects.filter(provider__in=[self.provider]).values_list(
+ "scope_name", flat=True
+ )
+ )
+ scopes_to_check = self.scope
+ if not scopes_to_check.issubset(allowed_scope_names):
+ LOGGER.info(
+ "Application requested scopes not configured, setting to overlap",
+ scope_allowed=allowed_scope_names,
+ scope_given=self.scope,
+ )
+ self.scope = self.scope.intersection(allowed_scope_names)
+
def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs):
with start_span(
op="authentik.providers.oauth2.token.policy",
@@ -149,7 +166,7 @@ def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
client_id=self.provider.client_id,
)
raise TokenError("invalid_client")
-
+ self.__check_scopes()
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
with start_span(
op="authentik.providers.oauth2.post.parse.code",
@@ -179,42 +196,7 @@ def __post_init_code(self, raw_code: str, request: HttpRequest):
LOGGER.warning("Missing authorization code")
raise TokenError("invalid_grant")
- allowed_redirect_urls = self.provider.redirect_uris.split()
- # At this point, no provider should have a blank redirect_uri, in case they do
- # this will check an empty array and raise an error
- try:
- if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
- LOGGER.warning(
- "Invalid redirect uri (regex comparison)",
- redirect_uri=self.redirect_uri,
- expected=allowed_redirect_urls,
- )
- Event.new(
- EventAction.CONFIGURATION_ERROR,
- message="Invalid redirect URI used by provider",
- provider=self.provider,
- redirect_uri=self.redirect_uri,
- expected=allowed_redirect_urls,
- ).from_http(request)
- raise TokenError("invalid_client")
- except RegexError as exc:
- LOGGER.info("Failed to parse regular expression, checking directly", exc=exc)
- if not any(x == self.redirect_uri for x in allowed_redirect_urls):
- LOGGER.warning(
- "Invalid redirect uri (strict comparison)",
- redirect_uri=self.redirect_uri,
- expected=allowed_redirect_urls,
- )
- Event.new(
- EventAction.CONFIGURATION_ERROR,
- message="Invalid redirect_uri configured",
- provider=self.provider,
- ).from_http(request)
- raise TokenError("invalid_client") from None
-
- # Check against forbidden schemes
- if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
- raise TokenError("invalid_request")
+ self.__check_redirect_uri(request)
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
if not self.authorization_code:
@@ -254,6 +236,48 @@ def __post_init_code(self, raw_code: str, request: HttpRequest):
if not self.authorization_code.code_challenge and self.code_verifier:
raise TokenError("invalid_grant")
+ def __check_redirect_uri(self, request: HttpRequest):
+ allowed_redirect_urls = self.provider.redirect_uris
+ # At this point, no provider should have a blank redirect_uri, in case they do
+ # this will check an empty array and raise an error
+
+ match_found = False
+ for allowed in allowed_redirect_urls:
+ if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
+ if self.redirect_uri == allowed.url:
+ match_found = True
+ break
+ if allowed.matching_mode == RedirectURIMatchingMode.REGEX:
+ try:
+ if fullmatch(allowed.url, self.redirect_uri):
+ match_found = True
+ break
+ except RegexError as exc:
+ LOGGER.warning(
+ "Failed to parse regular expression",
+ exc=exc,
+ url=allowed.url,
+ provider=self.provider,
+ )
+ Event.new(
+ EventAction.CONFIGURATION_ERROR,
+ message="Invalid redirect_uri configured",
+ provider=self.provider,
+ ).from_http(request)
+ if not match_found:
+ Event.new(
+ EventAction.CONFIGURATION_ERROR,
+ message="Invalid redirect URI used by provider",
+ provider=self.provider,
+ redirect_uri=self.redirect_uri,
+ expected=allowed_redirect_urls,
+ ).from_http(request)
+ raise TokenError("invalid_client")
+
+ # Check against forbidden schemes
+ if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
+ raise TokenError("invalid_request")
+
def __post_init_refresh(self, raw_token: str, request: HttpRequest):
if not raw_token:
LOGGER.warning("Missing refresh token")
@@ -497,7 +521,7 @@ def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpRespo
response = super().dispatch(request, *args, **kwargs)
allowed_origins = []
if self.provider:
- allowed_origins = self.provider.redirect_uris.split("\n")
+ allowed_origins = [x.url for x in self.provider.redirect_uris]
cors_allow(self.request, response, *allowed_origins)
return response
@@ -710,7 +734,7 @@ def create_device_code_response(self) -> dict[str, Any]:
"id_token": access_token.id_token.to_jwt(self.provider),
}
- if SCOPE_OFFLINE_ACCESS in self.params.scope:
+ if SCOPE_OFFLINE_ACCESS in self.params.device_code.scope:
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
refresh_token = RefreshToken(
user=self.params.device_code.user,
diff --git a/authentik/providers/oauth2/views/userinfo.py b/authentik/providers/oauth2/views/userinfo.py
index ad3c263643f0..cf151cf6d655 100644
--- a/authentik/providers/oauth2/views/userinfo.py
+++ b/authentik/providers/oauth2/views/userinfo.py
@@ -108,7 +108,7 @@ def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpRespo
response = super().dispatch(request, *args, **kwargs)
allowed_origins = []
if self.token:
- allowed_origins = self.token.provider.redirect_uris.split("\n")
+ allowed_origins = [x.url for x in self.token.provider.redirect_uris]
cors_allow(self.request, response, *allowed_origins)
return response
diff --git a/authentik/providers/proxy/api.py b/authentik/providers/proxy/api.py
index 88ff9fe01e4a..2d02096bb87c 100644
--- a/authentik/providers/proxy/api.py
+++ b/authentik/providers/proxy/api.py
@@ -13,6 +13,7 @@
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.lib.utils.time import timedelta_from_string
+from authentik.providers.oauth2.api.providers import RedirectURISerializer
from authentik.providers.oauth2.models import ScopeMapping
from authentik.providers.oauth2.views.provider import ProviderInfoView
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
@@ -39,7 +40,7 @@ class ProxyProviderSerializer(ProviderSerializer):
"""ProxyProvider Serializer"""
client_id = CharField(read_only=True)
- redirect_uris = CharField(read_only=True)
+ redirect_uris = RedirectURISerializer(many=True, read_only=True, source="_redirect_uris")
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
def validate_basic_auth_enabled(self, value: bool) -> bool:
@@ -121,7 +122,6 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
"basic_auth_password_attribute": ["iexact"],
"basic_auth_user_attribute": ["iexact"],
"mode": ["iexact"],
- "redirect_uris": ["iexact"],
"cookie_domain": ["iexact"],
}
search_fields = ["name"]
diff --git a/authentik/providers/proxy/models.py b/authentik/providers/proxy/models.py
index f82449530915..51c5e535252c 100644
--- a/authentik/providers/proxy/models.py
+++ b/authentik/providers/proxy/models.py
@@ -13,7 +13,13 @@
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
from authentik.outposts.models import OutpostModel
-from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ ClientTypes,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
SCOPE_AK_PROXY = "ak_proxy"
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
@@ -24,14 +30,14 @@ def get_cookie_secret():
return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
-def _get_callback_url(uri: str) -> str:
- return "\n".join(
- [
- urljoin(uri, "outpost.goauthentik.io/callback")
- + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
- uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
- ]
- )
+def _get_callback_url(uri: str) -> list[RedirectURI]:
+ return [
+ RedirectURI(
+ RedirectURIMatchingMode.STRICT,
+ urljoin(uri, "outpost.goauthentik.io/callback") + f"?{OUTPOST_CALLBACK_SIGNATURE}=true",
+ ),
+ RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"?{OUTPOST_CALLBACK_SIGNATURE}=true"),
+ ]
class ProxyMode(models.TextChoices):
diff --git a/authentik/root/monitoring.py b/authentik/root/monitoring.py
index 2820b0725807..ab6a7153fbb9 100644
--- a/authentik/root/monitoring.py
+++ b/authentik/root/monitoring.py
@@ -1,6 +1,8 @@
"""Metrics view"""
-from base64 import b64encode
+from hmac import compare_digest
+from pathlib import Path
+from tempfile import gettempdir
from django.conf import settings
from django.db import connections
@@ -16,22 +18,21 @@
class MetricsView(View):
- """Wrapper around ExportToDjangoView, using http-basic auth"""
+ """Wrapper around ExportToDjangoView with authentication, accessed by the authentik router"""
+
+ def __init__(self, **kwargs):
+ _tmp = Path(gettempdir())
+ with open(_tmp / "authentik-core-metrics.key") as _f:
+ self.monitoring_key = _f.read()
def get(self, request: HttpRequest) -> HttpResponse:
"""Check for HTTP-Basic auth"""
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
auth_type, _, given_credentials = auth_header.partition(" ")
- credentials = f"monitor:{settings.SECRET_KEY}"
- expected = b64encode(str.encode(credentials)).decode()
- authed = auth_type == "Basic" and given_credentials == expected
+ authed = auth_type == "Bearer" and compare_digest(given_credentials, self.monitoring_key)
if not authed and not settings.DEBUG:
- response = HttpResponse(status=401)
- response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"'
- return response
-
+ return HttpResponse(status=401)
monitoring_set.send_robust(self)
-
return ExportToDjangoView(request)
diff --git a/authentik/root/tests.py b/authentik/root/tests.py
index 93ebc659bc06..175caa8d994c 100644
--- a/authentik/root/tests.py
+++ b/authentik/root/tests.py
@@ -1,8 +1,9 @@
"""root tests"""
-from base64 import b64encode
+from pathlib import Path
+from secrets import token_urlsafe
+from tempfile import gettempdir
-from django.conf import settings
from django.test import TestCase
from django.urls import reverse
@@ -10,6 +11,16 @@
class TestRoot(TestCase):
"""Test root application"""
+ def setUp(self):
+ _tmp = Path(gettempdir())
+ self.token = token_urlsafe(32)
+ with open(_tmp / "authentik-core-metrics.key", "w") as _f:
+ _f.write(self.token)
+
+ def tearDown(self):
+ _tmp = Path(gettempdir())
+ (_tmp / "authentik-core-metrics.key").unlink()
+
def test_monitoring_error(self):
"""Test monitoring without any credentials"""
response = self.client.get(reverse("metrics"))
@@ -17,8 +28,7 @@ def test_monitoring_error(self):
def test_monitoring_ok(self):
"""Test monitoring with credentials"""
- creds = "Basic " + b64encode(f"monitor:{settings.SECRET_KEY}".encode()).decode("utf-8")
- auth_headers = {"HTTP_AUTHORIZATION": creds}
+ auth_headers = {"HTTP_AUTHORIZATION": f"Bearer {self.token}"}
response = self.client.get(reverse("metrics"), **auth_headers)
self.assertEqual(response.status_code, 200)
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 3abfd476d6b2..6fe68733b639 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -5570,9 +5570,30 @@
"description": "Key used to encrypt the tokens. When set, tokens will be encrypted and returned as JWEs."
},
"redirect_uris": {
- "type": "string",
- "title": "Redirect URIs",
- "description": "Enter each URI on a new line."
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "matching_mode": {
+ "type": "string",
+ "enum": [
+ "strict",
+ "regex"
+ ],
+ "title": "Matching mode"
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "title": "Url"
+ }
+ },
+ "required": [
+ "matching_mode",
+ "url"
+ ]
+ },
+ "title": "Redirect uris"
},
"sub_mode": {
"type": "string",
diff --git a/internal/outpost/ldap/ldap.go b/internal/outpost/ldap/ldap.go
index 0bdd2a231b5e..383682c78a8b 100644
--- a/internal/outpost/ldap/ldap.go
+++ b/internal/outpost/ldap/ldap.go
@@ -65,7 +65,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
ls.log.WithField("listen", listen).WithError(err).Warning("Failed to listen (SSL)")
return err
}
- proxyListener := &proxyproto.Listener{Listener: ln}
+ proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer proxyListener.Close()
ls.log.WithField("listen", listen).Info("Starting LDAP server")
diff --git a/internal/outpost/ldap/ldap_tls.go b/internal/outpost/ldap/ldap_tls.go
index 4866769a621e..48d4bcf8d95d 100644
--- a/internal/outpost/ldap/ldap_tls.go
+++ b/internal/outpost/ldap/ldap_tls.go
@@ -48,7 +48,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
return err
}
- proxyListener := &proxyproto.Listener{Listener: ln}
+ proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer proxyListener.Close()
tln := tls.NewListener(proxyListener, tlsConfig)
diff --git a/internal/outpost/proxyv2/proxyv2.go b/internal/outpost/proxyv2/proxyv2.go
index 9bc893b6c2f2..eed0ef18acf3 100644
--- a/internal/outpost/proxyv2/proxyv2.go
+++ b/internal/outpost/proxyv2/proxyv2.go
@@ -129,7 +129,7 @@ func (ps *ProxyServer) ServeHTTP() {
ps.log.WithField("listen", listenAddress).WithError(err).Warning("Failed to listen")
return
}
- proxyListener := &proxyproto.Listener{Listener: listener}
+ proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer proxyListener.Close()
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
@@ -148,7 +148,7 @@ func (ps *ProxyServer) ServeHTTPS() {
ps.log.WithError(err).Warning("Failed to listen (TLS)")
return
}
- proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}}
+ proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer proxyListener.Close()
tlsListener := tls.NewListener(proxyListener, tlsConfig)
diff --git a/internal/utils/proxy.go b/internal/utils/proxy.go
new file mode 100644
index 000000000000..42406f5acdf1
--- /dev/null
+++ b/internal/utils/proxy.go
@@ -0,0 +1,34 @@
+package utils
+
+import (
+ "net"
+
+ "github.com/pires/go-proxyproto"
+ log "github.com/sirupsen/logrus"
+ "goauthentik.io/internal/config"
+)
+
+func GetProxyConnectionPolicy() proxyproto.ConnPolicyFunc {
+ nets := []*net.IPNet{}
+ for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
+ _, cidr, err := net.ParseCIDR(rn)
+ if err != nil {
+ continue
+ }
+ nets = append(nets, cidr)
+ }
+ return func(connPolicyOptions proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
+ host, _, err := net.SplitHostPort(connPolicyOptions.Upstream.String())
+ if err == nil {
+ // remoteAddr will be nil if the IP cannot be parsed
+ remoteAddr := net.ParseIP(host)
+ for _, allowedCidr := range nets {
+ if remoteAddr != nil && allowedCidr.Contains(remoteAddr) {
+ log.WithField("remoteAddr", remoteAddr).WithField("cidr", allowedCidr.String()).Trace("Using remote IP from proxy protocol")
+ return proxyproto.USE, nil
+ }
+ }
+ }
+ return proxyproto.SKIP, nil
+ }
+}
diff --git a/internal/web/metrics.go b/internal/web/metrics.go
index baf486e26d18..40d9671f6e25 100644
--- a/internal/web/metrics.go
+++ b/internal/web/metrics.go
@@ -1,11 +1,15 @@
package web
import (
+ "encoding/base64"
"fmt"
"io"
"net/http"
+ "os"
+ "path"
"github.com/gorilla/mux"
+ "github.com/gorilla/securecookie"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -14,14 +18,25 @@ import (
"goauthentik.io/internal/utils/sentry"
)
+const MetricsKeyFile = "authentik-core-metrics.key"
+
var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_main_request_duration_seconds",
Help: "API request latencies in seconds",
}, []string{"dest"})
func (ws *WebServer) runMetricsServer() {
- m := mux.NewRouter()
l := log.WithField("logger", "authentik.router.metrics")
+ tmp := os.TempDir()
+ key := base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(64))
+ keyPath := path.Join(tmp, MetricsKeyFile)
+ err := os.WriteFile(keyPath, []byte(key), 0o600)
+ if err != nil {
+ l.WithError(err).Warning("failed to save metrics key")
+ return
+ }
+
+ m := mux.NewRouter()
m.Use(sentry.SentryNoSampleMiddleware)
m.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
promhttp.InstrumentMetricHandler(
@@ -36,7 +51,7 @@ func (ws *WebServer) runMetricsServer() {
l.WithError(err).Warning("failed to get upstream metrics")
return
}
- re.SetBasicAuth("monitor", config.Get().SecretKey)
+ re.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
res, err := ws.upstreamHttpClient().Do(re)
if err != nil {
l.WithError(err).Warning("failed to get upstream metrics")
@@ -49,9 +64,13 @@ func (ws *WebServer) runMetricsServer() {
}
})
l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server")
- err := http.ListenAndServe(config.Get().Listen.Metrics, m)
+ err = http.ListenAndServe(config.Get().Listen.Metrics, m)
if err != nil {
l.WithError(err).Warning("Failed to start metrics server")
}
l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server")
+ err = os.Remove(keyPath)
+ if err != nil {
+ l.WithError(err).Warning("failed to remove metrics key file")
+ }
}
diff --git a/internal/web/static.go b/internal/web/static.go
index 617c94578f93..650e38b058bb 100644
--- a/internal/web/static.go
+++ b/internal/web/static.go
@@ -42,8 +42,11 @@ func (ws *WebServer) configureStatic() {
// Media files, if backend is file
if config.Get().Storage.Media.Backend == "file" {
- fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
- staticRouter.PathPrefix("/media/").Handler(http.StripPrefix("/media", fsMedia))
+ fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)))
+ staticRouter.PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
+ fsMedia.ServeHTTP(w, r)
+ })
}
staticRouter.PathPrefix("/if/help/").Handler(http.StripPrefix("/if/help/", http.FileServer(http.Dir("./website/help/"))))
diff --git a/internal/web/web.go b/internal/web/web.go
index 66f4e29a63f9..186dd4ddf018 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -19,6 +19,7 @@ import (
"goauthentik.io/internal/config"
"goauthentik.io/internal/gounicorn"
"goauthentik.io/internal/outpost/proxyv2"
+ "goauthentik.io/internal/utils"
"goauthentik.io/internal/utils/web"
"goauthentik.io/internal/web/brand_tls"
)
@@ -52,7 +53,7 @@ func NewWebServer() *WebServer {
loggingHandler.Use(web.NewLoggingHandler(l, nil))
tmp := os.TempDir()
- socketPath := path.Join(tmp, "authentik-core.sock")
+ socketPath := path.Join(tmp, UnixSocketName)
// create http client to talk to backend, normal client if we're in debug more
// and a client that connects to our socket when in non debug mode
@@ -149,7 +150,7 @@ func (ws *WebServer) listenPlain() {
ws.log.WithError(err).Warning("failed to listen")
return
}
- proxyListener := &proxyproto.Listener{Listener: ln}
+ proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer proxyListener.Close()
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
diff --git a/internal/web/tls.go b/internal/web/web_tls.go
similarity index 94%
rename from internal/web/tls.go
rename to internal/web/web_tls.go
index f20cf5cc1524..7ccaf4dff97e 100644
--- a/internal/web/tls.go
+++ b/internal/web/web_tls.go
@@ -45,7 +45,7 @@ func (ws *WebServer) listenTLS() {
ws.log.WithError(err).Warning("failed to listen (TLS)")
return
}
- proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}}
+ proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer proxyListener.Close()
tlsListener := tls.NewListener(proxyListener, tlsConfig)
diff --git a/locale/it/LC_MESSAGES/django.po b/locale/it/LC_MESSAGES/django.po
index 000198dbaee3..47933b8d0838 100644
--- a/locale/it/LC_MESSAGES/django.po
+++ b/locale/it/LC_MESSAGES/django.po
@@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-28 00:09+0000\n"
+"POT-Creation-Date: 2024-11-18 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: tom max, 2024\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
@@ -121,6 +121,10 @@ msgstr "Brand"
msgid "Brands"
msgstr "Brands"
+#: authentik/core/api/devices.py
+msgid "Extra description not available"
+msgstr "Descrizione extra non disponibile"
+
#: authentik/core/api/providers.py
msgid ""
"When not set all providers are returned. When set to true, only backchannel "
@@ -131,6 +135,11 @@ msgstr ""
" vengono restituiti solo i provider di backchannel. Se impostato su falso, i"
" provider di backchannel vengono esclusi"
+#: authentik/core/api/transactional_applications.py
+#, python-brace-format
+msgid "User lacks permission to create {model}"
+msgstr "L'utente non ha i diritti per creare {model}"
+
#: authentik/core/api/users.py
msgid "No leading or trailing slashes allowed."
msgstr "Non sono consentite barre oblique iniziali o finali."
@@ -1240,6 +1249,10 @@ msgstr ""
msgid "Password not set in context"
msgstr "Password non impostata nel contesto"
+#: authentik/policies/password/models.py
+msgid "Invalid password."
+msgstr "Password invalida."
+
#: authentik/policies/password/models.py
#, python-format
msgid "Password exists on %(count)d online lists."
@@ -3550,6 +3563,12 @@ msgstr ""
msgid "Globally enable/disable impersonation."
msgstr "Abilita/disabilita globalmente la l'impersonazione."
+#: authentik/tenants/models.py
+msgid "Require administrators to provide a reason for impersonating a user."
+msgstr ""
+"Richiedi agli amministratori di fornire una ragione per impersonare un "
+"utente."
+
#: authentik/tenants/models.py
msgid "Default token duration"
msgstr "Durata token predefinita"
diff --git a/poetry.lock b/poetry.lock
index a0e42afc41ca..5f09dd79a482 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1790,13 +1790,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
-version = "2.153.0"
+version = "2.154.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
files = [
- {file = "google_api_python_client-2.153.0-py2.py3-none-any.whl", hash = "sha256:6ff13bbfa92a57972e33ec3808e18309e5981b8ca1300e5da23bf2b4d6947384"},
- {file = "google_api_python_client-2.153.0.tar.gz", hash = "sha256:35cce8647f9c163fc04fb4d811fc91aae51954a2bdd74918decbe0e65d791dd2"},
+ {file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"},
+ {file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"},
]
[package.dependencies]
@@ -1993,51 +1993,58 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0
[[package]]
name = "httptools"
-version = "0.6.1"
+version = "0.6.4"
description = "A collection of framework independent HTTP protocol utils."
optional = false
python-versions = ">=3.8.0"
files = [
- {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
- {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
- {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"},
- {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"},
- {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"},
- {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"},
- {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"},
- {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"},
- {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"},
- {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"},
- {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"},
- {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"},
- {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"},
- {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"},
- {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"},
- {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"},
- {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"},
- {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"},
- {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"},
- {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"},
- {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"},
- {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"},
- {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"},
- {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"},
- {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"},
- {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"},
- {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"},
- {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"},
- {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"},
- {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"},
- {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"},
- {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"},
- {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"},
- {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"},
- {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"},
- {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"},
+ {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"},
+ {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"},
+ {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"},
+ {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"},
+ {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"},
+ {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"},
+ {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"},
+ {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"},
+ {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"},
+ {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"},
+ {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"},
+ {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"},
+ {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"},
+ {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"},
+ {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"},
+ {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"},
+ {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"},
+ {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"},
+ {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"},
+ {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"},
+ {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"},
+ {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"},
+ {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"},
+ {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"},
+ {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"},
+ {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"},
+ {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"},
+ {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"},
+ {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"},
+ {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"},
+ {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"},
+ {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"},
+ {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"},
+ {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"},
+ {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"},
+ {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"},
+ {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"},
+ {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"},
+ {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"},
+ {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"},
+ {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"},
+ {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"},
+ {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"},
]
[package.extras]
-test = ["Cython (>=0.29.24,<0.30.0)"]
+test = ["Cython (>=0.29.24)"]
[[package]]
name = "httpx"
@@ -3711,20 +3718,20 @@ files = [
[[package]]
name = "pydantic"
-version = "2.9.2"
+version = "2.10.0"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
- {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
+ {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"},
+ {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
-pydantic-core = "2.23.4"
-typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
+pydantic-core = "2.27.0"
+typing-extensions = ">=4.12.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
@@ -3732,100 +3739,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
-version = "2.23.4"
+version = "2.27.0"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
- {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
- {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
- {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
- {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
- {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
- {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
- {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
- {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
- {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
- {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
- {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
- {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
- {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
- {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
- {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
- {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
- {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
- {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
- {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
- {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
- {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
- {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
- {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
- {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
- {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
- {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
- {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
- {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
- {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
- {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
- {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
- {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
- {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
- {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
- {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
- {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"},
+ {file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"},
+ {file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"},
+ {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"},
+ {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"},
+ {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"},
+ {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"},
+ {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"},
+ {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"},
+ {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"},
+ {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"},
+ {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"},
+ {file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"},
+ {file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"},
+ {file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"},
+ {file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"},
+ {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"},
]
[package.dependencies]
@@ -5041,20 +5059,20 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
-version = "0.32.0"
+version = "0.32.1"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
- {file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"},
- {file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"},
+ {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
+ {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
]
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
h11 = ">=0.8"
-httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
+httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
@@ -5062,7 +5080,7 @@ watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standar
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
[package.extras]
-standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "uvloop"
diff --git a/schema.yml b/schema.yml
index bf54f4a662b3..914c5d6e84db 100644
--- a/schema.yml
+++ b/schema.yml
@@ -20224,10 +20224,6 @@ paths:
format: uuid
explode: true
style: form
- - in: query
- name: redirect_uris
- schema:
- type: string
- in: query
name: refresh_token_validity
schema:
@@ -20643,10 +20639,6 @@ paths:
format: uuid
explode: true
style: form
- - in: query
- name: redirect_uris__iexact
- schema:
- type: string
- name: search
required: false
in: query
@@ -44074,6 +44066,11 @@ components:
required:
- challenge
- name
+ MatchingModeEnum:
+ enum:
+ - strict
+ - regex
+ type: string
Metadata:
type: object
description: Serializer for blueprint metadata
@@ -44776,8 +44773,9 @@ components:
description: Key used to encrypt the tokens. When set, tokens will be encrypted
and returned as JWEs.
redirect_uris:
- type: string
- description: Enter each URI on a new line.
+ type: array
+ items:
+ $ref: '#/components/schemas/RedirectURI'
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -44806,6 +44804,7 @@ components:
- meta_model_name
- name
- pk
+ - redirect_uris
- verbose_name
- verbose_name_plural
OAuth2ProviderRequest:
@@ -44877,8 +44876,9 @@ components:
description: Key used to encrypt the tokens. When set, tokens will be encrypted
and returned as JWEs.
redirect_uris:
- type: string
- description: Enter each URI on a new line.
+ type: array
+ items:
+ $ref: '#/components/schemas/RedirectURIRequest'
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -44900,6 +44900,7 @@ components:
- authorization_flow
- invalidation_flow
- name
+ - redirect_uris
OAuth2ProviderSetupURLs:
type: object
description: OAuth2 Provider Metadata serializer
@@ -48898,8 +48899,9 @@ components:
description: Key used to encrypt the tokens. When set, tokens will be encrypted
and returned as JWEs.
redirect_uris:
- type: string
- description: Enter each URI on a new line.
+ type: array
+ items:
+ $ref: '#/components/schemas/RedirectURIRequest'
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -51496,7 +51498,9 @@ components:
description: When enabled, this provider will intercept the authorization
header and authenticate requests based on its value.
redirect_uris:
- type: string
+ type: array
+ items:
+ $ref: '#/components/schemas/RedirectURI'
readOnly: true
cookie_domain:
type: string
@@ -52092,6 +52096,29 @@ components:
type: string
required:
- to
+ RedirectURI:
+ type: object
+ description: A single allowed redirect URI entry
+ properties:
+ matching_mode:
+ $ref: '#/components/schemas/MatchingModeEnum'
+ url:
+ type: string
+ required:
+ - matching_mode
+ - url
+ RedirectURIRequest:
+ type: object
+ description: A single allowed redirect URI entry
+ properties:
+ matching_mode:
+ $ref: '#/components/schemas/MatchingModeEnum'
+ url:
+ type: string
+ minLength: 1
+ required:
+ - matching_mode
+ - url
Reputation:
type: object
description: Reputation Serializer
diff --git a/tests/e2e/test_provider_oauth2_github.py b/tests/e2e/test_provider_oauth2_github.py
index 31db3b04c7d7..977308ead161 100644
--- a/tests/e2e/test_provider_oauth2_github.py
+++ b/tests/e2e/test_provider_oauth2_github.py
@@ -12,7 +12,12 @@
from authentik.lib.generators import generate_id, generate_key
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
-from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider
+from authentik.providers.oauth2.models import (
+ ClientTypes,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+)
from tests.e2e.utils import SeleniumTestCase, retry
@@ -73,7 +78,9 @@ def test_authorization_consent_implied(self):
client_id=self.client_id,
client_secret=self.client_secret,
client_type=ClientTypes.CONFIDENTIAL,
- redirect_uris="http://localhost:3000/login/github",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github")
+ ],
authorization_flow=authorization_flow,
)
Application.objects.create(
@@ -128,7 +135,9 @@ def test_authorization_consent_explicit(self):
client_id=self.client_id,
client_secret=self.client_secret,
client_type=ClientTypes.CONFIDENTIAL,
- redirect_uris="http://localhost:3000/login/github",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github")
+ ],
authorization_flow=authorization_flow,
)
app = Application.objects.create(
@@ -199,7 +208,9 @@ def test_denied(self):
client_id=self.client_id,
client_secret=self.client_secret,
client_type=ClientTypes.CONFIDENTIAL,
- redirect_uris="http://localhost:3000/login/github",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github")
+ ],
authorization_flow=authorization_flow,
)
app = Application.objects.create(
diff --git a/tests/e2e/test_provider_oauth2_grafana.py b/tests/e2e/test_provider_oauth2_grafana.py
index 1a862b3246f0..14816abf5635 100644
--- a/tests/e2e/test_provider_oauth2_grafana.py
+++ b/tests/e2e/test_provider_oauth2_grafana.py
@@ -19,7 +19,13 @@
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
)
-from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ ClientTypes,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from tests.e2e.utils import SeleniumTestCase, retry
@@ -82,7 +88,7 @@ def test_redirect_uri_error(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:3000/",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/")],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
@@ -131,7 +137,11 @@ def test_authorization_consent_implied(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:3000/login/generic_oauth",
+ redirect_uris=[
+ RedirectURI(
+ RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
+ )
+ ],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
@@ -200,7 +210,11 @@ def test_authorization_logout(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:3000/login/generic_oauth",
+ redirect_uris=[
+ RedirectURI(
+ RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
+ )
+ ],
authorization_flow=authorization_flow,
invalidation_flow=invalidation_flow,
)
@@ -275,7 +289,11 @@ def test_authorization_consent_explicit(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:3000/login/generic_oauth",
+ redirect_uris=[
+ RedirectURI(
+ RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
+ )
+ ],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
@@ -355,7 +373,11 @@ def test_authorization_denied(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:3000/login/generic_oauth",
+ redirect_uris=[
+ RedirectURI(
+ RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
+ )
+ ],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
diff --git a/tests/e2e/test_provider_oidc.py b/tests/e2e/test_provider_oidc.py
index 60ee951c2d1e..e8cf11c5b3f7 100644
--- a/tests/e2e/test_provider_oidc.py
+++ b/tests/e2e/test_provider_oidc.py
@@ -19,7 +19,13 @@
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
)
-from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ ClientTypes,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from tests.e2e.utils import SeleniumTestCase, retry
@@ -67,7 +73,7 @@ def test_redirect_uri_error(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
@@ -116,7 +122,9 @@ def test_authorization_consent_implied(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/auth/callback",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
+ ],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
@@ -188,7 +196,9 @@ def test_authorization_consent_explicit(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/auth/callback",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
+ ],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
@@ -259,7 +269,9 @@ def test_authorization_denied(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/auth/callback",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
+ ],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
diff --git a/tests/e2e/test_provider_oidc_implicit.py b/tests/e2e/test_provider_oidc_implicit.py
index 7d1a0497fecd..8c7cad0c6957 100644
--- a/tests/e2e/test_provider_oidc_implicit.py
+++ b/tests/e2e/test_provider_oidc_implicit.py
@@ -19,7 +19,13 @@
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
)
-from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.models import (
+ ClientTypes,
+ OAuth2Provider,
+ RedirectURI,
+ RedirectURIMatchingMode,
+ ScopeMapping,
+)
from tests.e2e.utils import SeleniumTestCase, retry
@@ -68,7 +74,7 @@ def test_redirect_uri_error(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/",
+ redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
@@ -117,7 +123,9 @@ def test_authorization_consent_implied(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/implicit/",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
+ ],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
@@ -170,7 +178,9 @@ def test_authorization_consent_explicit(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/implicit/",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
+ ],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
@@ -238,7 +248,9 @@ def test_authorization_denied(self):
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
- redirect_uris="http://localhost:9009/implicit/",
+ redirect_uris=[
+ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
+ ],
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
diff --git a/web/package-lock.json b/web/package-lock.json
index dbd76c407f51..ca83495fde43 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
- "@goauthentik/api": "^2024.10.2-1731887740",
+ "@goauthentik/api": "^2024.10.2-1732206118",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@@ -84,7 +84,7 @@
"@wdio/cli": "^9.1.2",
"@wdio/spec-reporter": "^9.1.2",
"chokidar": "^4.0.1",
- "chromedriver": "^129.0.2",
+ "chromedriver": "^130.0.4",
"esbuild": "^0.24.0",
"eslint": "^9.11.1",
"eslint-plugin-lit": "^1.15.0",
@@ -1775,9 +1775,9 @@
}
},
"node_modules/@goauthentik/api": {
- "version": "2024.10.2-1731887740",
- "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.2-1731887740.tgz",
- "integrity": "sha512-AYFgmLXPvwLsxMdCX0e51FD2Fj8T++L8beU3dyDvlWUp4jH5+2XMG/AtQI3v2mNCyKx1EDlublIVYzpmQnrtag=="
+ "version": "2024.10.2-1732206118",
+ "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.2-1732206118.tgz",
+ "integrity": "sha512-Zg90AJvGDquD3u73yIBKXFBDxsCljPxVqylylS6hgPzkLSogKVVkjhmKteWFXDrVxxsxo5XIa4FuTe3wAERyzw=="
},
"node_modules/@goauthentik/web": {
"resolved": "",
@@ -8699,9 +8699,9 @@
}
},
"node_modules/chromedriver": {
- "version": "129.0.2",
- "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-129.0.2.tgz",
- "integrity": "sha512-rUEFCJAmAwOdFfaDFtveT97fFeA7NOxlkgyPyN+G09Ws4qGW39aLDxMQBbS9cxQQHhTihqZZobgF5CLVYXnmGA==",
+ "version": "130.0.4",
+ "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-130.0.4.tgz",
+ "integrity": "sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
diff --git a/web/package.json b/web/package.json
index 97db2787cee7..691d9d99599c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
- "@goauthentik/api": "^2024.10.2-1731887740",
+ "@goauthentik/api": "^2024.10.2-1732206118",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@@ -72,7 +72,7 @@
"@wdio/cli": "^9.1.2",
"@wdio/spec-reporter": "^9.1.2",
"chokidar": "^4.0.1",
- "chromedriver": "^129.0.2",
+ "chromedriver": "^130.0.4",
"esbuild": "^0.24.0",
"eslint": "^9.11.1",
"eslint-plugin-lit": "^1.15.0",
diff --git a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts
index 087d05703a06..a571c89e20a1 100644
--- a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts
+++ b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts
@@ -25,6 +25,7 @@ import {
type TransactionApplicationRequest,
type TransactionApplicationResponse,
ValidationError,
+ instanceOfValidationError,
} from "@goauthentik/api";
import BasePanel from "../BasePanel";
@@ -69,6 +70,9 @@ const successState: State = {
icon: ["fa-check-circle", "pf-m-success"],
};
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
+
@customElement("ak-application-wizard-commit-application")
export class ApplicationWizardCommitApplication extends BasePanel {
static get styles() {
@@ -134,10 +138,25 @@ export class ApplicationWizardCommitApplication extends BasePanel {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = await parseAPIError(resolution);
+
+ // THIS is a really gross special case; if the user is duplicating the name of an
+ // existing provider, the error appears on the `app` (!) error object. We have to
+ // move that to the `provider.name` error field so it shows up in the right place.
+ if (isValidationError(errors) && Array.isArray(errors?.app?.provider)) {
+ const providerError = errors.app.provider;
+ errors.provider = errors.provider ?? {};
+ errors.provider.name = providerError;
+ delete errors.app.provider;
+ if (Object.keys(errors.app).length === 0) {
+ delete errors.app;
+ }
+ }
+
+ this.errors = errors;
this.dispatchWizardUpdate({
update: {
...this.wizard,
- errors,
+ errors: this.errors,
},
status: "failed",
});
diff --git a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts
index db1d6517f828..f689a04f90f7 100644
--- a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts
+++ b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts
@@ -11,6 +11,10 @@ import {
redirectUriHelp,
subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
+import {
+ IRedirectURIInput,
+ akOAuthRedirectURIInput,
+} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import {
makeSourceSelector,
oauth2SourcesProvider,
@@ -31,7 +35,13 @@ import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
-import { ClientTypeEnum, FlowsInstancesListDesignationEnum, SourcesApi } from "@goauthentik/api";
+import {
+ ClientTypeEnum,
+ FlowsInstancesListDesignationEnum,
+ MatchingModeEnum,
+ RedirectURI,
+ SourcesApi,
+} from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@@ -120,14 +130,27 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
>
-
+ ${this.provider.redirectUris.map((ru) => {
+ return html`
- ${this.provider.redirectUris.split("\n").map((url) => {
- return html`
${url}
+ ${this.provider.redirectUris.map((ru) => {
+ return html`
+ ${msg(str`${field}: ${errMsg}`)} +
`; + })}`; + } return html`${message}
`; diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf index 7b141ba7b5a1..37208473fdcb 100644 --- a/web/xliff/de.xlf +++ b/web/xliff/de.xlf @@ -5741,9 +5741,6 @@ Bindings to groups/users are checked against the user of the event.