Skip to content

Commit

Permalink
Merge branch 'main' into web/flow/disambiguate-brand-links
Browse files Browse the repository at this point in the history
* main: (23 commits)
  website/docs: update info about footer links to match new UI (#12120)
  website/docs: prepare release notes (#12142)
  providers/oauth2: fix migration (#12138)
  providers/oauth2: fix migration dependencies (#12123)
  web: bump API Client version (#12129)
  providers/oauth2: fix redirect uri input (#12122)
  providers/proxy: fix redirect_uri (#12121)
  website/docs: prepare release notes (#12119)
  web: bump API Client version (#12118)
  security: fix CVE 2024 52289 (#12113)
  security: fix CVE 2024 52307 (#12115)
  security: fix CVE 2024 52287 (#12114)
  website/docs: add CSP to hardening (#11970)
  core: bump uvicorn from 0.32.0 to 0.32.1 (#12103)
  core: bump google-api-python-client from 2.153.0 to 2.154.0 (#12104)
  core: bump pydantic from 2.9.2 to 2.10.0 (#12105)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#12110)
  internal: add CSP header to files in `/media` (#12092)
  core, web: update translations (#12101)
  web: fix bug that prevented error reporting in current wizard. (#12033)
  ...
  • Loading branch information
kensternberg-authentik committed Nov 21, 2024
2 parents c3b33fd + 1c5d612 commit 7edc630
Show file tree
Hide file tree
Showing 81 changed files with 1,381 additions and 504 deletions.
8 changes: 5 additions & 3 deletions authentik/brands/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.utils.translation import activate
from django.utils.translation import override

from authentik.brands.utils import get_brand_for_request

Expand All @@ -18,10 +18,12 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response

def __call__(self, request: HttpRequest) -> HttpResponse:
locale_to_set = None
if not hasattr(request, "brand"):
brand = get_brand_for_request(request)
request.brand = brand
locale = brand.default_locale
if locale != "":
activate(locale)
return self.get_response(request)
locale_to_set = locale
with override(locale_to_set):
return self.get_response(request)
8 changes: 5 additions & 3 deletions authentik/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from uuid import uuid4

from django.http import HttpRequest, HttpResponse
from django.utils.translation import activate
from django.utils.translation import override
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX

Expand All @@ -31,17 +31,19 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
def __call__(self, request: HttpRequest) -> HttpResponse:
# No permission checks are done here, they need to be checked before
# SESSION_KEY_IMPERSONATE_USER is set.
locale_to_set = None
if request.user.is_authenticated:
locale = request.user.locale(request)
if locale != "":
activate(locale)
locale_to_set = locale

if SESSION_KEY_IMPERSONATE_USER in request.session:
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
# Ensure that the user is active, otherwise nothing will work
request.user.is_active = True

return self.get_response(request)
with override(locale_to_set):
return self.get_response(request)


class RequestIDMiddleware:
Expand Down
4 changes: 2 additions & 2 deletions authentik/core/tests/test_applications_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.saml.models import SAMLProvider

Expand All @@ -24,7 +24,7 @@ def setUp(self) -> None:
self.user = create_test_admin_user()
self.provider = OAuth2Provider.objects.create(
name="test",
redirect_uris="http://some-other-domain",
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
authorization_flow=create_test_flow(),
)
self.allowed: Application = Application.objects.create(
Expand Down
3 changes: 3 additions & 0 deletions authentik/core/tests/test_transactional_applications_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def test_create_transactional(self):
"name": uid,
"authorization_flow": str(create_test_flow().pk),
"invalidation_flow": str(create_test_flow().pk),
"redirect_uris": [],
},
},
)
Expand Down Expand Up @@ -89,6 +90,7 @@ def test_create_transactional_bindings(self):
"name": uid,
"authorization_flow": str(authorization_flow.pk),
"invalidation_flow": str(authorization_flow.pk),
"redirect_uris": [],
},
"policy_bindings": [{"group": group.pk, "order": 0}],
},
Expand Down Expand Up @@ -120,6 +122,7 @@ def test_create_transactional_invalid(self):
"name": uid,
"authorization_flow": "",
"invalidation_flow": "",
"redirect_uris": [],
},
},
)
Expand Down
6 changes: 3 additions & 3 deletions authentik/crypto/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id, generate_key
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode


class TestCrypto(APITestCase):
Expand Down Expand Up @@ -274,7 +274,7 @@ def test_used_by(self):
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=keypair,
)
response = self.client.get(
Expand Down Expand Up @@ -306,7 +306,7 @@ def test_used_by_denied(self):
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
signing_key=keypair,
)
response = self.client.get(
Expand Down
34 changes: 31 additions & 3 deletions authentik/providers/oauth2/api/providers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
"""OAuth2Provider API Views"""

from copy import copy
from re import compile
from re import error as RegexError

from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
from rest_framework.fields import CharField, ChoiceField
from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response
Expand All @@ -20,13 +23,39 @@
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
from authentik.core.models import Provider
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.models import (
AccessToken,
OAuth2Provider,
RedirectURIMatchingMode,
ScopeMapping,
)
from authentik.rbac.decorators import permission_required


class RedirectURISerializer(PassiveSerializer):
"""A single allowed redirect URI entry"""

matching_mode = ChoiceField(choices=RedirectURIMatchingMode.choices)
url = CharField()


class OAuth2ProviderSerializer(ProviderSerializer):
"""OAuth2Provider Serializer"""

redirect_uris = RedirectURISerializer(many=True, source="_redirect_uris")

def validate_redirect_uris(self, data: list) -> list:
for entry in data:
if entry.get("matching_mode") == RedirectURIMatchingMode.REGEX:
url = entry.get("url")
try:
compile(url)
except RegexError:
raise ValidationError(
_("Invalid Regex Pattern: {url}".format(url=url))
) from None
return data

class Meta:
model = OAuth2Provider
fields = ProviderSerializer.Meta.fields + [
Expand Down Expand Up @@ -79,7 +108,6 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
"refresh_token_validity",
"include_claims_in_id_token",
"signing_key",
"redirect_uris",
"sub_mode",
"property_mappings",
"issuer_mode",
Expand Down
6 changes: 3 additions & 3 deletions authentik/providers/oauth2/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from authentik.events.models import Event, EventAction
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.views import bad_request_message
from authentik.providers.oauth2.models import GrantTypes
from authentik.providers.oauth2.models import GrantTypes, RedirectURI


class OAuth2Error(SentryIgnoredException):
Expand Down Expand Up @@ -46,9 +46,9 @@ class RedirectUriError(OAuth2Error):
)

provided_uri: str
allowed_uris: list[str]
allowed_uris: list[RedirectURI]

def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None:
def __init__(self, provided_uri: str, allowed_uris: list[RedirectURI]) -> None:
super().__init__()
self.provided_uri = provided_uri
self.allowed_uris = allowed_uris
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def migrate_session(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
class Migration(migrations.Migration):

dependencies = [
("authentik_core", "0040_provider_invalidation_flow"),
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
("authentik_providers_oauth2", "0021_oauth2provider_encryption_key_and_more"),
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class Migration(migrations.Migration):

dependencies = [
("authentik_core", "0040_provider_invalidation_flow"),
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
("authentik_providers_oauth2", "0022_remove_accesstoken_session_id_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 5.0.9 on 2024-11-04 12:56
from dataclasses import asdict
from django.apps.registry import Apps

from django.db.backends.base.schema import BaseDatabaseSchemaEditor

from django.db import migrations, models


def migrate_redirect_uris(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.providers.oauth2.models import RedirectURI, RedirectURIMatchingMode

OAuth2Provider = apps.get_model("authentik_providers_oauth2", "oauth2provider")

db_alias = schema_editor.connection.alias
for provider in OAuth2Provider.objects.using(db_alias).all():
uris = []
for old in provider.old_redirect_uris.split("\n"):
mode = RedirectURIMatchingMode.STRICT
if old == "*" or old == ".*":
mode = RedirectURIMatchingMode.REGEX
uris.append(asdict(RedirectURI(mode, url=old)))
provider._redirect_uris = uris
provider.save()


class Migration(migrations.Migration):

dependencies = [
("authentik_providers_oauth2", "0023_alter_accesstoken_refreshtoken_use_hash_index"),
]

operations = [
migrations.RenameField(
model_name="oauth2provider",
old_name="redirect_uris",
new_name="old_redirect_uris",
),
migrations.AddField(
model_name="oauth2provider",
name="_redirect_uris",
field=models.JSONField(default=dict, verbose_name="Redirect URIs"),
),
migrations.RunPython(migrate_redirect_uris, lambda *args: ...),
migrations.RemoveField(
model_name="oauth2provider",
name="old_redirect_uris",
),
]
52 changes: 43 additions & 9 deletions authentik/providers/oauth2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import base64
import binascii
import json
from dataclasses import asdict
from dataclasses import asdict, dataclass
from functools import cached_property
from hashlib import sha256
from typing import Any
Expand All @@ -12,6 +12,7 @@
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from dacite import Config
from dacite.core import from_dict
from django.contrib.postgres.indexes import HashIndex
from django.db import models
Expand Down Expand Up @@ -77,11 +78,25 @@ class IssuerMode(models.TextChoices):
"""Configure how the `iss` field is created."""

GLOBAL = "global", _("Same identifier is used for all providers")
PER_PROVIDER = "per_provider", _(
"Each provider has a different issuer, based on the application slug."
PER_PROVIDER = (
"per_provider",
_("Each provider has a different issuer, based on the application slug."),
)


class RedirectURIMatchingMode(models.TextChoices):
STRICT = "strict", _("Strict URL comparison")
REGEX = "regex", _("Regular Expression URL matching")


@dataclass
class RedirectURI:
"""A single redirect URI entry"""

matching_mode: RedirectURIMatchingMode
url: str


class ResponseTypes(models.TextChoices):
"""Response Type required by the client."""

Expand Down Expand Up @@ -156,11 +171,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
verbose_name=_("Client Secret"),
default=generate_client_secret,
)
redirect_uris = models.TextField(
default="",
blank=True,
_redirect_uris = models.JSONField(
default=dict,
verbose_name=_("Redirect URIs"),
help_text=_("Enter each URI on a new line."),
)

include_claims_in_id_token = models.BooleanField(
Expand Down Expand Up @@ -271,12 +284,33 @@ def get_issuer(self, request: HttpRequest) -> str | None:
except Provider.application.RelatedObjectDoesNotExist:
return None

@property
def redirect_uris(self) -> list[RedirectURI]:
uris = []
for entry in self._redirect_uris:
uris.append(
from_dict(
RedirectURI,
entry,
config=Config(type_hooks={RedirectURIMatchingMode: RedirectURIMatchingMode}),
)
)
return uris

@redirect_uris.setter
def redirect_uris(self, value: list[RedirectURI]):
cleansed = []
for entry in value:
cleansed.append(asdict(entry))
self._redirect_uris = cleansed

@property
def launch_url(self) -> str | None:
"""Guess launch_url based on first redirect_uri"""
if self.redirect_uris == "":
redirects = self.redirect_uris
if len(redirects) < 1:
return None
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
main_url = redirects[0].url
try:
launch_url = urlparse(main_url)._replace(path="")
return urlunparse(launch_url)
Expand Down
Loading

0 comments on commit 7edc630

Please sign in to comment.