Compare commits
7 Commits
version/20
...
version-20
| Author | SHA1 | Date | |
|---|---|---|---|
| 8469213d82 | |||
| 78f7b04d5a | |||
| 22e586bd8c | |||
| 8a0b31b922 | |||
| 359b343f51 | |||
| b727656b05 | |||
| 8f09c2c21c |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2024.6.3
|
current_version = 2024.6.5
|
||||||
tag = True
|
tag = True
|
||||||
commit = 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*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.6.3"
|
__version__ = "2024.6.5"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
|
|
||||||
class DeleteAction(Enum):
|
class DeleteAction(Enum):
|
||||||
@ -53,7 +54,7 @@ class UsedByMixin:
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={200: UsedBySerializer(many=True)},
|
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:
|
def used_by(self, request: Request, *args, **kwargs) -> Response:
|
||||||
"""Get a list of all objects that use this object"""
|
"""Get a list of all objects that use this object"""
|
||||||
model: Model = self.get_object()
|
model: Model = self.get_object()
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
|||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -265,7 +266,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
],
|
],
|
||||||
responses={200: CertificateDataSerializer(many=False)},
|
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:
|
def view_certificate(self, request: Request, pk: str) -> Response:
|
||||||
"""Return certificate-key pairs certificate and log access"""
|
"""Return certificate-key pairs certificate and log access"""
|
||||||
certificate: CertificateKeyPair = self.get_object()
|
certificate: CertificateKeyPair = self.get_object()
|
||||||
@ -295,7 +296,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
],
|
],
|
||||||
responses={200: CertificateDataSerializer(many=False)},
|
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:
|
def view_private_key(self, request: Request, pk: str) -> Response:
|
||||||
"""Return certificate-key pairs private key and log access"""
|
"""Return certificate-key pairs private key and log access"""
|
||||||
certificate: CertificateKeyPair = self.get_object()
|
certificate: CertificateKeyPair = self.get_object()
|
||||||
|
|||||||
@ -214,6 +214,46 @@ class TestCrypto(APITestCase):
|
|||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
self.assertIn("Content-Disposition", response)
|
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):
|
def test_used_by(self):
|
||||||
"""Test used_by endpoint"""
|
"""Test used_by endpoint"""
|
||||||
self.client.force_login(create_test_admin_user())
|
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):
|
def test_discovery(self):
|
||||||
"""Test certificate discovery"""
|
"""Test certificate discovery"""
|
||||||
name = generate_id()
|
name = generate_id()
|
||||||
|
|||||||
@ -37,6 +37,7 @@ from authentik.lib.utils.file import (
|
|||||||
)
|
)
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -281,7 +282,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
400: OpenApiResponse(description="Flow not applicable"),
|
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):
|
def execute(self, request: Request, slug: str):
|
||||||
"""Execute flow for current user"""
|
"""Execute flow for current user"""
|
||||||
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
|
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
|
||||||
|
|||||||
@ -30,6 +30,11 @@ class TestHTTP(TestCase):
|
|||||||
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="127.0.0.2")
|
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="127.0.0.2")
|
||||||
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "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):
|
def test_fake_outpost(self):
|
||||||
"""Test faked IP which is overridden by an outpost"""
|
"""Test faked IP which is overridden by an outpost"""
|
||||||
token = Token.objects.create(
|
token = Token.objects.create(
|
||||||
@ -53,6 +58,17 @@ class TestHTTP(TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.1")
|
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
|
# Valid
|
||||||
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|||||||
@ -26,6 +26,7 @@ from authentik.outposts.models import (
|
|||||||
KubernetesServiceConnection,
|
KubernetesServiceConnection,
|
||||||
OutpostServiceConnection,
|
OutpostServiceConnection,
|
||||||
)
|
)
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
|
|
||||||
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
@ -75,7 +76,7 @@ class ServiceConnectionViewSet(
|
|||||||
filterset_fields = ["name"]
|
filterset_fields = ["name"]
|
||||||
|
|
||||||
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
|
@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:
|
def state(self, request: Request, pk: str) -> Response:
|
||||||
"""Get the service connection's state"""
|
"""Get the service connection's state"""
|
||||||
connection = self.get_object()
|
connection = self.get_object()
|
||||||
|
|||||||
@ -29,7 +29,6 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
self.app = Application.objects.create(
|
self.app = Application.objects.create(
|
||||||
name=generate_id(), slug=generate_id(), provider=self.provider
|
name=generate_id(), slug=generate_id(), provider=self.provider
|
||||||
)
|
)
|
||||||
self.app.save()
|
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.auth = b64encode(
|
self.auth = b64encode(
|
||||||
f"{self.provider.client_id}:{self.provider.client_secret}".encode()
|
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):
|
def test_introspect_invalid_auth(self):
|
||||||
"""Test introspect (invalid auth)"""
|
"""Test introspect (invalid auth)"""
|
||||||
res = self.client.post(
|
res = self.client.post(
|
||||||
|
|||||||
@ -46,10 +46,10 @@ class TokenIntrospectionParams:
|
|||||||
if not provider:
|
if not provider:
|
||||||
raise TokenIntrospectionError
|
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:
|
if access_token:
|
||||||
return TokenIntrospectionParams(access_token, provider)
|
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:
|
if refresh_token:
|
||||||
return TokenIntrospectionParams(refresh_token, provider)
|
return TokenIntrospectionParams(refresh_token, provider)
|
||||||
LOGGER.debug("Token does not exist", token=raw_token)
|
LOGGER.debug("Token does not exist", token=raw_token)
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
|
from ipaddress import ip_address
|
||||||
from time import perf_counter, time
|
from time import perf_counter, time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -174,6 +175,7 @@ class ClientIPMiddleware:
|
|||||||
|
|
||||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
self.logger = get_logger().bind()
|
||||||
|
|
||||||
def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
|
def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
@ -185,10 +187,15 @@ class ClientIPMiddleware:
|
|||||||
"HTTP_X_FORWARDED_FOR",
|
"HTTP_X_FORWARDED_FOR",
|
||||||
"REMOTE_ADDR",
|
"REMOTE_ADDR",
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
for _header in headers:
|
for _header in headers:
|
||||||
if _header in meta:
|
if _header in meta:
|
||||||
ips: list[str] = meta.get(_header).split(",")
|
ips: list[str] = meta.get(_header).split(",")
|
||||||
return ips[0].strip()
|
# 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
|
return self.default_ip
|
||||||
|
|
||||||
# FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
|
# FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
|
||||||
@ -228,7 +235,11 @@ class ClientIPMiddleware:
|
|||||||
Hub.current.scope.set_user(user)
|
Hub.current.scope.set_user(user)
|
||||||
# Set the outpost service account on the request
|
# Set the outpost service account on the request
|
||||||
setattr(request, self.request_attr_outpost_user, user)
|
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:
|
def _get_client_ip(self, request: HttpRequest | None) -> str:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
|
|||||||
@ -39,7 +39,7 @@ def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
|||||||
@receiver(password_validate)
|
@receiver(password_validate)
|
||||||
def ldap_password_validate(sender, password: str, plan_context: dict[str, Any], **__):
|
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"""
|
"""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():
|
if not sources.exists():
|
||||||
return
|
return
|
||||||
source = sources.first()
|
source = sources.first()
|
||||||
@ -56,7 +56,7 @@ def ldap_password_validate(sender, password: str, plan_context: dict[str, Any],
|
|||||||
@receiver(password_changed)
|
@receiver(password_changed)
|
||||||
def ldap_sync_password(sender, user: User, password: str, **_):
|
def ldap_sync_password(sender, user: User, password: str, **_):
|
||||||
"""Connect to ldap and update password."""
|
"""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():
|
if not sources.exists():
|
||||||
return
|
return
|
||||||
source = sources.first()
|
source = sources.first()
|
||||||
|
|||||||
@ -82,3 +82,5 @@ entries:
|
|||||||
order: 10
|
order: 10
|
||||||
target: !KeyOf default-authentication-flow-password-binding
|
target: !KeyOf default-authentication-flow-password-binding
|
||||||
policy: !KeyOf default-authentication-flow-password-optional
|
policy: !KeyOf default-authentication-flow-password-optional
|
||||||
|
attrs:
|
||||||
|
failure_result: true
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "authentik 2024.6.3 Blueprint schema",
|
"title": "authentik 2024.6.5 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
|
|||||||
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.5}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -52,7 +52,7 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
- redis
|
- redis
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.5}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2024.6.3"
|
const VERSION = "2024.6.5"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2024.6.3",
|
"version": "2024.6.5",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2024.6.3"
|
version = "2024.6.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2024.6.3
|
version: 2024.6.5
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2024.6.3";
|
export const VERSION = "2024.6.5";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,14 @@ body {
|
|||||||
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
|
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
|
||||||
color: var(--ak-dark-foreground);
|
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__title,
|
||||||
.pf-c-card__body {
|
.pf-c-card__body {
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
|
|||||||
31
website/docs/security/CVE-2024-42490.md
Normal file
31
website/docs/security/CVE-2024-42490.md
Normal 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)
|
||||||
35
website/docs/security/CVE-2024-47070.md
Normal file
35
website/docs/security/CVE-2024-47070.md
Normal 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)
|
||||||
25
website/docs/security/CVE-2024-47077.md
Normal file
25
website/docs/security/CVE-2024-47077.md
Normal 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)
|
||||||
@ -511,6 +511,9 @@ const docsSidebar = {
|
|||||||
items: [
|
items: [
|
||||||
"security/security-hardening",
|
"security/security-hardening",
|
||||||
"security/policy",
|
"security/policy",
|
||||||
|
"security/CVE-2024-47077",
|
||||||
|
"security/CVE-2024-47070",
|
||||||
|
"security/CVE-2024-42490",
|
||||||
"security/CVE-2024-38371",
|
"security/CVE-2024-38371",
|
||||||
"security/CVE-2024-37905",
|
"security/CVE-2024-37905",
|
||||||
"security/CVE-2024-23647",
|
"security/CVE-2024-23647",
|
||||||
|
|||||||
Reference in New Issue
Block a user