Compare commits

..

29 Commits

Author SHA1 Message Date
8469213d82 release: 2024.6.5 2024-09-27 16:21:30 +02:00
78f7b04d5a security: fix CVE-2024-47070 (cherry-pick #11536) (#11540)
security: fix CVE-2024-47070 (#11536)

* security: fix CVE-2024-47070



* Update website/docs/security/CVE-2024-47070.md




---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-09-27 16:20:38 +02:00
22e586bd8c security: fix CVE-2024-47077 (cherry-pick #11535) (#11538)
security: fix CVE-2024-47077 (#11535)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-27 16:19:15 +02:00
8a0b31b922 release: 2024.6.4 2024-08-22 17:19:24 +02:00
359b343f51 security: fix CVE-2024-42490 (cherry-pick #11022) (#11025)
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:58 +02:00
b727656b05 sources/ldap: Add enabled filter for ldap_password_validate signal (cherry-pick #10823) (#10825)
sources/ldap: Add enabled filter for ldap_password_validate signal (#10823)

Co-authored-by: Allen <63997543+aaw3@users.noreply.github.com>
2024-08-08 14:23:44 +02:00
8f09c2c21c web/admin: fix selectable card colour in dark theme (cherry-pick #10794) (#10795)
web/admin: fix selectable card colour in dark theme (#10794)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-06 13:46:43 +02:00
8f207c7504 release: 2024.6.3 2024-08-05 18:35:33 +02:00
34d30bb549 root: fix opencontainers ref (#10776)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	poetry.lock
2024-08-05 16:30:54 +02:00
b4f04881e0 root: remove warnings (#10774)
* remove facebook sdk

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* switch to newer opencontainers fork

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	poetry.lock
2024-08-05 14:52:20 +02:00
5314485426 enterprise/rac: fix error when listing connection tokens as non-superuser (cherry-pick #10771) (#10773)
enterprise/rac: fix error when listing connection tokens as non-superuser (#10771)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-05 14:09:24 +02:00
ad6b6e4576 web: replace all occurences of the theme placeholder (cherry-pick #10749) (#10750)
web: replace all occurences of the theme placeholder (#10749)

Replace all occurences of the theme placeholder

This allows the placeholder to occur multiple times in the theme url.

Signed-off-by: Chasethechicken <neuringe1234@gmail.com>
Co-authored-by: Chasethechicken <neuringe1234@gmail.com>
2024-08-05 11:57:32 +02:00
fb9aa9d7f7 sources/scim: fix duplicate service account users and changing token (cherry-pick #10735) (#10737)
sources/scim: fix duplicate service account users and changing token (#10735)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-02 14:12:23 +02:00
fe7662f80d web: fix theme not applying to document correctly (cherry-pick #10721) (#10722)
web: fix theme not applying to document correctly (#10721)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-01 15:09:38 +02:00
d6904b6aa1 release: 2024.6.2 2024-07-31 16:54:24 +02:00
cd581efacd tests/e2e: fix ldap tests following #10270 (cherry-pick #10288) (#10703)
tests/e2e: fix ldap tests following #10270 (#10288)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-07-31 16:01:32 +02:00
6c159d120b outposts: ensure minimum refresh interval (cherry-pick #10701) (#10702)
outposts: ensure minimum refresh interval (#10701)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-31 14:59:14 +02:00
4ddd4e7f88 outposts: make refresh interval configurable (cherry-pick #10138) (#10700)
* outposts: make refresh interval configurable (#10138)

* outposts: make refresh interval configurable

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* frontend

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* black again

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* switch to using config attribute

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* bump api

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-07-31 14:38:09 +02:00
441912414f web/admin: show matching user reputation scores in user details (cherry-pick #10276) (#10699)
* web/admin: show matching user reputation scores in user details (#10276)

Co-authored-by: Jens Langhammer <jens@goauthentik.io>

* bump api

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-07-31 14:37:58 +02:00
9e177ed5c0 web: fix dark theme and theme switch (#10667)
* base locale off of ak-element

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* revert temp theme fixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix theme switching

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add basic support for theme-different images

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* sort outposts in card

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set default theme based on pre-hydrated brand settings

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* activate global theme before root in shadow dom

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* logging

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* when using _applyTheme, check media matcher

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/src/elements/Base.ts
#	website/docs/core/brands.md
2024-07-29 20:26:44 +02:00
881548176f events: associate login_failed events to a user if possible (cherry-pick #10270) (#10676)
* events: associate login_failed events to a user if possible (#10270)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-07-29 20:00:13 +02:00
56739d0dc4 web/flows: remove continue button from AutoSubmit stage (cherry-pick #10253) (#10677)
web/flows: remove continue button from AutoSubmit stage (#10253)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-29 19:32:29 +02:00
b23972e9c9 lifecycle: only create tenant media root if needed (cherry-pick #10616) (#10617)
lifecycle: only create tenant media root if needed (#10616)

Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 21:12:48 +02:00
0a9595089e web/admin: fix missing SAML Provider ECDSA options (cherry-pick #10612) (#10618)
web/admin: fix missing SAML Provider ECDSA options (#10612)

* web/admin: fix missing SAML Provider ECDSA options



* deduplicate



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 21:12:23 +02:00
72c22b5fab core: remove html language tag for pages that are translated (cherry-pick #10611) (#10613)
core: remove html language tag for pages that are translated (#10611)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 19:42:48 +02:00
84cdbb0a03 events: fix race condition (cherry-pick #10602) (#10609)
events: fix race condition (#10602)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 16:53:03 +02:00
9fc659f121 stages/prompt: fix prompt not editable with invalid expression (cherry-pick #10603) (#10604)
stages/prompt: fix prompt not editable with invalid expression (#10603)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 14:36:33 +02:00
db6abf61b8 lib/sync: handle SkipObject in direct triggered tasks (cherry-pick #10590) (#10591)
lib/sync: handle SkipObject in direct triggered tasks (#10590)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-23 15:38:37 +02:00
6426a1d177 core: improve error handling on ASGI level (cherry-pick #10547) (#10552)
core: improve error handling on ASGI level (#10547)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-19 17:19:29 +02:00
68 changed files with 593 additions and 195 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.6.1
current_version = 2024.6.5
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.6.1"
__version__ = "2024.6.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

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

@ -4,7 +4,7 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

View File

@ -35,6 +35,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()
@ -265,7 +266,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()
@ -295,7 +296,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

@ -34,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
]
class ConnectionTokenOwnerFilter(OwnerFilter):
"""Owner filter for connection tokens (checks session's user)"""
owner_key = "session__user"
class ConnectionTokenViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
@ -50,4 +56,9 @@ class ConnectionTokenViewSet(
search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
filter_backends = [
ConnectionTokenOwnerFilter,
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

View File

@ -5,7 +5,6 @@ from channels.sessions import CookieMiddleware
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from authentik.core.channels import TokenOutpostMiddleware
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
@ -13,6 +12,7 @@ from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -35,6 +35,7 @@ IGNORED_MODELS = tuple(
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None)
def should_log_model(model: Model) -> bool:
@ -149,11 +150,13 @@ class AuditMiddleware:
m2m_changed.disconnect(dispatch_uid=request.request_id)
def __call__(self, request: HttpRequest) -> HttpResponse:
_CTX_REQUEST.set(request)
self.connect(request)
response = self.get_response(request)
self.disconnect(request)
_CTX_REQUEST.set(None)
return response
def process_exception(self, request: HttpRequest, exception: Exception):
@ -167,7 +170,7 @@ class AuditMiddleware:
thread = EventNewThread(
EventAction.SUSPICIOUS_REQUEST,
request,
message=str(exception),
message=exception_to_string(exception),
)
thread.run()
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
@ -192,6 +195,8 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
@ -205,6 +210,8 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
EventNewThread(
@ -230,6 +237,8 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
EventNewThread(

View File

@ -238,6 +238,8 @@ class Event(SerializerModel, ExpiringModel):
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
}
if hasattr(request, "request_id"):
self.context["http_request"]["request_id"] = request.request_id
# Special case for events created during flow execution
# since they keep the http query within a wrapped query
if QS_QUERY in self.context["http_request"]["args"]:

View File

@ -75,7 +75,10 @@ def on_login_failed(
**kwargs,
):
"""Failed Login, authentik custom event"""
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(request)
user = User.objects.filter(username=credentials.get("username")).first()
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
request, user
)
@receiver(invitation_used)

View File

@ -37,6 +37,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()
@ -281,7 +282,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

@ -229,6 +229,8 @@ class SyncTasks:
client.delete(instance)
except TransientSyncException as exc:
raise Retry() from exc
except SkipObjectException:
continue
except StopSync as exc:
self.logger.warning(exc, provider_pk=provider.pk)
@ -259,5 +261,7 @@ class SyncTasks:
client.update_group(group, operation, pk_set)
except TransientSyncException as exc:
raise Retry() from exc
except SkipObjectException:
continue
except StopSync as exc:
self.logger.warning(exc, provider_pk=provider.pk)

View File

@ -30,6 +30,11 @@ class TestHTTP(TestCase):
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="127.0.0.2")
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.2")
def test_forward_for_invalid(self):
"""Test invalid forward for"""
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="foobar")
self.assertEqual(ClientIPMiddleware.get_client_ip(request), ClientIPMiddleware.default_ip)
def test_fake_outpost(self):
"""Test faked IP which is overridden by an outpost"""
token = Token.objects.create(
@ -53,6 +58,17 @@ class TestHTTP(TestCase):
},
)
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.1")
# Invalid, not a real IP
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
self.user.save()
request = self.factory.get(
"/",
**{
ClientIPMiddleware.outpost_remote_ip_header: "foobar",
ClientIPMiddleware.outpost_token_header: token.key,
},
)
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.1")
# Valid
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
self.user.save()

View File

@ -20,6 +20,7 @@ from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSeri
from authentik.core.models import Provider
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.providers.rac.models import RACProvider
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
from authentik.outposts.models import (
@ -49,6 +50,10 @@ class OutpostSerializer(ModelSerializer):
service_connection_obj = ServiceConnectionSerializer(
source="service_connection", read_only=True
)
refresh_interval_s = SerializerMethodField()
def get_refresh_interval_s(self, obj: Outpost) -> int:
return int(timedelta_from_string(obj.config.refresh_interval).total_seconds())
def validate_name(self, name: str) -> str:
"""Validate name (especially for embedded outpost)"""
@ -84,7 +89,8 @@ class OutpostSerializer(ModelSerializer):
def validate_config(self, config) -> dict:
"""Check that the config has all required fields"""
try:
from_dict(OutpostConfig, config)
parsed = from_dict(OutpostConfig, config)
timedelta_string_validator(parsed.refresh_interval)
except DaciteError as exc:
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
return config
@ -99,6 +105,7 @@ class OutpostSerializer(ModelSerializer):
"providers_obj",
"service_connection",
"service_connection_obj",
"refresh_interval_s",
"token_identifier",
"config",
"managed",

View File

@ -26,6 +26,7 @@ from authentik.outposts.models import (
KubernetesServiceConnection,
OutpostServiceConnection,
)
from authentik.rbac.filters import ObjectFilter
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
@ -75,7 +76,7 @@ class ServiceConnectionViewSet(
filterset_fields = ["name"]
@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

@ -61,6 +61,7 @@ class OutpostConfig:
log_level: str = CONFIG.get("log_level")
object_naming_template: str = field(default="ak-outpost-%(name)s")
refresh_interval: str = "minutes=5"
container_image: str | None = field(default=None)

View File

@ -2,7 +2,6 @@
from dataclasses import asdict
from channels.exceptions import DenyConnection
from channels.routing import URLRouter
from channels.testing import WebsocketCommunicator
from django.test import TransactionTestCase
@ -37,9 +36,8 @@ class TestOutpostWS(TransactionTestCase):
communicator = WebsocketCommunicator(
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
)
with self.assertRaises(DenyConnection):
connected, _ = await communicator.connect()
self.assertFalse(connected)
connected, _ = await communicator.connect()
self.assertFalse(connected)
async def test_auth_valid(self):
"""Test auth with token"""

View File

@ -2,13 +2,13 @@
from django.urls import path
from authentik.core.channels import TokenOutpostMiddleware
from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet,
ServiceConnectionViewSet,
)
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.outposts.consumer import OutpostConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -1,6 +1,8 @@
"""Reputation policy API Views"""
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BaseInFilter, CharFilter
from django_filters.filterset import FilterSet
from rest_framework import mixins
from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import GenericViewSet, ModelViewSet
@ -11,6 +13,10 @@ from authentik.policies.api.policies import PolicySerializer
from authentik.policies.reputation.models import Reputation, ReputationPolicy
class CharInFilter(BaseInFilter, CharFilter):
pass
class ReputationPolicySerializer(PolicySerializer):
"""Reputation Policy Serializer"""
@ -38,6 +44,16 @@ class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
ordering = ["name"]
class ReputationFilter(FilterSet):
"""Filter for reputation"""
identifier_in = CharInFilter(field_name="identifier", lookup_expr="in")
class Meta:
model = Reputation
fields = ["identifier", "ip", "score"]
class ReputationSerializer(ModelSerializer):
"""Reputation Serializer"""
@ -66,5 +82,5 @@ class ReputationViewSet(
queryset = Reputation.objects.all()
serializer_class = ReputationSerializer
search_fields = ["identifier", "ip", "score"]
filterset_fields = ["identifier", "ip", "score"]
filterset_class = ReputationFilter
ordering = ["ip"]

View File

@ -29,7 +29,6 @@ class TesOAuth2Introspection(OAuthTestCase):
self.app = Application.objects.create(
name=generate_id(), slug=generate_id(), provider=self.provider
)
self.app.save()
self.user = create_test_admin_user()
self.auth = b64encode(
f"{self.provider.client_id}:{self.provider.client_secret}".encode()
@ -114,6 +113,41 @@ class TesOAuth2Introspection(OAuthTestCase):
},
)
def test_introspect_invalid_provider(self):
"""Test introspection (mismatched provider and token)"""
provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
redirect_uris="",
signing_key=create_test_cert(),
)
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
token: AccessToken = AccessToken.objects.create(
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
res = self.client.post(
reverse("authentik_providers_oauth2:token-introspection"),
HTTP_AUTHORIZATION=f"Basic {auth}",
data={"token": token.token},
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"active": False,
},
)
def test_introspect_invalid_auth(self):
"""Test introspect (invalid auth)"""
res = self.client.post(

View File

@ -46,10 +46,10 @@ class TokenIntrospectionParams:
if not provider:
raise TokenIntrospectionError
access_token = AccessToken.objects.filter(token=raw_token).first()
access_token = AccessToken.objects.filter(token=raw_token, provider=provider).first()
if access_token:
return TokenIntrospectionParams(access_token, provider)
refresh_token = RefreshToken.objects.filter(token=raw_token).first()
refresh_token = RefreshToken.objects.filter(token=raw_token, provider=provider).first()
if refresh_token:
return TokenIntrospectionParams(refresh_token, provider)
LOGGER.debug("Token does not exist", token=raw_token)

View File

@ -2,6 +2,7 @@
from collections.abc import Callable
from hashlib import sha512
from ipaddress import ip_address
from time import perf_counter, time
from typing import Any
@ -174,6 +175,7 @@ class ClientIPMiddleware:
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
self.logger = get_logger().bind()
def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers.
@ -185,11 +187,16 @@ class ClientIPMiddleware:
"HTTP_X_FORWARDED_FOR",
"REMOTE_ADDR",
)
for _header in headers:
if _header in meta:
ips: list[str] = meta.get(_header).split(",")
return ips[0].strip()
return self.default_ip
try:
for _header in headers:
if _header in meta:
ips: list[str] = meta.get(_header).split(",")
# Ensure the IP parses as a valid IP
return str(ip_address(ips[0].strip()))
return self.default_ip
except ValueError as exc:
self.logger.debug("Invalid remote IP", exc=exc)
return self.default_ip
# FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
# but for now it's fine
@ -228,7 +235,11 @@ class ClientIPMiddleware:
Hub.current.scope.set_user(user)
# Set the outpost service account on the request
setattr(request, self.request_attr_outpost_user, user)
return delegated_ip
try:
return str(ip_address(delegated_ip))
except ValueError as exc:
self.logger.debug("Invalid remote IP from Outpost", exc=exc)
return None
def _get_client_ip(self, request: HttpRequest | None) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers.
@ -274,9 +285,13 @@ class ChannelsLoggingMiddleware:
self.log(scope)
try:
return await self.inner(scope, receive, send)
except DenyConnection:
return await send({"type": "websocket.close"})
except Exception as exc:
if settings.DEBUG:
raise exc
LOGGER.warning("Exception in ASGI application", exc=exc)
raise DenyConnection() from None
return await send({"type": "websocket.close"})
def log(self, scope: dict, **kwargs):
"""Log request"""

View File

@ -39,7 +39,7 @@ def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
@receiver(password_validate)
def ldap_password_validate(sender, password: str, plan_context: dict[str, Any], **__):
"""if there's an LDAP Source with enabled password sync, check the password"""
sources = LDAPSource.objects.filter(sync_users_password=True)
sources = LDAPSource.objects.filter(sync_users_password=True, enabled=True)
if not sources.exists():
return
source = sources.first()
@ -56,7 +56,7 @@ def ldap_password_validate(sender, password: str, plan_context: dict[str, Any],
@receiver(password_changed)
def ldap_sync_password(sender, user: User, password: str, **_):
"""Connect to ldap and update password."""
sources = LDAPSource.objects.filter(sync_users_password=True)
sources = LDAPSource.objects.filter(sync_users_password=True, enabled=True)
if not sources.exists():
return
source = sources.first()

View File

@ -2,9 +2,6 @@
from typing import Any
from facebook import GraphAPI
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -19,19 +16,9 @@ class FacebookOAuthRedirect(OAuthRedirect):
}
class FacebookOAuth2Client(OAuth2Client):
"""Facebook OAuth2 Client"""
def get_profile_info(self, token: dict[str, str]) -> dict[str, Any] | None:
api = GraphAPI(access_token=token["access_token"])
return api.get_object("me", fields="id,name,email")
class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback"""
client_class = FacebookOAuth2Client
def get_user_enroll_context(
self,
info: dict[str, Any],

View File

@ -1,7 +1,5 @@
"""SCIM Source"""
from uuid import uuid4
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
@ -19,8 +17,6 @@ class SCIMSource(Source):
@property
def service_account_identifier(self) -> str:
if not self.pk:
self.pk = uuid4()
return f"ak-source-scim-{self.pk}"
@property

View File

@ -1,41 +1,44 @@
from django.db.models import Model
from django.db.models.signals import pre_delete, pre_save
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
from authentik.events.middleware import audit_ignore
from authentik.sources.scim.models import SCIMSource
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
@receiver(pre_save, sender=SCIMSource)
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
@receiver(post_save, sender=SCIMSource)
def scim_source_post_save(sender: type[Model], instance: SCIMSource, created: bool, **_):
"""Create service account before source is saved"""
# .service_account_identifier will auto-assign a primary key uuid to the source
# if none is set yet, just so we can get the identifier before we save
identifier = instance.service_account_identifier
user = User.objects.create(
user, _ = User.objects.update_or_create(
username=identifier,
name=f"SCIM Source {instance.name} Service-Account",
type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
path=USER_PATH_SOURCE_SCIM,
defaults={
"name": f"SCIM Source {instance.name} Service-Account",
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
"path": USER_PATH_SOURCE_SCIM,
},
)
token = Token.objects.create(
user=user,
token, token_created = Token.objects.update_or_create(
identifier=identifier,
intent=TokenIntents.INTENT_API,
expiring=False,
managed=f"goauthentik.io/sources/scim/{instance.pk}",
defaults={
"user": user,
"intent": TokenIntents.INTENT_API,
"expiring": False,
"managed": f"goauthentik.io/sources/scim/{instance.pk}",
},
)
instance.token = token
if created or token_created:
with audit_ignore():
instance.token = token
instance.save()
@receiver(pre_delete, sender=SCIMSource)
def scim_source_pre_delete(sender: type[Model], instance: SCIMSource, **_):
"""Delete SCIM Source service account before deleting source"""
Token.objects.filter(
identifier=instance.service_account_identifier, intent=TokenIntents.INTENT_API
).delete()
@receiver(post_delete, sender=SCIMSource)
def scim_source_post_delete(sender: type[Model], instance: SCIMSource, **_):
"""Delete SCIM Source service account after deleting source"""
User.objects.filter(
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
).delete()

View File

@ -8,6 +8,7 @@ from django.urls import reverse
from rest_framework.exceptions import ValidationError
from authentik.brands.utils import get_brand_for_request
from authentik.core.middleware import RESPONSE_HEADER_ID
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.models import FlowDesignation, FlowStageBinding
@ -186,6 +187,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
"method": "GET",
"path": f"/api/v3/flows/executor/{flow.slug}/",
"user_agent": "",
"request_id": response[RESPONSE_HEADER_ID],
},
},
)

View File

@ -144,7 +144,7 @@ class Migration(migrations.Migration):
default=None,
help_text=(
"When set, shows a password field, instead of showing the password field as"
" seaprate step."
" separate step."
),
null=True,
on_delete=django.db.models.deletion.SET_NULL,

View File

@ -108,7 +108,7 @@ class PromptViewSet(UsedByMixin, ModelViewSet):
return Response(
{
"non_field_errors": [
exception_to_string(exc),
exception_to_string(exc.exc),
]
},
status=400,

View File

@ -170,7 +170,7 @@ class Prompt(SerializerModel):
try:
raw_choices = evaluator.evaluate(self.placeholder)
except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc))
wrapped = PropertyMappingExpressionException(exc, None)
LOGGER.warning(
"failed to evaluate prompt choices",
exc=wrapped,
@ -208,7 +208,7 @@ class Prompt(SerializerModel):
try:
return evaluator.evaluate(self.placeholder)
except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc), None)
wrapped = PropertyMappingExpressionException(exc, None)
LOGGER.warning(
"failed to evaluate prompt placeholder",
exc=wrapped,
@ -237,7 +237,7 @@ class Prompt(SerializerModel):
try:
value = evaluator.evaluate(self.initial_value)
except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc))
wrapped = PropertyMappingExpressionException(exc, None)
LOGGER.warning(
"failed to evaluate prompt initial value",
exc=wrapped,

View File

@ -82,3 +82,5 @@ entries:
order: 10
target: !KeyOf default-authentication-flow-password-binding
policy: !KeyOf default-authentication-flow-password-optional
attrs:
failure_result: true

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.6.1 Blueprint schema",
"title": "authentik 2024.6.5 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.5}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.5}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024042.11
goauthentik.io/api/v3 v3.2024060.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024042.11 h1:cGgUz1E8rlMphGvv04VI7i+MgT8eidZbxTpza5zd96I=
goauthentik.io/api/v3 v3.2024042.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024060.5 h1:AjvPUZoObk7a86ZZaz2tmruteY+1vAEfVzIOzQpWSXM=
goauthentik.io/api/v3 v3.2024060.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -183,7 +183,19 @@ func (ac *APIController) startWSHealth() {
func (ac *APIController) startIntervalUpdater() {
logger := ac.logger.WithField("loop", "interval-updater")
ticker := time.NewTicker(5 * time.Minute)
getInterval := func() time.Duration {
// Ensure timer interval is not negative or 0
// for 0 we assume migration or unconfigured, so default to 5 minutes
if ac.Outpost.RefreshIntervalS <= 0 {
return 5 * time.Minute
}
// Clamp interval to be at least 30 seconds
if ac.Outpost.RefreshIntervalS < 30 {
return 30 * time.Second
}
return time.Duration(ac.Outpost.RefreshIntervalS) * time.Second
}
ticker := time.NewTicker(getInterval())
for ; true; <-ticker.C {
logger.Debug("Running interval update")
err := ac.OnRefresh()
@ -198,6 +210,7 @@ func (ac *APIController) startIntervalUpdater() {
"build": constants.BUILD("tagged"),
}).SetToCurrentTime()
}
ticker.Reset(getInterval())
}
}

View File

@ -1,6 +1,7 @@
# flake8: noqa
from pathlib import Path
from authentik.lib.config import CONFIG
from lifecycle.migrate import BaseMigration
MEDIA_ROOT = Path(__file__).parent.parent.parent / "media"
@ -9,7 +10,9 @@ TENANT_MEDIA_ROOT = MEDIA_ROOT / "public"
class Migration(BaseMigration):
def needs_migration(self) -> bool:
return not TENANT_MEDIA_ROOT.exists()
return (
not TENANT_MEDIA_ROOT.exists() and CONFIG.get("storage.media.backend", "file") != "s3"
)
def run(self):
TENANT_MEDIA_ROOT.mkdir(parents=True)

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.6.1",
"version": "2024.6.5",
"private": true
}

29
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiohttp"
@ -1513,20 +1513,6 @@ files = [
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]]
name = "facebook-sdk"
version = "3.1.0"
description = "This client library is designed to support the Facebook Graph API and the official Facebook JavaScript SDK, which is the canonical way to implement Facebook authentication."
optional = false
python-versions = "*"
files = [
{file = "facebook-sdk-3.1.0.tar.gz", hash = "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"},
{file = "facebook_sdk-3.1.0-py2.py3-none-any.whl", hash = "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e"},
]
[package.dependencies]
requests = "*"
[[package]]
name = "fido2"
version = "1.1.3"
@ -2954,9 +2940,14 @@ version = "0.0.14"
description = "Python module for oci specifications"
optional = false
python-versions = "*"
files = [
{file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"},
]
files = []
develop = false
[package.source]
type = "git"
url = "https://github.com/vsoch/oci-python"
reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
resolved_reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
[[package]]
name = "opentelemetry-api"
@ -5350,4 +5341,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "~3.12"
content-hash = "f960013b56683ab42d82f8b49b2822dffc76046e3d22695ebb737b405a98dbaf"
content-hash = "055376879ff784080ab95c02eaa012fb1dad1213b1faa0dd1d61b0b812859b6d"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.6.1"
version = "2024.6.5"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]
@ -110,7 +110,6 @@ docker = "*"
drf-spectacular = "*"
dumb-init = "*"
duo-client = "*"
facebook-sdk = "*"
fido2 = "*"
flower = "*"
geoip2 = "*"
@ -121,7 +120,7 @@ kubernetes = "*"
ldap3 = "*"
lxml = "*"
msgraph-sdk = "*"
opencontainers = { extras = ["reggie"], version = "*" }
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
packaging = "*"
paramiko = "*"
psycopg = { extras = ["c"], version = "*" }

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.6.1
version: 2024.6.5
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -13080,6 +13080,15 @@ paths:
name: identifier
schema:
type: string
- in: query
name: identifier_in
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: ip
schema:
@ -39489,6 +39498,9 @@ components:
allOf:
- $ref: '#/components/schemas/ServiceConnection'
readOnly: true
refresh_interval_s:
type: integer
readOnly: true
token_identifier:
type: string
description: Get Token identifier
@ -39510,6 +39522,7 @@ components:
- pk
- providers
- providers_obj
- refresh_interval_s
- service_connection_obj
- token_identifier
- type

View File

@ -5,7 +5,6 @@ from time import sleep
from docker.client import DockerClient, from_env
from docker.models.containers import Container
from guardian.shortcuts import get_anonymous_user
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
@ -180,15 +179,13 @@ class TestProviderLDAP(SeleniumTestCase):
)
with self.assertRaises(LDAPInvalidCredentialsResult):
_connection.bind()
anon = get_anonymous_user()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN_FAILED,
user={
"pk": anon.pk,
"email": anon.email,
"username": anon.username,
"is_anonymous": True,
"pk": self.user.pk,
"email": self.user.email,
"username": self.user.username,
},
).exists(),
)

2
web/package-lock.json generated
View File

@ -17,7 +17,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.6.0-1720200294",
"@goauthentik/api": "^2024.6.0-1719577139",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",

View File

@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.6.0-1720200294",
"@goauthentik/api": "^2024.6.0-1719577139",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",

View File

@ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
}),
);
this.centerText = outposts.pagination.count.toString();
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
return outpostStats;
}

View File

@ -1,3 +1,7 @@
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
@ -14,7 +18,6 @@ import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
DigestAlgorithmEnum,
FlowsInstancesListDesignationEnum,
PaginatedSAMLPropertyMappingList,
PropertymappingsApi,
@ -22,7 +25,6 @@ import {
ProvidersApi,
SAMLPropertyMapping,
SAMLProvider,
SignatureAlgorithmEnum,
SpBindingEnum,
} from "@goauthentik/api";
@ -333,25 +335,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
name="digestAlgorithm"
>
<ak-radio
.options=${[
{
label: "SHA1",
value: DigestAlgorithmEnum._200009Xmldsigsha1,
},
{
label: "SHA256",
value: DigestAlgorithmEnum._200104Xmlencsha256,
default: true,
},
{
label: "SHA384",
value: DigestAlgorithmEnum._200104XmldsigMoresha384,
},
{
label: "SHA512",
value: DigestAlgorithmEnum._200104Xmlencsha512,
},
]}
.options=${digestAlgorithmOptions}
.value=${this.instance?.digestAlgorithm}
>
</ak-radio>
@ -362,29 +346,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
name="signatureAlgorithm"
>
<ak-radio
.options=${[
{
label: "RSA-SHA1",
value: SignatureAlgorithmEnum._200009XmldsigrsaSha1,
},
{
label: "RSA-SHA256",
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha256,
default: true,
},
{
label: "RSA-SHA384",
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha384,
},
{
label: "RSA-SHA512",
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha512,
},
{
label: "DSA-SHA1",
value: SignatureAlgorithmEnum._200009XmldsigdsaSha1,
},
]}
.options=${signatureAlgorithmOptions}
.value=${this.instance?.signatureAlgorithm}
>
</ak-radio>

View File

@ -36,11 +36,13 @@ import "@goauthentik/elements/oauth/UserRefreshTokenList";
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList";
import "@goauthentik/elements/user/UserReputationList";
import "@goauthentik/elements/user/sources/SourceSettings";
import { msg, str } from "@lit/localize";
import { TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -274,6 +276,21 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
</div>
</div>
</section>
<section
slot="page-reputation"
data-tab-title="${msg("Reputation scores")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-reputation-list
targetUsername=${user.username}
targetEmail=${ifDefined(user.email)}
>
</ak-user-reputation-list>
</div>
</div>
</section>
<section
slot="page-consent"
data-tab-title="${msg("Explicit Consent")}"

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.6.1";
export const VERSION = "2024.6.5";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -42,6 +42,14 @@ body {
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}
.pf-c-card.pf-m-non-selectable-raised {
--pf-c-card--BackgroundColor: var(--ak-dark-background-lighter);
}
.pf-c-card.pf-m-hoverable-raised::before,
.pf-c-card.pf-m-selectable-raised::before,
.pf-c-card.pf-m-non-selectable-raised::before {
--pf-c-card--m-selectable-raised--before--BackgroundColor: var(--ak-dark-background-light);
}
.pf-c-card__title,
.pf-c-card__body {
color: var(--ak-dark-foreground);

View File

@ -1,4 +1,5 @@
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { UIConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
@ -16,6 +17,7 @@ type AkInterface = HTMLElement & {
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
get activeTheme(): UiThemeEnum | undefined;
};
export const rootInterface = <T extends AkInterface>(): T | undefined =>
@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> {
return css;
}
const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
// when changing themes we might not remove the correct css stylesheet instance.
const _darkTheme = ensureCSSStyleSheet(ThemeDark);
@localized()
export class AKElement extends LitElement {
@ -90,12 +96,7 @@ export class AKElement extends LitElement {
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
// Early activate theme based on media query to prevent light flash
// when dark is preferred
this._activateTheme(
root,
window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
? UiThemeEnum.Light
: UiThemeEnum.Dark,
);
this._applyTheme(root, globalAK().brand.uiTheme);
this._applyTheme(root, await this.getTheme());
}
@ -125,8 +126,9 @@ export class AKElement extends LitElement {
ev?.matches || this._mediaMatcher?.matches
? UiThemeEnum.Light
: UiThemeEnum.Dark;
this._activateTheme(root, theme);
this._activateTheme(theme, root);
};
this._mediaMatcherHandler(undefined);
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
}
return;
@ -136,17 +138,21 @@ export class AKElement extends LitElement {
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
this._mediaMatcher = undefined;
}
this._activateTheme(root, theme);
this._activateTheme(theme, root);
}
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
if (theme === UiThemeEnum.Dark) {
return ThemeDark;
return _darkTheme;
}
return undefined;
}
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum) {
/**
* Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet
* to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active.
*/
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) {
if (theme === this._activeTheme) {
return;
}
@ -161,12 +167,19 @@ export class AKElement extends LitElement {
this.setAttribute("theme", theme);
const stylesheet = AKElement.themeToStylesheet(theme);
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
if (stylesheet) {
root.adoptedStyleSheets = [...root.adoptedStyleSheets, ensureCSSStyleSheet(stylesheet)];
}
if (oldStylesheet) {
root.adoptedStyleSheets = root.adoptedStyleSheets.filter((v) => v !== oldStylesheet);
}
roots.forEach((root) => {
if (stylesheet) {
root.adoptedStyleSheets = [
...root.adoptedStyleSheets,
ensureCSSStyleSheet(stylesheet),
];
}
if (oldStylesheet) {
root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
(v) => v !== oldStylesheet,
);
}
});
this._activeTheme = theme;
this.requestUpdate();
}

View File

@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api";
import { AKElement } from "../Base";
import { AKElement, rootInterface } from "../Base";
import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController";
@ -50,9 +50,19 @@ export class Interface extends AKElement implements AkInterface {
this.dataset.akInterfaceRoot = "true";
}
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void {
super._activateTheme(root, theme);
super._activateTheme(document as unknown as DocumentOrShadowRoot, theme);
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void {
if (theme === this._activeTheme) {
return;
}
console.debug(
`authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
);
// Special case for root interfaces, as they need to modify the global document CSS too
// Instead of calling ._activateTheme() twice, we insert the root document in the call
// since multiple calls to ._activateTheme() would not do anything after the first call
// as the theme is already enabled.
roots.unshift(document as unknown as DocumentOrShadowRoot);
super._activateTheme(theme, ...roots);
}
async getTheme(): Promise<UiThemeEnum> {

View File

@ -1,7 +1,8 @@
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { LitElement, html } from "lit";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider";
@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale";
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
const LocaleContextBase = WithBrandConfig(LitElement);
/**
* A component to manage your locale settings.
*
@ -25,7 +24,7 @@ const LocaleContextBase = WithBrandConfig(LitElement);
* @fires ak-locale-change - When a valid locale has been swapped in
*/
@customElement("ak-locale-context")
export class LocaleContext extends LocaleContextBase {
export class LocaleContext extends WithBrandConfig(AKElement) {
/// @attribute The text representation of the current locale */
@property({ attribute: true, type: String })
locale = DEFAULT_LOCALE;
@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase {
return;
}
locale.locale().then(() => {
console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`);
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
this.setLocale(locale.code).then(() => {
window.setTimeout(this.notifyApplication, 0);
});

View File

@ -1,6 +1,7 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { themeImage } from "@goauthentik/elements/utils/images";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
@ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
<a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand">
<img
src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo}
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
alt="authentik Logo"
loading="lazy"
/>

View File

@ -0,0 +1,83 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { PoliciesApi, Reputation } from "@goauthentik/api";
@customElement("ak-user-reputation-list")
export class UserReputationList extends Table<Reputation> {
@property()
targetUsername!: string;
@property()
targetEmail!: string | undefined;
async apiEndpoint(page: number): Promise<PaginatedResponse<Reputation>> {
const identifiers = [this.targetUsername];
if (this.targetEmail !== undefined) {
identifiers.push(this.targetEmail);
}
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresList({
identifierIn: identifiers,
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
});
}
checkbox = true;
clearOnRefresh = true;
order = "identifier";
columns(): TableColumn[] {
return [
new TableColumn(msg("Identifier"), "identifier"),
new TableColumn(msg("IP"), "ip"),
new TableColumn(msg("Score"), "score"),
new TableColumn(msg("Updated"), "updated"),
];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Reputation score(s)")}
.objects=${this.selectedElements}
.usedBy=${(item: Reputation) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresUsedByList({
reputationUuid: item.pk || "",
});
}}
.delete=${(item: Reputation) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresDestroy({
reputationUuid: item.pk || "",
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Reputation): TemplateResult[] {
return [
html`${item.identifier}`,
html`${item.ipGeoData?.country
? html` ${getUnicodeFlagIcon(item.ipGeoData.country)} `
: html``}
${item.ip}`,
html`${item.score}`,
html`<div>${getRelativeTime(item.updated)}</div>
<small>${item.updated.toLocaleString()}</small>`,
];
}
}

View File

@ -0,0 +1,13 @@
import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base";
import { UiThemeEnum } from "@goauthentik/api";
export function themeImage(rawPath: string) {
let enabledTheme = rootInterface()?.activeTheme;
if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) {
enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
? UiThemeEnum.Light
: UiThemeEnum.Dark;
}
return rawPath.replaceAll("%(theme)s", enabledTheme);
}

View File

@ -11,6 +11,7 @@ import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/LoadingOverlay";
import "@goauthentik/elements/ak-locale-context";
import { themeImage } from "@goauthentik/elements/utils/images";
import "@goauthentik/flow/sources/apple/AppleLoginInit";
import "@goauthentik/flow/sources/plex/PlexLoginInit";
import "@goauthentik/flow/stages/FlowErrorStage";
@ -442,7 +443,9 @@ export class FlowExecutor extends Interface implements StageHost {
renderChallengeWrapper(): TemplateResult {
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img
src="${first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, "")}"
src="${themeImage(
first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, ""),
)}"
alt="authentik Logo"
/>
</div>`;

View File

@ -51,11 +51,6 @@ export class AutosubmitStage extends BaseStage<
/>`;
})}
<ak-empty-state ?loading="${true}"> </ak-empty-state>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">

View File

@ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils";
import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { themeImage } from "@goauthentik/elements/utils/images";
import "rapidoc";
import { CSSResult, TemplateResult, css, html } from "lit";
@ -103,7 +104,9 @@ export class APIBrowser extends Interface {
<img
alt="authentik Logo"
class="logo"
src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}"
src="${themeImage(
first(this.brand?.brandingLogo, DefaultBrand.brandingLogo),
)}"
/>
</div>
</rapi-doc>

View File

@ -21,6 +21,7 @@ import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarItem";
import { themeImage } from "@goauthentik/elements/utils/images";
import { ROUTES } from "@goauthentik/user/Routes";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { match } from "ts-pattern";
@ -193,7 +194,7 @@ class UserInterfacePresentation extends AKElement {
<a href="#/" class="pf-c-page__header-brand-link">
<img
class="pf-c-brand"
src="${this.brand.brandingLogo}"
src="${themeImage(this.brand.brandingLogo)}"
alt="${this.brand.brandingTitle}"
/>
</a>

View File

@ -20,3 +20,7 @@ This means that if you want to select a default flow based on policy, you can le
## Branding
The brand configuration controls the branding title (shown in website document title and several other places), and the sidebar/header logo that appears in the upper left of the product interface.
:::info
Starting with authentik 2024.6.2, the placeholder `%(theme)s` can be used in the logo configuration option, which will be replaced with the active theme.
:::

View File

@ -3,6 +3,11 @@
# Allowed levels: trace, debug, info, warning, error
# Applies to: non-embedded
log_level: debug
# Interval at which the outpost will refresh the providers
# from authentik. For caching outposts (such as LDAP), the
# cache will also be invalidated at that interval.
# (Format: hours=1;minutes=2;seconds=3).
refresh_interval: minutes=5
########################################
# The settings below are only relevant when using a managed outpost
########################################

View File

@ -0,0 +1,31 @@
# CVE-2024-42490
_Reported by [@m2a2](https://github.com/m2a2)_
## Insufficient Authorization for several API endpoints
### 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

@ -0,0 +1,35 @@
# CVE-2024-47070
_Reported by [@efpi-bot](https://github.com/efpi-bot) from [LogicalTrust](https://logicaltrust.net/en/)_
## Password authentication bypass via X-Forwarded-For HTTP header
### Summary
The vulnerability allows bypassing policies by adding X-Forwarded-For header with unparsable IP address, e.g. "a". This results in a possibility to authenticate/authorize to any account with known login or email address.
Since the default authentication flow uses a policy to enable the password stage only when there is no password stage selected on the Identification stage, this vulnerability can be used to skip this policy and continue without the password stage.
### Am I affected
This can be exploited for the following configurations:
- An attacker can access authentik without a reverse proxy (and `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS` is not configured properly)
- The reverse proxy configuration does not correctly overwrite X-Forwarded-For
- Policies (User and group bindings do _not_ apply) are bound to authentication/authorization flows
### Patches
authentik 2024.6.5 and 2024.8.3 fix this issue.
### Workarounds
Ensure the X-Forwarded-For header is always set by the reverse proxy, and is always set to a correct IP.
In addition you can manually change the _Failure result_ option on policy bindings to _Pass_, which will prevent any stages from being skipped if a malicious request is received.
### 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,25 @@
# CVE-2024-47077
_Reported by [@quentinmit](https://github.com/quentinmit)_
## Insufficient cross-provider token validation during introspection
### Summary
Access tokens issued to one application can be stolen by that application and used to impersonate the user against any other proxy provider. Also, a user can steal an access token they were legitimately issued for one application and use it to access another application that they aren't allowed to access.
### Details
The proxy provider uses `/application/o/introspect/` to validate bearer tokens provided in the `Authorization` header:
The implementation of this endpoint separately validates the `client_id` and `client_secret` (which are that of the proxy provider) and the `token` without validating that they correspond to the same provider.
### Patches
authentik 2024.6.5 and 2024.8.3 fix this issue.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -511,6 +511,9 @@ const docsSidebar = {
items: [
"security/security-hardening",
"security/policy",
"security/CVE-2024-47077",
"security/CVE-2024-47070",
"security/CVE-2024-42490",
"security/CVE-2024-38371",
"security/CVE-2024-37905",
"security/CVE-2024-23647",