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 { > - - + ({ + matchingMode: MatchingModeEnum.Strict, + url: "", + })} + .row=${(f?: RedirectURI) => + akOAuthRedirectURIInput({ + ".redirectURI": f, + "style": "width: 100%", + "name": "oauth2-redirect-uri", + } as unknown as IRedirectURIInput)} + > + + ${redirectUriHelp} + { @state() showClientSecret = true; + @state() + redirectUris: RedirectURI[] = []; + + static get styles() { + return super.styles.concat(css` + ak-array-input { + width: 100%; + } + `); + } + async loadInstance(pk: number): Promise { const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({ id: pk, }); this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential; + this.redirectUris = provider.redirectUris; return provider; } @@ -203,13 +222,24 @@ export class OAuth2ProviderFormPage extends BaseProviderForm { ?hidden=${!this.showClientSecret} > - - + ({ matchingMode: MatchingModeEnum.Strict, url: "" })} + .row=${(f?: RedirectURI) => + akOAuthRedirectURIInput({ + ".redirectURI": f, + "style": "width: 100%", + "name": "oauth2-redirect-uri", + } as unknown as IRedirectURIInput)} + > + + ${redirectUriHelp} + diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts b/web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts new file mode 100644 index 000000000000..324f5e97f6be --- /dev/null +++ b/web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts @@ -0,0 +1,104 @@ +import "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI"; +import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; +import { type Spread } from "@goauthentik/elements/types"; +import { spread } from "@open-wc/lit-helpers"; + +import { msg } from "@lit/localize"; +import { css, html } from "lit"; +import { customElement, property, queryAll } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { MatchingModeEnum, RedirectURI } from "@goauthentik/api"; + +export interface IRedirectURIInput { + redirectURI: RedirectURI; +} + +@customElement("ak-provider-oauth2-redirect-uri") +export class OAuth2ProviderRedirectURI extends AkControlElement { + static get styles() { + return [ + PFBase, + PFInputGroup, + PFFormControl, + css` + .pf-c-input-group select { + width: 10em; + } + `, + ]; + } + + @property({ type: Object, attribute: false }) + redirectURI: RedirectURI = { + matchingMode: MatchingModeEnum.Strict, + url: "", + }; + + @queryAll(".ak-form-control") + controls?: HTMLInputElement[]; + + json() { + return Object.fromEntries( + Array.from(this.controls ?? []).map((control) => [control.name, control.value]), + ) as unknown as RedirectURI; + } + + get isValid() { + return true; + } + + render() { + const onChange = () => { + this.dispatchEvent(new Event("change", { composed: true, bubbles: true })); + }; + + return html`
+ + +
`; + } +} + +export function akOAuthRedirectURIInput(properties: IRedirectURIInput) { + return html``; +} + +declare global { + interface HTMLElementTagNameMap { + "ak-provider-oauth2-redirect-uri": OAuth2ProviderRedirectURI; + } +} diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts b/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts index 6bba6b3bccc3..d1245b4e9449 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts @@ -234,7 +234,11 @@ export class OAuth2ProviderViewPage extends AKElement {
- ${this.provider.redirectUris} +
    + ${this.provider.redirectUris.map((ru) => { + return html`
  • ${ru.matchingMode}: ${ru.url}
  • `; + })} +
diff --git a/web/src/admin/providers/proxy/ProxyProviderViewPage.ts b/web/src/admin/providers/proxy/ProxyProviderViewPage.ts index bf68b9ba1ba1..c7e42a32288f 100644 --- a/web/src/admin/providers/proxy/ProxyProviderViewPage.ts +++ b/web/src/admin/providers/proxy/ProxyProviderViewPage.ts @@ -392,9 +392,13 @@ export class ProxyProviderViewPage extends AKElement {
    - ${this.provider.redirectUris.split("\n").map((url) => { - return html`
  • ${url}
  • `; - })} +
      + ${this.provider.redirectUris.map((ru) => { + return html`
    • + ${ru.matchingMode}: ${ru.url} +
    • `; + })} +
diff --git a/web/src/elements/forms/HorizontalFormElement.ts b/web/src/elements/forms/HorizontalFormElement.ts index 5e4b9bcc6eb4..8d995a48e75e 100644 --- a/web/src/elements/forms/HorizontalFormElement.ts +++ b/web/src/elements/forms/HorizontalFormElement.ts @@ -2,7 +2,7 @@ import { convertToSlug } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base"; import { FormGroup } from "@goauthentik/elements/forms/FormGroup"; -import { msg } from "@lit/localize"; +import { msg, str } from "@lit/localize"; import { CSSResult, css } from "lit"; import { TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; @@ -33,7 +33,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; * where the field isn't available for the user to view unless they explicitly request to be able * to see the content; otherwise, a dead password field is shown. There are 10 uses of this * feature. - * + * */ const isAkControl = (el: unknown): boolean => @@ -86,7 +86,7 @@ export class HorizontalFormElement extends AKElement { writeOnlyActivated = false; @property({ attribute: false }) - errorMessages: string[] = []; + errorMessages: string[] | string[][] = []; @property({ type: Boolean }) slugMode = false; @@ -183,6 +183,16 @@ export class HorizontalFormElement extends AKElement {

` : html``} ${this.errorMessages.map((message) => { + if (message instanceof Object) { + return html`${Object.entries(message).map(([field, errMsg]) => { + 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. One hint, 'New Application Wizard', is currently hidden - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Deny message @@ -7053,6 +7050,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/en.xlf b/web/xliff/en.xlf index 54696cee1804..2370065535e8 100644 --- a/web/xliff/en.xlf +++ b/web/xliff/en.xlf @@ -6006,9 +6006,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Deny message @@ -7318,6 +7315,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/es.xlf b/web/xliff/es.xlf index 12a221348376..bb7c9719405d 100644 --- a/web/xliff/es.xlf +++ b/web/xliff/es.xlf @@ -5658,9 +5658,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Deny message @@ -6970,6 +6967,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/fr.xlf b/web/xliff/fr.xlf index 4c0342d8305c..18bbf9b75307 100644 --- a/web/xliff/fr.xlf +++ b/web/xliff/fr.xlf @@ -7542,10 +7542,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti One hint, 'New Application Wizard', is currently hidden Un indice, l'assistant nouvelle application est actuellement caché - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Applications externes qui utilisent authentik comme fournisseur d'identité, en utilisant des protocoles comme OAuth2 et SAML. Toutes les applications sont affichées ici, même celles auxquelles vous n'avez pas accès. - Deny message Message de refus @@ -9278,6 +9274,9 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/it.xlf b/web/xliff/it.xlf index 89edb3181aa8..53c2b6aa7660 100644 --- a/web/xliff/it.xlf +++ b/web/xliff/it.xlf @@ -7498,10 +7498,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden Un suggerimento, "New Application Wizard", è attualmente nascosto - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Applicazioni esterne che utilizzano Authenk come fornitore di identità tramite protocolli come OAuth2 e SAML. Tutte le applicazioni sono mostrate qui, anche quelle a cui non è possibile accedere. - Deny message Negare il messaggio @@ -9251,6 +9247,9 @@ Bindings to groups/users are checked against the user of the event. Require administrators to provide a reason for impersonating a user. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/ko.xlf b/web/xliff/ko.xlf index d68416773523..eeff3cd93d64 100644 --- a/web/xliff/ko.xlf +++ b/web/xliff/ko.xlf @@ -7512,10 +7512,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden 힌트, '새 애플리케이션 마법사'는 현재, 숨겨져 있습니다. - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - OAuth2 및 SAML과 같은 프로토콜을 통해 인증서를 ID 공급자로 사용하는 외부 애플리케이션. 액세스할 수 없는 애플리케이션을 포함한 모든 애플리케이션이 여기에 표시됩니다. - Deny message 거부 메시지 @@ -8885,6 +8881,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/nl.xlf b/web/xliff/nl.xlf index e3a18c7a761b..925a2908c5cf 100644 --- a/web/xliff/nl.xlf +++ b/web/xliff/nl.xlf @@ -7495,9 +7495,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de Sync currently running. - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Also known as Client ID. @@ -8732,6 +8729,9 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/pl.xlf b/web/xliff/pl.xlf index 37fce6b4671f..e90145830c25 100644 --- a/web/xliff/pl.xlf +++ b/web/xliff/pl.xlf @@ -7546,10 +7546,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz One hint, 'New Application Wizard', is currently hidden Jedna podpowiedź, „Kreator nowej aplikacji”, jest obecnie ukryty - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Aplikacje zewnętrzne, które używają authentik jako dostawcy tożsamości za pośrednictwem protokołów takich jak OAuth2 i SAML. Tutaj wyświetlane są wszystkie aplikacje, nawet te, do których nie masz dostępu. - Deny message Komunikat odmowy @@ -9148,6 +9144,9 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/pseudo-LOCALE.xlf b/web/xliff/pseudo-LOCALE.xlf index 230b4e989946..7c23fbcdf301 100644 --- a/web/xliff/pseudo-LOCALE.xlf +++ b/web/xliff/pseudo-LOCALE.xlf @@ -7490,10 +7490,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden Ōńē ĥĩńţ, 'Ńēŵ Àƥƥĺĩćàţĩōń Ŵĩźàŕď', ĩś ćũŕŕēńţĺŷ ĥĩďďēń - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Ēxţēŕńàĺ àƥƥĺĩćàţĩōńś ţĥàţ ũśē àũţĥēńţĩķ àś àń ĩďēńţĩţŷ ƥŕōvĩďēŕ vĩà ƥŕōţōćōĺś ĺĩķē ŌÀũţĥ2 àńď ŚÀḾĹ. Àĺĺ àƥƥĺĩćàţĩōńś àŕē śĥōŵń ĥēŕē, ēvēń ōńēś ŷōũ ćàńńōţ àććēśś. - Deny message Ďēńŷ ḿēśśàĝē @@ -9188,4 +9184,7 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. + diff --git a/web/xliff/ru.xlf b/web/xliff/ru.xlf index f3773aada11c..e908501995d8 100644 --- a/web/xliff/ru.xlf +++ b/web/xliff/ru.xlf @@ -7545,10 +7545,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden Одна подсказка, "Мастер создания нового приложения", в настоящее время скрыта - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Внешние приложения, использующие authentik в качестве поставщика идентификационных данных по таким протоколам, как OAuth2 и SAML. Здесь показаны все приложения, даже те, к которым вы не можете получить доступ. - Deny message Запретить сообщение @@ -9211,6 +9207,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/tr.xlf b/web/xliff/tr.xlf index 67239023afc1..2678b2330323 100644 --- a/web/xliff/tr.xlf +++ b/web/xliff/tr.xlf @@ -7495,10 +7495,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar One hint, 'New Application Wizard', is currently hidden Bir ipucu, 'Yeni Uygulama Sihirbazı' şu anda gizli - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - OAuth2 ve SAML gibi protokoller aracılığıyla kimlik sağlayıcı olarak authentik kullanan harici uygulamalar. Erişemedikleriniz de dahil olmak üzere tüm uygulamalar burada gösterilir. - Deny message İletiyi reddet @@ -9241,6 +9237,9 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/zh-CN.xlf b/web/xliff/zh-CN.xlf index 2ddc6c0d4dec..ab014442eb19 100644 --- a/web/xliff/zh-CN.xlf +++ b/web/xliff/zh-CN.xlf @@ -1989,9 +1989,6 @@ doesn't pass when either or both of the selected options are equal or above the Applications - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Provider Type @@ -5902,6 +5899,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. + diff --git a/web/xliff/zh-Hans.xlf b/web/xliff/zh-Hans.xlf index 159caf7b1813..ff8837a2d98d 100644 --- a/web/xliff/zh-Hans.xlf +++ b/web/xliff/zh-Hans.xlf @@ -1,4 +1,4 @@ - + @@ -596,9 +596,9 @@ - The URL "" was not found. - 未找到 URL " - "。 + The URL "" was not found. + 未找到 URL " + "。 @@ -1030,8 +1030,8 @@ - To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. - 要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。 + To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. + 要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。 @@ -1752,8 +1752,8 @@ - Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". - 输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。 + Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". + 输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。 @@ -2916,8 +2916,8 @@ doesn't pass when either or both of the selected options are equal or above the - Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' - 包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...' + Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' + 包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...' @@ -3663,8 +3663,8 @@ doesn't pass when either or both of the selected options are equal or above the - When using an external logging solution for archiving, this can be set to "minutes=5". - 使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。 + When using an external logging solution for archiving, this can be set to "minutes=5". + 使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。 @@ -3840,10 +3840,10 @@ doesn't pass when either or both of the selected options are equal or above the - Are you sure you want to update ""? + Are you sure you want to update ""? 您确定要更新 - " - " 吗? + " + " 吗? @@ -4919,7 +4919,7 @@ doesn't pass when either or both of the selected options are equal or above the - A "roaming" authenticator, like a YubiKey + A "roaming" authenticator, like a YubiKey 像 YubiKey 这样的“漫游”身份验证器 @@ -5298,7 +5298,7 @@ doesn't pass when either or both of the selected options are equal or above the - If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. + If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. 如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。 @@ -7544,10 +7544,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden “新应用程序向导”提示目前已隐藏 - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - 通过 OAuth2 和 SAML 等协议,使用 authentik 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。 - Deny message 拒绝消息 @@ -7713,7 +7709,7 @@ Bindings to groups/users are checked against the user of the event. 成功创建用户并添加到组 - This user will be added to the group "". + This user will be added to the group "". 此用户将会被添加到组 &quot;&quot;。 @@ -9063,7 +9059,7 @@ Bindings to groups/users are checked against the user of the event. 同步组 - ("", of type ) + ("", of type ) (&quot;&quot;,类型为 @@ -9297,7 +9293,10 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. 此选项配置流程执行器页面上的页脚链接。URL 限为 Web 和电子邮件地址。如果名称留空,则显示 URL 自身。 + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - \ No newline at end of file + diff --git a/web/xliff/zh-Hant.xlf b/web/xliff/zh-Hant.xlf index 48c245a8618a..5a272c47657a 100644 --- a/web/xliff/zh-Hant.xlf +++ b/web/xliff/zh-Hant.xlf @@ -5699,9 +5699,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - Deny message @@ -7011,6 +7008,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/web/xliff/zh_TW.xlf b/web/xliff/zh_TW.xlf index 4f6a84215a58..d020a2ba0fe3 100644 --- a/web/xliff/zh_TW.xlf +++ b/web/xliff/zh_TW.xlf @@ -7486,10 +7486,6 @@ Bindings to groups/users are checked against the user of the event. One hint, 'New Application Wizard', is currently hidden 提示:「新增應用程式設定精靈」目前處於隱藏中 - - External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. - 使用 authentik 作為身份供應商的外部應用程式,透過像 OAuth2 和 SAML 這樣的協議。此處顯示所有應用程式,即使是您無法存取的應用程式也包括在內。 - Deny message 拒絕的訊息 @@ -8846,6 +8842,9 @@ Bindings to groups/users are checked against the user of the event. This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown. + + + External applications that use as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access. diff --git a/website/docs/developer-docs/releases/index.md b/website/docs/developer-docs/releases/index.md index 7cf300e51d54..8d076af56a66 100644 --- a/website/docs/developer-docs/releases/index.md +++ b/website/docs/developer-docs/releases/index.md @@ -78,7 +78,7 @@ Short summary of the issue ### Patches -authentik x, y and z fix this issue, for other versions the workaround can be used. +authentik x, y and z fix this issue, for other versions the workaround below can be used. ### Impact @@ -96,7 +96,7 @@ Describe a workaround if possible If you have any questions or comments about this advisory: -- Email us at [security@goauthentik.io](mailto:security@goauthentik.io) +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io). ``` diff --git a/website/docs/releases/2024/v2024.10.md b/website/docs/releases/2024/v2024.10.md index d8edae393c2e..e7f3a8190b03 100644 --- a/website/docs/releases/2024/v2024.10.md +++ b/website/docs/releases/2024/v2024.10.md @@ -157,6 +157,30 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.10 - stages/password: use recovery flow from brand (cherry-pick #11953) (#11969) - web: bump API Client version (#11992) +## Fixed in 2024.10.3 + +- core: fix source_flow_manager throwing error when authenticated user attempts to re-authenticate with existing link (cherry-pick #12080) (#12081) +- internal: add CSP header to files in `/media` (cherry-pick #12092) (#12108) +- providers/ldap: fix global search_full_directory permission not being sufficient (cherry-pick #12028) (#12030) +- providers/scim: accept string and int for SCIM IDs (cherry-pick #12093) (#12095) +- rbac: fix incorrect object_description for object-level permissions (cherry-pick #12029) (#12043) +- root: check remote IP for proxy protocol same as HTTP/etc (cherry-pick #12094) (#12097) +- root: fix activation of locale not being scoped (cherry-pick #12091) (#12096) +- security: fix [CVE-2024-52287](../../security/cves/CVE-2024-52287.md), reported by [@matt1097](https://github.com/matt1097) (#12117) +- security: fix [CVE-2024-52289](../../security/cves/CVE-2024-52289.md), reported by [@PontusHanssen](https://github.com/PontusHanssen) (#12113) +- security: fix [CVE-2024-52307](../../security/cves/CVE-2024-52307.md), reported by [@mgerstner](https://github.com/mgerstner) (#12115) +- web/admin: better footer links (#12004) +- web/flows: fix invisible captcha call (cherry-pick #12048) (#12049) +- website/docs: add CSP to hardening (cherry-pick #11970) (#12116) + +## Fixed in 2024.10.4 + +- providers/oauth2: fix migration (cherry-pick #12138) (#12139) +- providers/oauth2: fix migration dependencies (cherry-pick #12123) (#12132) +- providers/oauth2: fix redirect uri input (cherry-pick #12122) (#12127) +- providers/proxy: fix redirect_uri (cherry-pick #12121) (#12125) +- web: bump API Client version (cherry-pick #12129) (#12130) + ## API Changes ### API Changes in 2024.10.0 diff --git a/website/docs/releases/2024/v2024.8.md b/website/docs/releases/2024/v2024.8.md index 2544f8da07e1..1e51d2f0af3f 100644 --- a/website/docs/releases/2024/v2024.8.md +++ b/website/docs/releases/2024/v2024.8.md @@ -300,6 +300,21 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.8 - web/admin: fix invalid create date shown for MFA registered before date was saved (cherry-pick #11728) (#11729) - web/admin: fix sync single button throwing error (cherry-pick #11727) (#11730) +## Fixed in 2024.8.5 + +- security: fix [CVE-2024-52287](../../security/cves/CVE-2024-52287.md), reported by [@matt1097](https://github.com/matt1097) (#12114) +- security: fix [CVE-2024-52289](../../security/cves/CVE-2024-52289.md), reported by [@PontusHanssen](https://github.com/PontusHanssen) (#12113) +- security: fix [CVE-2024-52307](../../security/cves/CVE-2024-52307.md), reported by [@mgerstner](https://github.com/mgerstner) (#12115) +- web/admin: better footer links (#12004) +- web: bump API Client version (#12118) + +## Fixed in 2024.8.6 + +- providers/oauth2: fix migration (cherry-pick #12138) (#12140) +- providers/oauth2: fix redirect uri input (cherry-pick #12122) (#12128) +- providers/proxy: fix redirect_uri (cherry-pick #12121) (#12126) +- web: bump API Client version (cherry-pick #12129) (#12131) + ## API Changes #### What's New diff --git a/website/docs/security/cves/CVE-2024-52287.md b/website/docs/security/cves/CVE-2024-52287.md new file mode 100644 index 000000000000..05852e9f765f --- /dev/null +++ b/website/docs/security/cves/CVE-2024-52287.md @@ -0,0 +1,27 @@ +# CVE-2024-52287 + +_Reported by [@matt1097](https://github.com/matt1097)_ + +## Insufficient validation of OAuth scopes for client_credentials and device_code grants + +### Summary + +When using the `client_credentials` or `device_code` OAuth grants, it was possible for an attacker to get a token from authentik with scopes that haven't been configured in authentik. + +### Details + +With the `device_code` grant, it was possible to have a user authorize a set of permitted scopes, and then acquire a token with a different set of scopes, including scopes not configured. This token could potentially be used to send requests to another system which trusts tokens signed by authentik and execute malicious actions on behalf of the user. + +With the `client_credentials` grant, because there is no user authorization process, authentik would not validate the scopes requested for the token, allowing tokens to be issued with scopes not configured in authentik. These could similarly be used to execute malicious actions in other systems. + +There is no workaround for this issue; however this issue could only be exploited if an attacker possesses a valid set of OAuth2 `client_id` and `client_secret` credentials, and has the knowledge of another system that trusts tokens issued by authentik and what scopes it checks for. + +### Patches + +authentik 2024.8.5 and 2024.10.3 fix this issue. + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io) diff --git a/website/docs/security/cves/CVE-2024-52289.md b/website/docs/security/cves/CVE-2024-52289.md new file mode 100644 index 000000000000..c9443d64c93e --- /dev/null +++ b/website/docs/security/cves/CVE-2024-52289.md @@ -0,0 +1,30 @@ +# CVE-2024-52289 + +_Reported by [@PontusHanssen](https://github.com/PontusHanssen)_ + +## Insecure default configuration for OAuth2 Redirect URIs + +### Summary + +Redirect URIs in the OAuth2 provider in authentik are checked by RegEx comparison. +When no Redirect URIs are configured in a provider, authentik will automatically use the first `redirect_uri` value received as an allowed redirect URI, without escaping characters that have a special meaning in RegEx. Similarly, the documentation did not take this into consideration either. + +Given a provider with the Redirect URIs set to `https://foo.example.com`, an attacker can register a domain `fooaexample.com`, and it will correctly pass validation. + +### Patches + +authentik 2024.8.5 and 2024.10.3 fix this issue. + +The patched versions remedy this issue by changing the format that the Redirect URIs are saved in, allowing for the explicit configuration if the URL should be checked strictly or as a RegEx. This means that these patches include a backwards-incompatible database change and API change. + +Manual action _is required_ if any provider is intended to use RegEx for Redirect URIs because the migration will set the comparison type to strict for every Redirect URI. + +### Workarounds + +When configuring OAuth2 providers, make sure to escape any wildcard characters that are not intended to function as a wildcard, for example replace `.` with `\.`. + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io) diff --git a/website/docs/security/cves/CVE-2024-52307.md b/website/docs/security/cves/CVE-2024-52307.md new file mode 100644 index 000000000000..978d7cc015e9 --- /dev/null +++ b/website/docs/security/cves/CVE-2024-52307.md @@ -0,0 +1,36 @@ +# CVE-2024-52307 + +_Reported by [@mgerstner](https://github.com/mgerstner)_ + +## Timing attack due to a lack of constant time comparison for metrics view + +### Summary + +Due to the usage of a non-constant time comparison for the `/-/metrics/` endpoint it was possible to brute-force the `SECRET_KEY`, which is used to authenticate the endpoint. The `/-/metrics/` endpoint returns Prometheus metrics and is not intended to be accessed directly, as the Go proxy running in the authentik server container fetches data from this endpoint and serves it on a separate port (9300 by default), which can be scraped by Prometheus without being exposed publicly. + +### Patches + +authentik 2024.8.5 and 2024.10.3 fix this issue, for other versions the workaround below can be used. + +### Impact + +With enough attempts the `SECRET_KEY` of the authentik installation can be brute-forced, which can be used to sign new or modify existing cookies. + +### Workarounds + +Since the `/-/metrics/` endpoint is not intended to be accessed publicly, requests to the endpoint can be blocked by the reverse proxy/load balancer used in conjunction with authentik. + +For example for nginx: + +``` +location /-/metrics/ { + deny all; + return 404; +} +``` + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io). diff --git a/website/docs/security/security-hardening.md b/website/docs/security/security-hardening.md index a705e4ca2da3..6279c4f5636c 100644 --- a/website/docs/security/security-hardening.md +++ b/website/docs/security/security-hardening.md @@ -47,3 +47,32 @@ To prevent any user from creating/editing CAPTCHA stages block API requests to t - `/api/v3/managed/blueprints*` With these restrictions in place, CAPTCHA stages can only be edited using [Blueprints on the file system](../customize/blueprints/index.md#storage---file). + +### Content Security Policy (CSP) + +:::caution +Setting up CSP incorrectly might result in the client not loading necessary third-party code. +::: + +:::caution +In some cases, a CSP header will already be set by authentik (for example, in [user uploaded content](https://github.com/goauthentik/authentik/pull/12092/)). Do not overwrite an already existing header as doing so might result in vulnerabilities. Instead, add a new CSP header. +::: + +Content Security Policy (CSP) is a security standard that mitigates the risk of content injection vulnerabilities. authentik doesn't currently support CSP natively, so setting it up depends on your installation. We recommend using a [reverse proxy](../install-config/reverse-proxy.md) to set a CSP header. + +authentik requires at least the following allowed locations: + +``` +default-src 'self'; +img-src 'https:' 'http:' 'data:'; +object-src 'none'; +style-src 'self' 'unsafe-inline'; # Required due to Lit/ShadowDOM +script-src 'self' 'unsafe-inline'; # Required for generated scripts +``` + +Your use case might require more allowed locations for various directives, e.g. + +- when using a CAPTCHA service +- when using Sentry +- when using any custom JavaScript in a prompt stage +- when using Spotlight Sidecar for development diff --git a/website/docs/sys-mgmt/settings.md b/website/docs/sys-mgmt/settings.md index 0c794590958c..5173cfe11f02 100644 --- a/website/docs/sys-mgmt/settings.md +++ b/website/docs/sys-mgmt/settings.md @@ -35,7 +35,7 @@ Enable the ability for users to change their Email address, defaults to `false`. ### Allow users to change username -Enable the ability for users to change their Usernames, defaults to `false`. +Enable the ability for users to change their usernames, defaults to `false`. ### Event retention @@ -43,15 +43,11 @@ Configure how long [Events](./events/index.md) are retained for within authentik ### Footer links -This option configures the footer links on the flow executor pages. +This option allows you to add linked text (footer links) on the bottom of flow pages. You can also use this setting to display additional static text to the flow pages, even if no URL is provided. -The setting can be used as follows: +The URL is limited to web and email addresses. If the name is left blank, the URL will be shown. -```json -[{ "name": "Link Name", "href": "https://goauthentik.io" }] -``` - -Starting with authentik 2024.6.1, the `href` attribute is optional, and this option can be used to add additional text to the flow executor pages. +This is a global setting. All flow pages that are rendered by the [Flow Executor](../add-secure-apps/flows-stages/flow/executors/if-flow.md) will display the footer links. ### GDPR compliance diff --git a/website/sidebars.js b/website/sidebars.js index 0f71dac56a40..7a3f51adc5d4 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -654,20 +654,41 @@ export default { type: "category", label: "CVEs", items: [ - "security/cves/CVE-2024-47077", - "security/cves/CVE-2024-47070", - "security/cves/CVE-2024-38371", - "security/cves/CVE-2024-37905", - "security/cves/CVE-2024-23647", - "security/cves/CVE-2024-21637", - "security/cves/CVE-2023-48228", - "security/cves/GHSA-rjvp-29xq-f62w", - "security/cves/CVE-2023-39522", - "security/cves/CVE-2023-36456", - "security/cves/CVE-2023-26481", - "security/cves/CVE-2022-23555", - "security/cves/CVE-2022-46145", - "security/cves/CVE-2022-46172", + { + type: "category", + label: "2024", + items: [ + "security/cves/CVE-2024-52307", + "security/cves/CVE-2024-52289", + "security/cves/CVE-2024-52287", + "security/cves/CVE-2024-47077", + "security/cves/CVE-2024-47070", + "security/cves/CVE-2024-38371", + "security/cves/CVE-2024-37905", + "security/cves/CVE-2024-23647", + "security/cves/CVE-2024-21637", + ], + }, + { + type: "category", + label: "2023", + items: [ + "security/cves/CVE-2023-48228", + "security/cves/GHSA-rjvp-29xq-f62w", + "security/cves/CVE-2023-39522", + "security/cves/CVE-2023-36456", + "security/cves/CVE-2023-26481", + ], + }, + { + type: "category", + label: "2022", + items: [ + "security/cves/CVE-2022-23555", + "security/cves/CVE-2022-46145", + "security/cves/CVE-2022-46172", + ], + }, ], }, ],