Compare commits

..

10 Commits

Author SHA1 Message Date
1a21479b0d release: 2024.4.4 2024-08-22 17:38:56 +02:00
38154f72e0 rbac: check user type correctly
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-22 17:38:52 +02:00
19318d4c00 security: fix CVE-2024-42490 (cherry-pick #11022) (#11024)
security: fix CVE-2024-42490 (#11022)

CVE-2024-42490

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-22 17:18:55 +02:00
be3d7c0666 website/docs: update 2024.4 release notes with latest changes (cherry-pick #10231) (#10244)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-26 16:56:28 +00:00
5afceaa55f release: 2024.4.3 2024-06-26 19:36:51 +09:00
72dc27f1c9 security: fix CVE-2024-37905 (cherry-pick #10230) (#10236)
Co-authored-by: Jens L <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix CVE-2024-37905 (#10230)
2024-06-26 10:24:15 +00:00
b5ffd16861 security: fix CVE-2024-38371 (cherry-pick #10229) (#10233)
Co-authored-by: Jens L <jens@goauthentik.io>
fix CVE-2024-38371 (#10229)
2024-06-26 09:42:57 +00:00
8af754e88c sources/saml: fix FlowPlanner error due to pickle (cherry-pick #9708) (#9709)
sources/saml: fix FlowPlanner error due to pickle (#9708)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-13 21:04:12 +02:00
ade1f08c89 web: fix value handling inside controlled components (cherry-pick #9648) (#9685)
web: fix value handling inside controlled components (#9648)

* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: fix value handling inside controlled components

This is one of those stupid bugs that drive web developers crazy. The basics are straightforward:
when you cause a higher-level component to have a "big enough re-render," for some unknown
definition of "big enough," it will re-render the sub-components. In traditional web interaction,
those components should never be re-rendered while the user is interacting with the form, but in
frameworks where there's dynamic re-arrangement, part or all of the form could get re-rendered at
any mmoment. Since neither the form nor any of its intermediaries is tracking the values as they're
changed, it's up to the components themselves to keep the user's input-- and to be hardened against
property changes coming from the outside world.

So static memoization of the initial value passed in, and aggressively walling off the values the
customer generates from that field, are needed to protect the user's work from any framework's
dynamic DOM management. I remember struggling with this in React; I had hoped Lit was better, but in
this case, not better enough.

The protocol for "is it an ak-data-control" is "it has a `json()` method that returns the data ready
to be sent to the authentik server."  I missed that in one place, so that's on me.

* Eslint had opinions.

* Added comments to explain something.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-05-13 12:26:20 +02:00
9240fa1037 core: fix source flow_manager not always appending save stage (cherry-pick #9659) (#9662)
core: fix source flow_manager not always appending save stage (#9659)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-09 20:13:53 +02:00
31 changed files with 366 additions and 118 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.4.2
current_version = 2024.4.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.4.2"
__version__ = "2024.4.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -45,6 +45,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")

View File

@ -14,6 +14,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
from authentik.rbac.filters import ObjectFilter
class DeleteAction(Enum):
@ -53,7 +54,7 @@ class UsedByMixin:
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
model: Model = self.get_object()

View File

@ -13,7 +13,7 @@ from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import Source, SourceUserMatchingModes, User, UserSourceConnection
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostUserEnrollmentStage
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostSourceStage
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
@ -206,13 +206,9 @@ class SourceFlowManager:
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
"""Hook to override stages which are appended to the flow"""
if not self.source.enrollment_flow:
return []
if flow.slug == self.source.enrollment_flow.slug:
return [
in_memory_stage(PostUserEnrollmentStage),
]
return []
return [
in_memory_stage(PostSourceStage),
]
def _prepare_flow(
self,
@ -266,6 +262,9 @@ class SourceFlowManager:
)
# We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow)
# We append some stages so the initial flow we get might be empty
planner.allow_empty_flows = True
planner.use_cache = False
plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
@ -324,7 +323,7 @@ class SourceFlowManager:
reverse(
"authentik_core:if-user",
)
+ f"#/settings;page-{self.source.slug}"
+ "#/settings;page-sources"
)
def handle_enroll(

View File

@ -10,7 +10,7 @@ from authentik.flows.stage import StageView
PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection"
class PostUserEnrollmentStage(StageView):
class PostSourceStage(StageView):
"""Dynamically injected stage which saves the Connection after
the user has been enrolled."""
@ -21,10 +21,12 @@ class PostUserEnrollmentStage(StageView):
]
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
connection.user = user
linked = connection.pk is None
connection.save()
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",
source=connection.source,
).from_http(self.request)
if linked:
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",
source=connection.source,
).from_http(self.request)
return self.executor.stage_ok()

View File

@ -2,11 +2,15 @@
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase
from django.urls import reverse
from guardian.utils import get_anonymous_user
from authentik.core.models import SourceUserMatchingModes, User
from authentik.core.sources.flow_manager import Action
from authentik.core.sources.stage import PostSourceStage
from authentik.core.tests.utils import create_test_flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.policies.denied import AccessDeniedResponse
@ -21,41 +25,55 @@ class TestSourceFlowManager(TestCase):
def setUp(self) -> None:
super().setUp()
self.source: OAuthSource = OAuthSource.objects.create(name="test")
self.authentication_flow = create_test_flow()
self.enrollment_flow = create_test_flow()
self.source: OAuthSource = OAuthSource.objects.create(
name=generate_id(),
slug=generate_id(),
authentication_flow=self.authentication_flow,
enrollment_flow=self.enrollment_flow,
)
self.identifier = generate_id()
def test_unauthenticated_enroll(self):
"""Test un-authenticated user enrolling"""
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)
flow_manager.get_flow()
response = flow_manager.get_flow()
self.assertEqual(response.status_code, 302)
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
self.assertEqual(flow_plan.bindings[0].stage.view, PostSourceStage)
def test_unauthenticated_auth(self):
"""Test un-authenticated user authenticating"""
UserOAuthSourceConnection.objects.create(
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.AUTH)
flow_manager.get_flow()
response = flow_manager.get_flow()
self.assertEqual(response.status_code, 302)
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
self.assertEqual(flow_plan.bindings[0].stage.view, PostSourceStage)
def test_authenticated_link(self):
"""Test authenticated user linking"""
user = User.objects.create(username="foo", email="foo@bar.baz")
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=user), self.identifier, {}
)
request = get_request("/", user=user)
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
flow_manager.get_flow()
response = flow_manager.get_flow()
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url,
reverse("authentik_core:if-user") + "#/settings;page-sources",
)
def test_unauthenticated_link(self):
"""Test un-authenticated user linking"""

View File

@ -13,9 +13,8 @@ from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
)
from authentik.core.tests.utils import create_test_admin_user
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
@ -24,7 +23,7 @@ class TestTokenAPI(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="testuser")
self.user = create_test_user()
self.admin = create_test_admin_user()
self.client.force_login(self.user)
@ -154,6 +153,24 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
def test_token_change_user(self):
"""Test creating a token and then changing the user"""
ident = generate_id()
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier=ident)
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
response = self.client.put(
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
)
self.assertEqual(response.status_code, 400)
token.refresh_from_db()
self.assertEqual(token.user, self.user)
def test_list(self):
"""Test Token List (Test normal authentication)"""
Token.objects.all().delete()

View File

@ -36,6 +36,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -266,7 +267,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def view_certificate(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs certificate and log access"""
certificate: CertificateKeyPair = self.get_object()
@ -296,7 +297,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def view_private_key(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs private key and log access"""
certificate: CertificateKeyPair = self.get_object()

View File

@ -214,6 +214,46 @@ class TestCrypto(APITestCase):
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_certificate_download_denied(self):
"""Test certificate export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)
def test_private_key_download_denied(self):
"""Test private_key export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)
def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(create_test_admin_user())
@ -246,6 +286,26 @@ class TestCrypto(APITestCase):
],
)
def test_used_by_denied(self):
"""Test used_by endpoint"""
self.client.logout()
keypair = create_test_cert()
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
signing_key=keypair,
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
def test_discovery(self):
"""Test certificate discovery"""
name = generate_id()

View File

@ -33,6 +33,7 @@ from authentik.lib.utils.file import (
)
from authentik.lib.views import bad_request_message
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -277,7 +278,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
400: OpenApiResponse(description="Flow not applicable"),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def execute(self, request: Request, slug: str):
"""Execute flow for current user"""
# Because we pre-plan the flow here, and not in the planner, we need to manually clear

View File

@ -203,7 +203,8 @@ class FlowPlanner:
"f(plan): building plan",
)
plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
if self.use_cache:
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
if not plan.bindings and not self.allow_empty_flows:
raise EmptyFlowException()
return plan

View File

@ -23,6 +23,7 @@ from authentik.outposts.models import (
KubernetesServiceConnection,
OutpostServiceConnection,
)
from authentik.rbac.filters import ObjectFilter
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
@ -88,7 +89,7 @@ class ServiceConnectionViewSet(
return Response(TypeCreateSerializer(data, many=True).data)
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def state(self, request: Request, pk: str) -> Response:
"""Get the service connection's state"""
connection = self.get_object()

View File

@ -4,9 +4,10 @@ from urllib.parse import urlencode
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.models import Application, Group
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
@ -77,3 +78,23 @@ class TesOAuth2DeviceInit(OAuthTestCase):
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code}),
)
def test_device_init_denied(self):
"""Test device init"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.application,
order=0,
)
token = DeviceToken.objects.create(
user_code="foo",
provider=self.provider,
)
res = self.client.get(
reverse("authentik_providers_oauth2_root:device-login")
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code})
)
self.assertEqual(res.status_code, 200)
self.assertIn(b"Permission denied", res.content)

View File

@ -11,10 +11,11 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework.throttling import AnonRateThrottle
from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
LOGGER = get_logger()
@ -37,7 +38,9 @@ class DeviceView(View):
).first()
if not provider:
return HttpResponseBadRequest()
if not get_application(provider):
try:
_ = provider.application
except Application.DoesNotExist:
return HttpResponseBadRequest()
self.provider = provider
self.client_id = client_id

View File

@ -1,8 +1,9 @@
"""Device flow views"""
from typing import Any
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, IntegerField
from structlog.stdlib import get_logger
@ -16,7 +17,8 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.policies.views import PolicyAccessView
from authentik.providers.oauth2.models import DeviceToken
from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE,
OAuthDeviceCodeFinishStage,
@ -31,60 +33,52 @@ LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec
def get_application(provider: OAuth2Provider) -> Application | None:
"""Get application from provider"""
try:
app = provider.application
if not app:
class CodeValidatorView(PolicyAccessView):
"""Helper to validate frontside token"""
def __init__(self, code: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.code = code
def resolve_provider_application(self):
self.token = DeviceToken.objects.filter(user_code=self.code).first()
if not self.token:
raise Application.DoesNotExist
self.provider = self.token.provider
self.application = self.token.provider.application
def get(self, request: HttpRequest, *args, **kwargs):
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: self.token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
return app
except Application.DoesNotExist:
return None
def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
"""Validate user token"""
token = DeviceToken.objects.filter(
user_code=code,
).first()
if not token:
return None
app = get_application(token.provider)
if not app:
return None
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: app,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.token.provider.authorization_flow.slug,
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=token.provider.authorization_flow.slug,
)
class DeviceEntryView(View):
class DeviceEntryView(PolicyAccessView):
"""View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
@ -94,7 +88,9 @@ class DeviceEntryView(View):
LOGGER.info("Brand has no device code flow configured", brand=brand)
return HttpResponse(status=404)
if QS_KEY_CODE in request.GET:
validation = validate_code(request.GET[QS_KEY_CODE], request)
validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
request
)
if validation:
return validation
LOGGER.info("Got code from query parameter but no matching token found")
@ -131,7 +127,7 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
def validate_code(self, code: int) -> HttpResponse | None:
"""Validate code and save the returned http response"""
response = validate_code(code, self.stage.request)
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
if not response:
raise ValidationError(_("Invalid code"), "invalid")
return response

View File

@ -25,7 +25,7 @@ class ObjectFilter(ObjectPermissionsFilter):
# Outposts (which are the only objects using internal service accounts)
# except requests to return an empty list when they have no objects
# assigned
if request.user.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
if getattr(request.user, "type", UserTypes.INTERNAL) == UserTypes.INTERNAL_SERVICE_ACCOUNT:
return queryset
if not queryset.exists():
# User doesn't have direct permission to all objects

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.4.2 Blueprint schema",
"title": "authentik 2024.4.4 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.4}
restart: unless-stopped
command: server
environment:
@ -53,7 +53,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.4}
restart: unless-stopped
command: worker
environment:

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2024.4.2"
const VERSION = "2024.4.4"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.4.2"
version = "2024.4.4"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.4.2
version: 2024.4.4
description: Making authentication simple.
contact:
email: hello@goauthentik.io

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.4.2";
export const VERSION = "2024.4.4";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -2,8 +2,9 @@ import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { PropertyValues } from "@lit/reactive-element/reactive-element";
import { TemplateResult, css, html } from "lit";
import { customElement, property, queryAll } from "lit/decorators.js";
import { customElement, property, queryAll, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
@ -112,10 +113,14 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
@queryAll('input[type="checkbox"]')
checkboxes!: NodeListOf<HTMLInputElement>;
internals?: ElementInternals;
@state()
values: string[] = [];
get json() {
return this.value;
internals?: ElementInternals;
doneFirstUpdate = false;
json() {
return this.values;
}
private get formValue() {
@ -124,7 +129,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
}
const name = this.name;
const entries = new FormData();
this.value.forEach((v) => entries.append(name, v));
this.values.forEach((v) => entries.append(name, v));
return entries;
}
@ -136,14 +141,14 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
onClick(ev: Event) {
ev.stopPropagation();
this.value = Array.from(this.checkboxes)
this.values = Array.from(this.checkboxes)
.filter((checkbox) => checkbox.checked)
.map((checkbox) => checkbox.name);
this.dispatchCustomEvent("change", this.value);
this.dispatchCustomEvent("input", this.value);
this.dispatchCustomEvent("change", this.values);
this.dispatchCustomEvent("input", this.values);
if (this.internals) {
this.internals.setValidity({});
if (this.required && this.value.length === 0) {
if (this.required && this.values.length === 0) {
this.internals.setValidity(
{
valueMissing: true,
@ -154,6 +159,16 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
}
this.internals.setFormValue(this.formValue);
}
// Doing a write-back so anyone examining the checkbox.value field will get something
// meaningful. Doesn't do anything for anyone, usually, but it's nice to have.
this.value = this.values;
}
willUpdate(changed: PropertyValues<this>) {
if (changed.has("value") && !this.doneFirstUpdate) {
this.doneFirstUpdate = true;
this.values = this.value;
}
}
connectedCallback() {
@ -183,7 +198,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
render() {
const renderOne = ([name, label]: CheckboxPr) => {
const selected = this.value.includes(name);
const selected = this.values.includes(name);
const blockFwd = (e: Event) => {
e.stopImmediatePropagation();
};

View File

@ -53,6 +53,9 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
private isLoading = false;
private doneFirstUpdate = false;
private internalSelected: DualSelectPair[] = [];
private pagination?: Pagination;
constructor() {
@ -69,6 +72,11 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
}
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected") && !this.doneFirstUpdate) {
this.doneFirstUpdate = true;
this.internalSelected = this.selected;
}
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(
AkDualSelectProvider.prototype.doSearch.bind(this),
@ -105,7 +113,8 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.selected = event.detail.value;
this.internalSelected = event.detail.value;
this.selected = this.internalSelected;
}
onSearch(event: Event) {
@ -124,12 +133,16 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
return this.dualSelector.value!.selected.map(([k, _]) => k);
}
json() {
return this.value;
}
render() {
return html`<ak-dual-select
${ref(this.dualSelector)}
.options=${this.options}
.pages=${this.pagination}
.selected=${this.selected}
.selected=${this.internalSelected}
available-label=${this.availableLabel}
selected-label=${this.selectedLabel}
></ak-dual-select>`;

View File

@ -80,7 +80,7 @@ export function serializeForm<T extends KeyUnknown>(
}
if ("akControl" in inputElement.dataset) {
assignValue(element, inputElement.value, json);
assignValue(element, (inputElement as unknown as AkControlElement).json(), json);
return;
}

View File

@ -235,6 +235,14 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.4
- web/flows: fix missing fallback for flow logo (cherry-pick #9487) (#9492)
- web: Add missing integrity hashes to package-lock.json (#9527)
## Fixed in 2024.4.3
- core: fix source flow_manager not always appending save stage (cherry-pick #9659) (#9662)
- security: fix [CVE-2024-37905](../../security/CVE-2024-37905.md), reported by [@m2a2](https://github.com/m2a2) (cherry-pick #10230) (#10236)
- security: fix [CVE-2024-38371](../../security/CVE-2024-38371.md), reported by Stefan Zwanenburg (cherry-pick #10229) (#10233)
- sources/saml: fix FlowPlanner error due to pickle (cherry-pick #9708) (#9709)
- web: fix value handling inside controlled components (cherry-pick #9648) (#9685)
## API Changes
#### What's New

View File

@ -0,0 +1,27 @@
# CVE-2024-37905
_Reported by [@m2a2](https://github.com/m2a2)_
## Improper Authorization for Token modification
### Summary
Due to insufficient permission checks it was possible for any authenticated user to elevate their permissions to a superuser by creating an API token and changing the user the token belonged to.
### Patches
authentik 2024.6.0, 2024.4.3 and 2024.2.4 fix this issue, for other versions the workaround can be used.
### Details
By setting a token's user ID to the ID of a higher privileged user, the token will inherit the higher privileged access to the API. This can be used to change the password of the affected user or to modify the authentik configuration in a potentially malicious way.
### Workarounds
As a workaround it is possible to block any requests to `/api/v3/core/tokens*` at the reverse-proxy/load-balancer level. Doing so prevents this issue from being exploited.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -0,0 +1,23 @@
# CVE-2024-38371
_Reported by Stefan Zwanenburg_
## Insufficient access control for OAuth2 Device Code flow
### Impact
Due to a bug, access restrictions assigned to an application were not checked when using the OAuth2 Device code flow. This could potentially allow users without the correct authorization to get OAuth tokens for an application, and access the application.
### Patches
authentik 2024.6.0, 2024.4.3 and 2024.2.4 fix this issue, for other versions the workaround can be used.
### Workarounds
As authentik flows are still used as part of the OAuth2 Device code flow, it is possible to add access control to the configured flows.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -0,0 +1,31 @@
# CVE-2024-42490
_Reported by [@m2a2](https://github.com/m2a2)_
## Improper Authorization for Token modification
### Summary
Several API endpoints can be accessed by users without correct authentication/authorization.
The main API endpoints affected by this:
- `/api/v3/crypto/certificatekeypairs/<uuid>/view_certificate/`
- `/api/v3/crypto/certificatekeypairs/<uuid>/view_private_key/`
- `/api/v3/.../used_by/`
Note that all of the affected API endpoints require the knowledge of the ID of an object, which especially for certificates is not accessible to an unprivileged user. Additionally the IDs for most objects are UUIDv4, meaning they are not easily guessable/enumerable.
### Patches
authentik 2024.4.4, 2024.6.4 and 2024.8.0 fix this issue.
### Workarounds
Access to the API endpoints can be blocked at a Reverse-proxy/Load balancer level to prevent this issue from being exploited.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -436,6 +436,9 @@ const docsSidebar = {
},
items: [
"security/policy",
"security/CVE-2024-42490",
"security/CVE-2024-38371",
"security/CVE-2024-37905",
"security/CVE-2024-23647",
"security/CVE-2024-21637",
"security/CVE-2023-48228",