security: fix CVE 2024 52289 (#12113)
* initial migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix loading Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start dynamic ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * initial ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add serialize Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add error message handling Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix/add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prepare docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate to new input Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> # Conflicts: # authentik/core/tests/test_transactional_applications_api.py # authentik/providers/oauth2/tests/test_authorize.py # authentik/providers/oauth2/tests/test_jwks.py # authentik/providers/oauth2/tests/test_token.py # website/docs/security/CVE-2024-52289.md # website/sidebars.js
This commit is contained in:
@ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
|||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
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.proxy.models import ProxyProvider
|
||||||
from authentik.providers.saml.models import SAMLProvider
|
from authentik.providers.saml.models import SAMLProvider
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
redirect_uris="http://some-other-domain",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
)
|
)
|
||||||
self.allowed: Application = Application.objects.create(
|
self.allowed: Application = Application.objects.create(
|
||||||
|
@ -31,6 +31,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"provider": {
|
"provider": {
|
||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": str(authorization_flow.pk),
|
"authorization_flow": str(authorization_flow.pk),
|
||||||
|
"redirect_uris": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -56,6 +57,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"provider": {
|
"provider": {
|
||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": "",
|
"authorization_flow": "",
|
||||||
|
"redirect_uris": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,7 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
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):
|
class TestCrypto(APITestCase):
|
||||||
@ -263,7 +263,7 @@ class TestCrypto(APITestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=keypair,
|
signing_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@ -295,7 +295,7 @@ class TestCrypto(APITestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=keypair,
|
signing_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
"""OAuth2Provider API Views"""
|
"""OAuth2Provider API Views"""
|
||||||
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
from re import compile
|
||||||
|
from re import error as RegexError
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
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.generics import get_object_or_404
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -20,13 +23,39 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
|
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.providers.oauth2.id_token import IDToken
|
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
|
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):
|
class OAuth2ProviderSerializer(ProviderSerializer):
|
||||||
"""OAuth2Provider Serializer"""
|
"""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:
|
class Meta:
|
||||||
model = OAuth2Provider
|
model = OAuth2Provider
|
||||||
fields = ProviderSerializer.Meta.fields + [
|
fields = ProviderSerializer.Meta.fields + [
|
||||||
@ -78,7 +107,6 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"refresh_token_validity",
|
"refresh_token_validity",
|
||||||
"include_claims_in_id_token",
|
"include_claims_in_id_token",
|
||||||
"signing_key",
|
"signing_key",
|
||||||
"redirect_uris",
|
|
||||||
"sub_mode",
|
"sub_mode",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"issuer_mode",
|
"issuer_mode",
|
||||||
|
@ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.views import bad_request_message
|
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):
|
class OAuth2Error(SentryIgnoredException):
|
||||||
@ -46,9 +46,9 @@ class RedirectUriError(OAuth2Error):
|
|||||||
)
|
)
|
||||||
|
|
||||||
provided_uri: str
|
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__()
|
super().__init__()
|
||||||
self.provided_uri = provided_uri
|
self.provided_uri = provided_uri
|
||||||
self.allowed_uris = allowed_uris
|
self.allowed_uris = allowed_uris
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-04 12:56
|
||||||
|
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(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",
|
||||||
|
),
|
||||||
|
]
|
@ -3,7 +3,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import json
|
import json
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict, dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -12,6 +12,7 @@ from urllib.parse import urlparse, urlunparse
|
|||||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||||
|
from dacite import Config
|
||||||
from dacite.core import from_dict
|
from dacite.core import from_dict
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -67,11 +68,25 @@ class IssuerMode(models.TextChoices):
|
|||||||
"""Configure how the `iss` field is created."""
|
"""Configure how the `iss` field is created."""
|
||||||
|
|
||||||
GLOBAL = "global", _("Same identifier is used for all providers")
|
GLOBAL = "global", _("Same identifier is used for all providers")
|
||||||
PER_PROVIDER = "per_provider", _(
|
PER_PROVIDER = (
|
||||||
"Each provider has a different issuer, based on the application slug."
|
"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):
|
class ResponseTypes(models.TextChoices):
|
||||||
"""Response Type required by the client."""
|
"""Response Type required by the client."""
|
||||||
|
|
||||||
@ -146,11 +161,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
|||||||
verbose_name=_("Client Secret"),
|
verbose_name=_("Client Secret"),
|
||||||
default=generate_client_secret,
|
default=generate_client_secret,
|
||||||
)
|
)
|
||||||
redirect_uris = models.TextField(
|
_redirect_uris = models.JSONField(
|
||||||
default="",
|
default=dict,
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Redirect URIs"),
|
verbose_name=_("Redirect URIs"),
|
||||||
help_text=_("Enter each URI on a new line."),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
include_claims_in_id_token = models.BooleanField(
|
include_claims_in_id_token = models.BooleanField(
|
||||||
@ -251,12 +264,33 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
|||||||
except Provider.application.RelatedObjectDoesNotExist:
|
except Provider.application.RelatedObjectDoesNotExist:
|
||||||
return None
|
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
|
@property
|
||||||
def launch_url(self) -> str | None:
|
def launch_url(self) -> str | None:
|
||||||
"""Guess launch_url based on first redirect_uri"""
|
"""Guess launch_url based on first redirect_uri"""
|
||||||
if self.redirect_uris == "":
|
redirects = self.redirect_uris
|
||||||
|
if len(redirects) < 1:
|
||||||
return None
|
return None
|
||||||
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
|
main_url = redirects[0].url
|
||||||
try:
|
try:
|
||||||
launch_url = urlparse(main_url)._replace(path="")
|
launch_url = urlparse(main_url)._replace(path="")
|
||||||
return urlunparse(launch_url)
|
return urlunparse(launch_url)
|
||||||
|
@ -10,7 +10,13 @@ from rest_framework.test import APITestCase
|
|||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
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):
|
class TestAPI(APITestCase):
|
||||||
@ -21,7 +27,7 @@ class TestAPI(APITestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
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.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
|
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
|
||||||
@ -50,9 +56,29 @@ class TestAPI(APITestCase):
|
|||||||
@skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up")
|
@skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up")
|
||||||
def test_launch_url(self):
|
def test_launch_url(self):
|
||||||
"""Test launch_url"""
|
"""Test launch_url"""
|
||||||
self.provider.redirect_uris = (
|
self.provider.redirect_uris = [
|
||||||
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/\n"
|
RedirectURI(
|
||||||
)
|
RedirectURIMatchingMode.REGEX,
|
||||||
|
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/",
|
||||||
|
),
|
||||||
|
]
|
||||||
self.provider.save()
|
self.provider.save()
|
||||||
self.provider.refresh_from_db()
|
self.provider.refresh_from_db()
|
||||||
self.assertIsNone(self.provider.launch_url)
|
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)
|
||||||
|
@ -19,6 +19,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
GrantTypes,
|
GrantTypes,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
@ -39,7 +41,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid/Foo",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(AuthorizeError):
|
with self.assertRaises(AuthorizeError):
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
@ -64,7 +66,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid/Foo",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(AuthorizeError):
|
with self.assertRaises(AuthorizeError):
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
@ -84,7 +86,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -106,7 +108,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="data:local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
@ -125,7 +127,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -140,7 +142,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
)
|
)
|
||||||
OAuthAuthorizationParams.from_request(request)
|
OAuthAuthorizationParams.from_request(request)
|
||||||
provider.refresh_from_db()
|
provider.refresh_from_db()
|
||||||
self.assertEqual(provider.redirect_uris, "+")
|
self.assertEqual(provider.redirect_uris, [RedirectURI(RedirectURIMatchingMode.STRICT, "+")])
|
||||||
|
|
||||||
def test_invalid_redirect_uri_regex(self):
|
def test_invalid_redirect_uri_regex(self):
|
||||||
"""test missing/invalid redirect URI"""
|
"""test missing/invalid redirect URI"""
|
||||||
@ -148,7 +150,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid?",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -170,7 +172,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="+",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -213,7 +215,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid/Foo",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -301,7 +303,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -343,7 +345,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -419,7 +421,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -474,7 +476,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -532,7 +534,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||||
|
@ -11,7 +11,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.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +30,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.app = Application.objects.create(
|
self.app = Application.objects.create(
|
||||||
@ -118,7 +125,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
|
@ -13,7 +13,7 @@ from authentik.core.tests.utils import create_test_cert, create_test_flow
|
|||||||
from authentik.crypto.builder import PrivateKeyAlg
|
from authentik.crypto.builder import PrivateKeyAlg
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.generators import generate_id
|
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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
TEST_CORDS_CERT = """
|
TEST_CORDS_CERT = """
|
||||||
@ -49,7 +49,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
@ -68,7 +68,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
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)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@ -82,7 +82,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
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),
|
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
@ -104,7 +104,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=cert,
|
signing_key=cert,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
|
@ -10,7 +10,14 @@ from django.utils import timezone
|
|||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
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.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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +29,7 @@ class TesOAuth2Revoke(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.app = Application.objects.create(
|
self.app = Application.objects.create(
|
||||||
|
@ -22,6 +22,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
AccessToken,
|
AccessToken,
|
||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
@ -42,7 +44,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://TestServer",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
@ -69,7 +71,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
@ -90,7 +92,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
@ -118,7 +120,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
# Needs to be assigned to an application for iss to be set
|
# Needs to be assigned to an application for iss to be set
|
||||||
@ -158,7 +160,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -220,7 +222,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -278,7 +280,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
|
@ -19,7 +19,12 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
TOKEN_TYPE,
|
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.tests.utils import OAuthTestCase
|
||||||
from authentik.providers.oauth2.views.jwks import JWKSView
|
from authentik.providers.oauth2.views.jwks import JWKSView
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
@ -54,7 +59,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=self.cert,
|
signing_key=self.cert,
|
||||||
)
|
)
|
||||||
self.provider.jwks_sources.add(self.source)
|
self.provider.jwks_sources.add(self.source)
|
||||||
|
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.errors import TokenError
|
from authentik.providers.oauth2.errors import TokenError
|
||||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +39,7 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
@ -20,7 +20,12 @@ from authentik.providers.oauth2.constants import (
|
|||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.errors import TokenError
|
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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +39,7 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
@ -19,7 +19,12 @@ from authentik.providers.oauth2.constants import (
|
|||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.errors import TokenError
|
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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +38,7 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
@ -14,7 +14,14 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID,
|
SCOPE_OPENID,
|
||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import AccessToken, DeviceToken, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
DeviceToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +35,7 @@ class TestTokenDeviceCode(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
@ -10,7 +10,12 @@ from authentik.core.models import Application
|
|||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
|
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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -30,7 +35,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -93,7 +98,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -154,7 +159,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -210,7 +215,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
|
@ -11,7 +11,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.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.generators import generate_id
|
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
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +32,7 @@ class TestUserinfo(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
@ -57,6 +57,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
GrantTypes,
|
GrantTypes,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
ResponseMode,
|
ResponseMode,
|
||||||
ResponseTypes,
|
ResponseTypes,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
@ -188,40 +190,39 @@ class OAuthAuthorizationParams:
|
|||||||
|
|
||||||
def check_redirect_uri(self):
|
def check_redirect_uri(self):
|
||||||
"""Redirect URI validation."""
|
"""Redirect URI validation."""
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
allowed_redirect_urls = self.provider.redirect_uris
|
||||||
if not self.redirect_uri:
|
if not self.redirect_uri:
|
||||||
LOGGER.warning("Missing redirect uri.")
|
LOGGER.warning("Missing redirect uri.")
|
||||||
raise RedirectUriError("", allowed_redirect_urls)
|
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)
|
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()
|
self.provider.save()
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
allowed_redirect_urls = self.provider.redirect_uris
|
||||||
|
|
||||||
if self.provider.redirect_uris == "*":
|
match_found = False
|
||||||
LOGGER.info("Converting redirect_uris to regex", redirect=self.redirect_uri)
|
for allowed in allowed_redirect_urls:
|
||||||
self.provider.redirect_uris = ".*"
|
if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
|
||||||
self.provider.save()
|
if self.redirect_uri == allowed.url:
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
match_found = True
|
||||||
|
break
|
||||||
try:
|
if allowed.matching_mode == RedirectURIMatchingMode.REGEX:
|
||||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
try:
|
||||||
LOGGER.warning(
|
if fullmatch(allowed.url, self.redirect_uri):
|
||||||
"Invalid redirect uri (regex comparison)",
|
match_found = True
|
||||||
redirect_uri_given=self.redirect_uri,
|
break
|
||||||
redirect_uri_expected=allowed_redirect_urls,
|
except RegexError as exc:
|
||||||
)
|
LOGGER.warning(
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
"Failed to parse regular expression",
|
||||||
except RegexError as exc:
|
exc=exc,
|
||||||
LOGGER.info("Failed to parse regular expression, checking directly", exc=exc)
|
url=allowed.url,
|
||||||
if not any(x == self.redirect_uri for x in allowed_redirect_urls):
|
provider=self.provider,
|
||||||
LOGGER.warning(
|
)
|
||||||
"Invalid redirect uri (strict comparison)",
|
if not match_found:
|
||||||
redirect_uri_given=self.redirect_uri,
|
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||||
redirect_uri_expected=allowed_redirect_urls,
|
|
||||||
)
|
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) from None
|
|
||||||
# Check against forbidden schemes
|
# Check against forbidden schemes
|
||||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||||
|
@ -158,5 +158,5 @@ class ProviderInfoView(View):
|
|||||||
OAuth2Provider, pk=application.provider_id
|
OAuth2Provider, pk=application.provider_id
|
||||||
)
|
)
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
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
|
return response
|
||||||
|
@ -58,6 +58,7 @@ from authentik.providers.oauth2.models import (
|
|||||||
ClientTypes,
|
ClientTypes,
|
||||||
DeviceToken,
|
DeviceToken,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
@ -195,42 +196,7 @@ class TokenParams:
|
|||||||
LOGGER.warning("Missing authorization code")
|
LOGGER.warning("Missing authorization code")
|
||||||
raise TokenError("invalid_grant")
|
raise TokenError("invalid_grant")
|
||||||
|
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
self.__check_redirect_uri(request)
|
||||||
# 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.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
|
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
|
||||||
if not self.authorization_code:
|
if not self.authorization_code:
|
||||||
@ -270,6 +236,48 @@ class TokenParams:
|
|||||||
if not self.authorization_code.code_challenge and self.code_verifier:
|
if not self.authorization_code.code_challenge and self.code_verifier:
|
||||||
raise TokenError("invalid_grant")
|
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):
|
def __post_init_refresh(self, raw_token: str, request: HttpRequest):
|
||||||
if not raw_token:
|
if not raw_token:
|
||||||
LOGGER.warning("Missing refresh token")
|
LOGGER.warning("Missing refresh token")
|
||||||
@ -513,7 +521,7 @@ class TokenView(View):
|
|||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
allowed_origins = []
|
allowed_origins = []
|
||||||
if self.provider:
|
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)
|
cors_allow(self.request, response, *allowed_origins)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class UserInfoView(View):
|
|||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
allowed_origins = []
|
allowed_origins = []
|
||||||
if self.token:
|
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)
|
cors_allow(self.request, response, *allowed_origins)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -121,7 +121,6 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"basic_auth_password_attribute": ["iexact"],
|
"basic_auth_password_attribute": ["iexact"],
|
||||||
"basic_auth_user_attribute": ["iexact"],
|
"basic_auth_user_attribute": ["iexact"],
|
||||||
"mode": ["iexact"],
|
"mode": ["iexact"],
|
||||||
"redirect_uris": ["iexact"],
|
|
||||||
"cookie_domain": ["iexact"],
|
"cookie_domain": ["iexact"],
|
||||||
}
|
}
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
|
@ -13,7 +13,13 @@ from rest_framework.serializers import Serializer
|
|||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.models import DomainlessURLValidator
|
from authentik.lib.models import DomainlessURLValidator
|
||||||
from authentik.outposts.models import OutpostModel
|
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"
|
SCOPE_AK_PROXY = "ak_proxy"
|
||||||
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
|
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
|
||||||
@ -24,14 +30,15 @@ def get_cookie_secret():
|
|||||||
return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
|
|
||||||
|
|
||||||
def _get_callback_url(uri: str) -> str:
|
def _get_callback_url(uri: str) -> list[RedirectURI]:
|
||||||
return "\n".join(
|
return [
|
||||||
[
|
RedirectURI(
|
||||||
|
RedirectURIMatchingMode.STRICT,
|
||||||
urljoin(uri, "outpost.goauthentik.io/callback")
|
urljoin(uri, "outpost.goauthentik.io/callback")
|
||||||
+ f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
+ f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
||||||
uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
),
|
||||||
]
|
RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true"),
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProxyMode(models.TextChoices):
|
class ProxyMode(models.TextChoices):
|
||||||
|
@ -5345,9 +5345,30 @@
|
|||||||
"description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."
|
"description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."
|
||||||
},
|
},
|
||||||
"redirect_uris": {
|
"redirect_uris": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"title": "Redirect URIs",
|
"items": {
|
||||||
"description": "Enter each URI on a new line."
|
"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": {
|
"sub_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
53
schema.yml
53
schema.yml
@ -19493,10 +19493,6 @@ paths:
|
|||||||
format: uuid
|
format: uuid
|
||||||
explode: true
|
explode: true
|
||||||
style: form
|
style: form
|
||||||
- in: query
|
|
||||||
name: redirect_uris
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
- in: query
|
||||||
name: refresh_token_validity
|
name: refresh_token_validity
|
||||||
schema:
|
schema:
|
||||||
@ -19912,10 +19908,6 @@ paths:
|
|||||||
format: uuid
|
format: uuid
|
||||||
explode: true
|
explode: true
|
||||||
style: form
|
style: form
|
||||||
- in: query
|
|
||||||
name: redirect_uris__iexact
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: search
|
- name: search
|
||||||
required: false
|
required: false
|
||||||
in: query
|
in: query
|
||||||
@ -41665,6 +41657,11 @@ components:
|
|||||||
required:
|
required:
|
||||||
- challenge
|
- challenge
|
||||||
- name
|
- name
|
||||||
|
MatchingModeEnum:
|
||||||
|
enum:
|
||||||
|
- strict
|
||||||
|
- regex
|
||||||
|
type: string
|
||||||
Metadata:
|
Metadata:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for blueprint metadata
|
description: Serializer for blueprint metadata
|
||||||
@ -42353,8 +42350,9 @@ components:
|
|||||||
description: Key used to sign the tokens. Only required when JWT Algorithm
|
description: Key used to sign the tokens. Only required when JWT Algorithm
|
||||||
is set to RS256.
|
is set to RS256.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
description: Enter each URI on a new line.
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURI'
|
||||||
sub_mode:
|
sub_mode:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/SubModeEnum'
|
- $ref: '#/components/schemas/SubModeEnum'
|
||||||
@ -42382,6 +42380,7 @@ components:
|
|||||||
- meta_model_name
|
- meta_model_name
|
||||||
- name
|
- name
|
||||||
- pk
|
- pk
|
||||||
|
- redirect_uris
|
||||||
- verbose_name
|
- verbose_name
|
||||||
- verbose_name_plural
|
- verbose_name_plural
|
||||||
OAuth2ProviderRequest:
|
OAuth2ProviderRequest:
|
||||||
@ -42444,8 +42443,9 @@ components:
|
|||||||
description: Key used to sign the tokens. Only required when JWT Algorithm
|
description: Key used to sign the tokens. Only required when JWT Algorithm
|
||||||
is set to RS256.
|
is set to RS256.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
description: Enter each URI on a new line.
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURIRequest'
|
||||||
sub_mode:
|
sub_mode:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/SubModeEnum'
|
- $ref: '#/components/schemas/SubModeEnum'
|
||||||
@ -42466,6 +42466,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- name
|
- name
|
||||||
|
- redirect_uris
|
||||||
OAuth2ProviderSetupURLs:
|
OAuth2ProviderSetupURLs:
|
||||||
type: object
|
type: object
|
||||||
description: OAuth2 Provider Metadata serializer
|
description: OAuth2 Provider Metadata serializer
|
||||||
@ -46220,8 +46221,9 @@ components:
|
|||||||
description: Key used to sign the tokens. Only required when JWT Algorithm
|
description: Key used to sign the tokens. Only required when JWT Algorithm
|
||||||
is set to RS256.
|
is set to RS256.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
description: Enter each URI on a new line.
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURIRequest'
|
||||||
sub_mode:
|
sub_mode:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/SubModeEnum'
|
- $ref: '#/components/schemas/SubModeEnum'
|
||||||
@ -49357,6 +49359,29 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- to
|
- 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:
|
Reputation:
|
||||||
type: object
|
type: object
|
||||||
description: Reputation Serializer
|
description: Reputation Serializer
|
||||||
|
@ -13,7 +13,12 @@ from authentik.flows.models import Flow
|
|||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
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
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +84,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
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,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
Application.objects.create(
|
Application.objects.create(
|
||||||
@ -134,7 +141,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
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,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
@ -205,7 +214,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
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,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
|
@ -20,7 +20,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
SCOPE_OPENID_PROFILE,
|
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
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -87,7 +93,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:3000/",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/")],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -136,7 +142,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -198,7 +208,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -270,7 +284,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -350,7 +368,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
|
@ -21,7 +21,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
SCOPE_OPENID_PROFILE,
|
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
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -73,7 +79,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -122,7 +128,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -193,7 +201,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -264,7 +274,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
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(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
|
@ -21,7 +21,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
SCOPE_OPENID_PROFILE,
|
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
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +80,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -123,7 +129,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/implicit/",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -176,7 +184,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/implicit/",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -244,7 +254,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/implicit/",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||||
|
import {
|
||||||
|
IRedirectURIInput,
|
||||||
|
akOAuthRedirectURIInput,
|
||||||
|
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/components/ak-textarea-input";
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
|
import "@goauthentik/elements/ak-array-input.js";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
@ -15,7 +20,7 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, css, html } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
@ -23,8 +28,10 @@ import {
|
|||||||
ClientTypeEnum,
|
ClientTypeEnum,
|
||||||
FlowsInstancesListDesignationEnum,
|
FlowsInstancesListDesignationEnum,
|
||||||
IssuerModeEnum,
|
IssuerModeEnum,
|
||||||
|
MatchingModeEnum,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
|
RedirectURI,
|
||||||
SubModeEnum,
|
SubModeEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@ -98,13 +105,13 @@ export const issuerModeOptions = [
|
|||||||
|
|
||||||
const redirectUriHelpMessages = [
|
const redirectUriHelpMessages = [
|
||||||
msg(
|
msg(
|
||||||
"Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
||||||
),
|
),
|
||||||
msg(
|
msg(
|
||||||
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
||||||
),
|
),
|
||||||
msg(
|
msg(
|
||||||
'To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.',
|
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -124,11 +131,23 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
|||||||
@state()
|
@state()
|
||||||
showClientSecret = true;
|
showClientSecret = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
redirectUris: RedirectURI[] = [];
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return super.styles.concat(css`
|
||||||
|
ak-array-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
async loadInstance(pk: number): Promise<OAuth2Provider> {
|
async loadInstance(pk: number): Promise<OAuth2Provider> {
|
||||||
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
|
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
|
||||||
id: pk,
|
id: pk,
|
||||||
});
|
});
|
||||||
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
|
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
|
||||||
|
this.redirectUris = provider.redirectUris;
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,13 +235,23 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
|||||||
?hidden=${!this.showClientSecret}
|
?hidden=${!this.showClientSecret}
|
||||||
>
|
>
|
||||||
</ak-text-input>
|
</ak-text-input>
|
||||||
<ak-textarea-input
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Redirect URIs/Origins")}
|
||||||
|
required
|
||||||
name="redirectUris"
|
name="redirectUris"
|
||||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
|
||||||
.value=${provider?.redirectUris}
|
|
||||||
.bighelp=${redirectUriHelp}
|
|
||||||
>
|
>
|
||||||
</ak-textarea-input>
|
<ak-array-input
|
||||||
|
.items=${this.instance?.redirectUris ?? []}
|
||||||
|
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
|
||||||
|
.row=${(f?: RedirectURI) =>
|
||||||
|
akOAuthRedirectURIInput({
|
||||||
|
".redirectURI": f,
|
||||||
|
"style": "width: 100%",
|
||||||
|
} as unknown as IRedirectURIInput)}
|
||||||
|
>
|
||||||
|
</ak-array-input>
|
||||||
|
${redirectUriHelp}
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||||
|
104
web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts
Normal file
104
web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts
Normal file
@ -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<RedirectURI> {
|
||||||
|
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`<div class="pf-c-input-group">
|
||||||
|
<select
|
||||||
|
name="matchingMode"
|
||||||
|
class="pf-c-form-control ak-form-control"
|
||||||
|
@change=${onChange}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="${MatchingModeEnum.Strict}"
|
||||||
|
?selected=${this.redirectURI.matchingMode === MatchingModeEnum.Strict}
|
||||||
|
>
|
||||||
|
${msg("Strict")}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="${MatchingModeEnum.Regex}"
|
||||||
|
?selected=${this.redirectURI.matchingMode === MatchingModeEnum.Regex}
|
||||||
|
>
|
||||||
|
${msg("Regex")}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
@change=${onChange}
|
||||||
|
value="${ifDefined(this.redirectURI.url ?? undefined)}"
|
||||||
|
class="pf-c-form-control ak-form-control"
|
||||||
|
required
|
||||||
|
id="url"
|
||||||
|
placeholder=${msg("URL")}
|
||||||
|
name="href"
|
||||||
|
tabindex="1"
|
||||||
|
/>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function akOAuthRedirectURIInput(properties: IRedirectURIInput) {
|
||||||
|
return html`<ak-provider-oauth2-redirect-uri
|
||||||
|
${spread(properties as unknown as Spread)}
|
||||||
|
></ak-provider-oauth2-redirect-uri>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-provider-oauth2-redirect-uri": OAuth2ProviderRedirectURI;
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { convertToSlug } from "@goauthentik/common/utils";
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
|
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
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
|
* 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
|
* to see the content; otherwise, a dead password field is shown. There are 10 uses of this
|
||||||
* feature.
|
* feature.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isAkControl = (el: unknown): boolean =>
|
const isAkControl = (el: unknown): boolean =>
|
||||||
@ -86,7 +86,7 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
writeOnlyActivated = false;
|
writeOnlyActivated = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
errorMessages: string[] = [];
|
errorMessages: string[] | string[][] = [];
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
slugMode = false;
|
slugMode = false;
|
||||||
@ -183,6 +183,16 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
</p>`
|
</p>`
|
||||||
: html``}
|
: html``}
|
||||||
${this.errorMessages.map((message) => {
|
${this.errorMessages.map((message) => {
|
||||||
|
if (message instanceof Object) {
|
||||||
|
return html`${Object.entries(message).map(([field, errMsg]) => {
|
||||||
|
return html`<p
|
||||||
|
class="pf-c-form__helper-text pf-m-error"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
${msg(str`${field}: ${errMsg}`)}
|
||||||
|
</p>`;
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||||
${message}
|
${message}
|
||||||
</p>`;
|
</p>`;
|
||||||
|
30
website/docs/security/CVE-2024-52289.md
Normal file
30
website/docs/security/CVE-2024-52289.md
Normal file
@ -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)
|
@ -522,6 +522,7 @@ const docsSidebar = {
|
|||||||
"security/security-hardening",
|
"security/security-hardening",
|
||||||
"security/policy",
|
"security/policy",
|
||||||
"security/CVE-2024-52307",
|
"security/CVE-2024-52307",
|
||||||
|
"security/CVE-2024-52289",
|
||||||
"security/CVE-2024-52287",
|
"security/CVE-2024-52287",
|
||||||
"security/CVE-2024-47077",
|
"security/CVE-2024-47077",
|
||||||
"security/CVE-2024-47070",
|
"security/CVE-2024-47070",
|
||||||
|
Reference in New Issue
Block a user