Compare commits

...

7 Commits

Author SHA1 Message Date
bb4602745e clean up recovery process by admin 2025-02-19 17:58:32 +01:00
0ae373bc1e web/admin: update Application Wizard button placement (#12771)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* web: Make using the wizard the default for new applications

# What

1. I removed the "Wizard Hint" bar and migrated the "Create With Wizard" button down to the default
   position as "Create With Provider," moving the "Create" button to a secondary position.
   Primary coloring has been kept for both.

2. Added an alert to the "Create" legacy dialog:

> Using this form will only create an Application. In order to authenticate with the application,
> you will have to manually pair it with a Provider.

3. Updated the subtitle on the Wizard dialog:

``` diff
-    wizardDescription = msg("Create a new application");
+    wizardDescription = msg("Create a new application and configure a provider for it.");
```

4. Updated the User page so that, if the User is-a Administrator and the number of Applications in
   the system is zero, the user will be invited to create a new Application using the Wizard rather
   than the legacy Form:

```diff
     renderNewAppButton() {
         const href = paramURL("/core/applications", {
-            createForm: true,
+            createWizard: true,
         });
```

5. Fixed a bug where, on initial render, if the `this.brand` field was not available, an error would
   appear in the console. The effects were usually harmless, as brand information came quickly and
   filled in before the user could notice, but it looked bad in the debugger.

6. Fixed a bug in testing where the wizard page "Configure Policy Bindings" had been changed to
   "Configure Policy/User/Group Binding".

# Testing

Since the wizard OUID didn't change (`data-ouia-component-id="start-application-wizard"`), the E2E
tests for "Application Wizard" completed without any substantial changes to the routine or to the
tests.

``` sh
npm run test:e2e:watch -- --spec ./tests/specs/new-application-by-wizard.ts
```

# User documentation changes required.

These changes were made at the request of docs, as an initial draft to show how the page looks with
the Application Wizard as he default tool for creating new Applications.

# Developer documentation changes required.

None.
2025-02-19 08:41:39 -08:00
6facb5872e web/user: fix opening application with Enter not respecting new tab setting (#13115)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-19 15:49:40 +01:00
c67de17dd8 web: bump API Client version (#13113)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-02-19 13:16:28 +01:00
2128e7f45f providers/rac: move to open source (#13015)
* move RAC to open source

* move web out of enterprise

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

* remove enterprise license requirements from RAC

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-02-19 12:48:11 +01:00
0e7a4849f6 website/docs: add 2025.2 release notes (#13002)
* website/docs: add 2025.2 release notes

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

* make compile

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

* ffs

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

* ffs

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-19 01:43:39 +01:00
85343fa5d4 core: clear expired database sessions (#13105)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-18 20:40:03 +01:00
61 changed files with 852 additions and 778 deletions

View File

@ -50,7 +50,6 @@ from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.enterprise.providers.ssf.models import StreamEvent
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
EndpointDevice,
@ -72,6 +71,7 @@ from authentik.providers.oauth2.models import (
DeviceToken,
RefreshToken,
)
from authentik.providers.rac.models import ConnectionToken
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser

View File

@ -1,11 +1,12 @@
"""User API Views"""
from datetime import timedelta
from datetime import datetime, timedelta
from hashlib import sha256
from json import loads
from typing import Any
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.auth.models import AnonymousUser, Permission
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour
@ -84,6 +85,7 @@ from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.avatars import get_avatar
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import get_permission_choices
from authentik.stages.email.models import EmailStage
@ -446,15 +448,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def _create_recovery_link(self) -> tuple[str, Token]:
def _create_recovery_link(self, expires: datetime) -> tuple[str, Token]:
"""Create a recovery link (when the current brand has a recovery flow set),
that can either be shown to an admin or sent to the user directly"""
brand: Brand = self.request._request.brand
# Check that there is a recovery flow, if not return an error
flow = brand.flow_recovery
if not flow:
raise ValidationError({"non_field_errors": "No recovery flow set."})
raise ValidationError(
{"non_field_errors": [_("Recovery flow is not set for this brand.")]}
)
# Mimic an unauthenticated user navigating the recovery flow
user: User = self.get_object()
self.request._request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
try:
@ -466,16 +472,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
)
except FlowNonApplicableException:
raise ValidationError(
{"non_field_errors": "Recovery flow not applicable to user"}
{"non_field_errors": [_("Recovery flow is not applicable to this user.")]}
) from None
token, __ = FlowToken.objects.update_or_create(
identifier=f"{user.uid}-password-reset",
defaults={
"user": user,
"flow": flow,
"_plan": FlowToken.pickle(plan),
},
token = FlowToken.objects.create(
identifier=f"{user.uid}-password-reset-{sha256(str(datetime.now()).encode('UTF-8')).hexdigest()[:8]}",
user=user,
flow=flow,
_plan=FlowToken.pickle(plan),
expires=expires,
)
querystring = urlencode({QS_KEY_TOKEN: token.key})
link = self.request.build_absolute_uri(
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
@ -610,61 +616,68 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@permission_required("authentik_core.reset_user_password")
@extend_schema(
parameters=[
OpenApiParameter(
name="email_stage",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
),
OpenApiParameter(
name="token_duration",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
required=True,
),
],
responses={
"200": LinkSerializer(many=False),
},
request=None,
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
def recovery(self, request: Request, pk: int) -> Response:
def recovery_link(self, request: Request, pk: int) -> Response:
"""Create a temporary link that a user can use to recover their accounts"""
link, _ = self._create_recovery_link()
return Response({"link": link})
token_duration = request.query_params.get("token_duration", "")
timedelta_string_validator(token_duration)
expires = now() + timedelta_from_string(token_duration)
link, token = self._create_recovery_link(expires)
@permission_required("authentik_core.reset_user_password")
@extend_schema(
parameters=[
OpenApiParameter(
name="email_stage",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
required=True,
if email_stage := request.query_params.get("email_stage"):
for_user: User = self.get_object()
if for_user.email == "":
LOGGER.debug("User doesn't have an email address")
raise ValidationError(
{"non_field_errors": [_("User does not have an email address set.")]}
)
# Lookup the email stage to assure the current user can access it
stages = get_objects_for_user(
request.user, "authentik_stages_email.view_emailstage"
).filter(pk=email_stage)
if not stages.exists():
if stages := EmailStage.objects.filter(pk=email_stage).exists():
raise ValidationError(
{"non_field_errors": [_("User has no permissions to this Email stage.")]}
)
else:
raise ValidationError(
{"non_field_errors": [_("The given Email stage does not exist.")]}
)
email_stage: EmailStage = stages.first()
message = TemplateEmailMessage(
subject=_(email_stage.subject),
to=[(for_user.name, for_user.email)],
template_name=email_stage.template,
language=for_user.locale(request),
template_context={
"url": link,
"user": for_user,
"expires": token.expires,
},
)
],
responses={
"204": OpenApiResponse(description="Successfully sent recover email"),
},
request=None,
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
def recovery_email(self, request: Request, pk: int) -> Response:
"""Create a temporary link that a user can use to recover their accounts"""
for_user: User = self.get_object()
if for_user.email == "":
LOGGER.debug("User doesn't have an email address")
raise ValidationError({"non_field_errors": "User does not have an email address set."})
link, token = self._create_recovery_link()
# Lookup the email stage to assure the current user can access it
stages = get_objects_for_user(
request.user, "authentik_stages_email.view_emailstage"
).filter(pk=request.query_params.get("email_stage"))
if not stages.exists():
LOGGER.debug("Email stage does not exist/user has no permissions")
raise ValidationError({"non_field_errors": "Email stage does not exist."})
email_stage: EmailStage = stages.first()
message = TemplateEmailMessage(
subject=_(email_stage.subject),
to=[(for_user.name, for_user.email)],
template_name=email_stage.template,
language=for_user.locale(request),
template_context={
"url": link,
"user": for_user,
"expires": token.expires,
},
)
send_mails(email_stage, message)
return Response(status=204)
send_mails(email_stage, message)
return Response({"link": link})
@permission_required("authentik_core.impersonate")
@extend_schema(

View File

@ -67,6 +67,8 @@ def clean_expired_models(self: SystemTask):
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
if CONFIG.get("session_storage", "cache") == "db":
DBSessionStore.clear_expired()
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")

View File

@ -1,14 +0,0 @@
"""RAC app config"""
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderRAC(EnterpriseConfig):
"""authentik enterprise rac app config"""
name = "authentik.enterprise.providers.rac"
label = "authentik_providers_rac"
verbose_name = "authentik Enterprise.Providers.RAC"
default = True
mountpoint = ""
ws_mountpoint = "authentik.enterprise.providers.rac.urls"

View File

@ -16,7 +16,6 @@ TENANT_APPS = [
"authentik.enterprise.audit",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.rac",
"authentik.enterprise.providers.ssf",
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
"authentik.enterprise.stages.source",

View File

@ -36,6 +36,15 @@ class FlowAuthenticationRequirement(models.TextChoices):
REQUIRE_REDIRECT = "require_redirect"
REQUIRE_OUTPOST = "require_outpost"
@property
def possibly_unauthenticated(self) -> bool:
"""Check if unauthenticated users can run this flow. Flows like this may require additional
hardening."""
return self in [
FlowAuthenticationRequirement.NONE,
FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
]
class NotConfiguredAction(models.TextChoices):
"""Decides how the FlowExecutor should proceed when a stage isn't configured"""

View File

@ -31,7 +31,7 @@ def timedelta_string_validator(value: str):
def timedelta_from_string(expr: str) -> datetime.timedelta:
"""Convert a string with the format of 'hours=1;minute=3;seconds=5' to a
"""Convert a string with the format of 'hours=1;minutes=3;seconds=5' to a
`datetime.timedelta` Object with hours = 1, minutes = 3, seconds = 5"""
kwargs = {}
for duration_pair in expr.split(";"):

View File

@ -19,7 +19,6 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
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
@ -31,6 +30,7 @@ from authentik.outposts.models import (
)
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.rac.models import RACProvider
from authentik.providers.radius.models import RadiusProvider

View File

@ -18,8 +18,6 @@ from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
from structlog.stdlib import get_logger
from yaml import safe_load
from authentik.enterprise.providers.rac.controllers.docker import RACDockerController
from authentik.enterprise.providers.rac.controllers.kubernetes import RACKubernetesController
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask, prefill_task
from authentik.lib.config import CONFIG
@ -41,6 +39,8 @@ from authentik.providers.ldap.controllers.docker import LDAPDockerController
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.providers.rac.controllers.docker import RACDockerController
from authentik.providers.rac.controllers.kubernetes import RACKubernetesController
from authentik.providers.radius.controllers.docker import RadiusDockerController
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
from authentik.root.celery import CELERY_APP

View File

@ -6,13 +6,12 @@ from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.providers.rac.api.endpoints import EndpointSerializer
from authentik.providers.rac.api.providers import RACProviderSerializer
from authentik.providers.rac.models import ConnectionToken
class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
class ConnectionTokenSerializer(ModelSerializer):
"""ConnectionToken Serializer"""
provider_obj = RACProviderSerializer(source="provider", read_only=True)

View File

@ -14,10 +14,9 @@ from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Provider
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import Endpoint
from authentik.policies.engine import PolicyEngine
from authentik.providers.rac.api.providers import RACProviderSerializer
from authentik.providers.rac.models import Endpoint
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -28,7 +27,7 @@ def user_endpoint_cache_key(user_pk: str) -> str:
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
class EndpointSerializer(ModelSerializer):
"""Endpoint Serializer"""
provider_obj = RACProviderSerializer(source="provider", read_only=True)

View File

@ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField
from authentik.enterprise.providers.rac.models import RACPropertyMapping
from authentik.providers.rac.models import RACPropertyMapping
class RACPropertyMappingSerializer(PropertyMappingSerializer):

View File

@ -5,11 +5,10 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.models import RACProvider
from authentik.providers.rac.models import RACProvider
class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
class RACProviderSerializer(ProviderSerializer):
"""RACProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")

View File

@ -0,0 +1,14 @@
"""RAC app config"""
from django.apps import AppConfig
class AuthentikProviderRAC(AppConfig):
"""authentik rac app config"""
name = "authentik.providers.rac"
label = "authentik_providers_rac"
verbose_name = "authentik Providers.RAC"
default = True
mountpoint = ""
ws_mountpoint = "authentik.providers.rac.urls"

View File

@ -7,22 +7,22 @@ from channels.generic.websocket import AsyncWebsocketConsumer
from django.http.request import QueryDict
from structlog.stdlib import BoundLogger, get_logger
from authentik.enterprise.providers.rac.models import ConnectionToken, RACProvider
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
from authentik.outposts.models import Outpost, OutpostState, OutpostType
from authentik.providers.rac.models import ConnectionToken, RACProvider
# Global broadcast group, which messages are sent to when the outpost connects back
# to authentik for a specific connection
# The `RACClientConsumer` consumer adds itself to this group on connection,
# and removes itself once it has been assigned a specific outpost channel
RAC_CLIENT_GROUP = "group_enterprise_rac_client"
RAC_CLIENT_GROUP = "group_rac_client"
# A group for all connections in a given authentik session ID
# A disconnect message is sent to this group when the session expires/is deleted
RAC_CLIENT_GROUP_SESSION = "group_enterprise_rac_client_%(session)s"
RAC_CLIENT_GROUP_SESSION = "group_rac_client_%(session)s"
# A group for all connections with a specific token, which in almost all cases
# is just one connection, however this is used to disconnect the connection
# when the token is deleted
RAC_CLIENT_GROUP_TOKEN = "group_enterprise_rac_token_%(token)s" # nosec
RAC_CLIENT_GROUP_TOKEN = "group_rac_token_%(token)s" # nosec
# Step 1: Client connects to this websocket endpoint
# Step 2: We prepare all the connection args for Guac

View File

@ -3,7 +3,7 @@
from channels.exceptions import ChannelFull
from channels.generic.websocket import AsyncWebsocketConsumer
from authentik.enterprise.providers.rac.consumer_client import RAC_CLIENT_GROUP
from authentik.providers.rac.consumer_client import RAC_CLIENT_GROUP
class RACOutpostConsumer(AsyncWebsocketConsumer):

View File

@ -74,7 +74,7 @@ class RACProvider(Provider):
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.providers.rac.api.providers import RACProviderSerializer
return RACProviderSerializer
@ -100,7 +100,7 @@ class Endpoint(SerializerModel, PolicyBindingModel):
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.providers.rac.api.endpoints import EndpointSerializer
return EndpointSerializer
@ -129,7 +129,7 @@ class RACPropertyMapping(PropertyMapping):
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.rac.api.property_mappings import (
from authentik.providers.rac.api.property_mappings import (
RACPropertyMappingSerializer,
)

View File

@ -10,12 +10,12 @@ from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import User
from authentik.enterprise.providers.rac.api.endpoints import user_endpoint_cache_key
from authentik.enterprise.providers.rac.consumer_client import (
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
from authentik.providers.rac.consumer_client import (
RAC_CLIENT_GROUP_SESSION,
RAC_CLIENT_GROUP_TOKEN,
)
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
from authentik.providers.rac.models import ConnectionToken, Endpoint
@receiver(user_logged_out)

View File

@ -3,7 +3,7 @@
{% load authentik_core %}
{% block head %}
<script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon_url }}">

View File

@ -1,16 +1,9 @@
"""Test RAC Provider"""
from datetime import timedelta
from time import mktime
from unittest.mock import MagicMock, patch
from django.urls import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import License
from authentik.lib.generators import generate_id
@ -20,21 +13,8 @@ class TestAPI(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_create(self):
"""Test creation of RAC Provider"""
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.post(
reverse("authentik_api:racprovider-list"),

View File

@ -5,10 +5,10 @@ from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
class TestEndpointsAPI(APITestCase):

View File

@ -4,14 +4,14 @@ from django.test import TransactionTestCase
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.providers.rac.models import (
from authentik.lib.generators import generate_id
from authentik.providers.rac.models import (
ConnectionToken,
Endpoint,
Protocols,
RACPropertyMapping,
RACProvider,
)
from authentik.lib.generators import generate_id
class TestModels(TransactionTestCase):

View File

@ -1,23 +1,17 @@
"""RAC Views tests"""
from datetime import timedelta
from json import loads
from time import mktime
from unittest.mock import MagicMock, patch
from django.urls import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import License
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
from authentik.lib.generators import generate_id
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
class TestRACViews(APITestCase):
@ -39,21 +33,8 @@ class TestRACViews(APITestCase):
provider=self.provider,
)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_no_policy(self):
"""Test request"""
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.get(
reverse(
@ -70,18 +51,6 @@ class TestRACViews(APITestCase):
final_response = self.client.get(next_url)
self.assertEqual(final_response.status_code, 200)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_app_deny(self):
"""Test request (deny on app level)"""
PolicyBinding.objects.create(
@ -89,7 +58,6 @@ class TestRACViews(APITestCase):
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
order=0,
)
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.get(
reverse(
@ -99,18 +67,6 @@ class TestRACViews(APITestCase):
)
self.assertIsInstance(response, AccessDeniedResponse)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_endpoint_deny(self):
"""Test request (deny on endpoint level)"""
PolicyBinding.objects.create(
@ -118,7 +74,6 @@ class TestRACViews(APITestCase):
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
order=0,
)
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.get(
reverse(

View File

@ -4,14 +4,14 @@ from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.urls import path
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
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.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.providers.rac.api.endpoints import EndpointViewSet
from authentik.providers.rac.api.property_mappings import RACPropertyMappingViewSet
from authentik.providers.rac.api.providers import RACProviderViewSet
from authentik.providers.rac.consumer_client import RACClientConsumer
from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.providers.rac.views import RACInterface, RACStartView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -10,8 +10,6 @@ from django.utils.translation import gettext as _
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.views.interface import InterfaceView
from authentik.enterprise.policy import EnterprisePolicyAccessView
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint, RACProvider
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.exceptions import FlowNonApplicableException
@ -20,9 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(EnterprisePolicyAccessView):
class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint

View File

@ -87,6 +87,7 @@ TENANT_APPS = [
"authentik.providers.ldap",
"authentik.providers.oauth2",
"authentik.providers.proxy",
"authentik.providers.rac",
"authentik.providers.radius",
"authentik.providers.saml",
"authentik.providers.scim",

View File

@ -17,7 +17,7 @@ from rest_framework.serializers import ValidationError
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.exceptions import StageInvalidException
from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.models import FlowAuthenticationRequirement, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
@ -97,14 +97,27 @@ class EmailStageView(ChallengeStageView):
"""Helper function that sends the actual email. Implies that you've
already checked that there is a pending user."""
pending_user = self.get_pending_user()
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
# Pending user does not have a primary key, and we're in a recovery flow,
# which means the user entered an invalid identifier, so we pretend to send the
# email, to not disclose if the user exists
return
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, pending_user.email)
if FlowAuthenticationRequirement(
self.executor.flow.authentication
).possibly_unauthenticated:
# In possibly unauthenticated flows, do not disclose whether user or their email exists
# to prevent enumeration attacks
if not pending_user.pk:
self.logger.debug(
"User object does not exist. Email not sent.", pending_user=pending_user
)
return
if not email:
self.logger.debug(
"No recipient email address could be determined. Email not sent.",
pending_user=pending_user,
)
return
if not email:
email = pending_user.email
raise StageInvalidException(
"No recipient email address could be determined. Email not sent."
)
current_stage: EmailStage = self.executor.current_stage
token = self.get_token()
# Send mail to user
@ -133,7 +146,9 @@ class EmailStageView(ChallengeStageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
restore_token: FlowToken | None = self.executor.plan.context.get(
PLAN_CONTEXT_IS_RESTORED, None
)
user = self.get_pending_user()
if restore_token:
if restore_token.user != user:

View File

@ -801,6 +801,126 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racprovider"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.endpoint"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racpropertymapping"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
}
}
},
{
"type": "object",
"required": [
@ -3561,126 +3681,6 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racprovider"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.endpoint"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racpropertymapping"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
}
}
},
{
"type": "object",
"required": [
@ -4663,6 +4663,7 @@
"authentik.providers.ldap",
"authentik.providers.oauth2",
"authentik.providers.proxy",
"authentik.providers.rac",
"authentik.providers.radius",
"authentik.providers.saml",
"authentik.providers.scim",
@ -4703,7 +4704,6 @@
"authentik.enterprise.audit",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.rac",
"authentik.enterprise.providers.ssf",
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
"authentik.enterprise.stages.source",
@ -4738,6 +4738,9 @@
"authentik_providers_oauth2.scopemapping",
"authentik_providers_oauth2.oauth2provider",
"authentik_providers_proxy.proxyprovider",
"authentik_providers_rac.racprovider",
"authentik_providers_rac.endpoint",
"authentik_providers_rac.racpropertymapping",
"authentik_providers_radius.radiusprovider",
"authentik_providers_radius.radiusproviderpropertymapping",
"authentik_providers_saml.samlprovider",
@ -4807,9 +4810,6 @@
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
"authentik_providers_microsoft_entra.microsoftentraprovider",
"authentik_providers_microsoft_entra.microsoftentraprovidermapping",
"authentik_providers_rac.racprovider",
"authentik_providers_rac.endpoint",
"authentik_providers_rac.racpropertymapping",
"authentik_providers_ssf.ssfprovider",
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
"authentik_stages_source.sourcestage",
@ -6046,6 +6046,216 @@
}
}
},
"model_authentik_providers_rac.racprovider": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"authentication_flow": {
"type": "string",
"format": "uuid",
"title": "Authentication flow",
"description": "Flow used for authentication when the associated application is accessed by an un-authenticated user."
},
"authorization_flow": {
"type": "string",
"format": "uuid",
"title": "Authorization flow",
"description": "Flow used when authorizing this provider."
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"connection_expiry": {
"type": "string",
"minLength": 1,
"title": "Connection expiry",
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
},
"delete_token_on_disconnect": {
"type": "boolean",
"title": "Delete token on disconnect",
"description": "When set to true, connection tokens will be deleted upon disconnect."
}
},
"required": []
},
"model_authentik_providers_rac.racprovider_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racprovider",
"change_racprovider",
"delete_racprovider",
"view_racprovider"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.endpoint": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"provider": {
"type": "integer",
"title": "Provider"
},
"protocol": {
"type": "string",
"enum": [
"rdp",
"vnc",
"ssh"
],
"title": "Protocol"
},
"host": {
"type": "string",
"minLength": 1,
"title": "Host"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"auth_mode": {
"type": "string",
"enum": [
"static",
"prompt"
],
"title": "Auth mode"
},
"maximum_connections": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Maximum connections"
}
},
"required": []
},
"model_authentik_providers_rac.endpoint_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_endpoint",
"change_endpoint",
"delete_endpoint",
"view_endpoint"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.racpropertymapping": {
"type": "object",
"properties": {
"managed": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Managed by authentik",
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
},
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"expression": {
"type": "string",
"title": "Expression"
},
"static_settings": {
"type": "object",
"additionalProperties": true,
"title": "Static settings"
}
},
"required": []
},
"model_authentik_providers_rac.racpropertymapping_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racpropertymapping",
"change_racpropertymapping",
"delete_racpropertymapping",
"view_racpropertymapping"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_radius.radiusprovider": {
"type": "object",
"properties": {
@ -14215,216 +14425,6 @@
}
}
},
"model_authentik_providers_rac.racprovider": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"authentication_flow": {
"type": "string",
"format": "uuid",
"title": "Authentication flow",
"description": "Flow used for authentication when the associated application is accessed by an un-authenticated user."
},
"authorization_flow": {
"type": "string",
"format": "uuid",
"title": "Authorization flow",
"description": "Flow used when authorizing this provider."
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"connection_expiry": {
"type": "string",
"minLength": 1,
"title": "Connection expiry",
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
},
"delete_token_on_disconnect": {
"type": "boolean",
"title": "Delete token on disconnect",
"description": "When set to true, connection tokens will be deleted upon disconnect."
}
},
"required": []
},
"model_authentik_providers_rac.racprovider_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racprovider",
"change_racprovider",
"delete_racprovider",
"view_racprovider"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.endpoint": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"provider": {
"type": "integer",
"title": "Provider"
},
"protocol": {
"type": "string",
"enum": [
"rdp",
"vnc",
"ssh"
],
"title": "Protocol"
},
"host": {
"type": "string",
"minLength": 1,
"title": "Host"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"auth_mode": {
"type": "string",
"enum": [
"static",
"prompt"
],
"title": "Auth mode"
},
"maximum_connections": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Maximum connections"
}
},
"required": []
},
"model_authentik_providers_rac.endpoint_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_endpoint",
"change_endpoint",
"delete_endpoint",
"view_endpoint"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.racpropertymapping": {
"type": "object",
"properties": {
"managed": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Managed by authentik",
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
},
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"expression": {
"type": "string",
"title": "Expression"
},
"static_settings": {
"type": "object",
"additionalProperties": true,
"title": "Static settings"
}
},
"required": []
},
"model_authentik_providers_rac.racpropertymapping_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racpropertymapping",
"change_racpropertymapping",
"delete_racpropertymapping",
"view_racpropertymapping"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_ssf.ssfprovider": {
"type": "object",
"properties": {

View File

@ -6095,17 +6095,26 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/recovery/:
/core/users/{id}/recovery_link/:
post:
operationId: core_users_recovery_create
operationId: core_users_recovery_link_create
description: Create a temporary link that a user can use to recover their accounts
parameters:
- in: query
name: email_stage
schema:
type: string
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this User.
required: true
- in: query
name: token_duration
schema:
type: string
required: true
tags:
- core
security:
@ -6129,41 +6138,6 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/recovery_email/:
post:
operationId: core_users_recovery_email_create
description: Create a temporary link that a user can use to recover their accounts
parameters:
- in: query
name: email_stage
schema:
type: string
required: true
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this User.
required: true
tags:
- core
security:
- authentik: []
responses:
'204':
description: Successfully sent recover email
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/set_password/:
post:
operationId: core_users_set_password_create
@ -39482,6 +39456,7 @@ components:
- authentik.providers.ldap
- authentik.providers.oauth2
- authentik.providers.proxy
- authentik.providers.rac
- authentik.providers.radius
- authentik.providers.saml
- authentik.providers.scim
@ -39522,7 +39497,6 @@ components:
- authentik.enterprise.audit
- authentik.enterprise.providers.google_workspace
- authentik.enterprise.providers.microsoft_entra
- authentik.enterprise.providers.rac
- authentik.enterprise.providers.ssf
- authentik.enterprise.stages.authenticator_endpoint_gdtc
- authentik.enterprise.stages.source
@ -46625,6 +46599,9 @@ components:
- authentik_providers_oauth2.scopemapping
- authentik_providers_oauth2.oauth2provider
- authentik_providers_proxy.proxyprovider
- authentik_providers_rac.racprovider
- authentik_providers_rac.endpoint
- authentik_providers_rac.racpropertymapping
- authentik_providers_radius.radiusprovider
- authentik_providers_radius.radiusproviderpropertymapping
- authentik_providers_saml.samlprovider
@ -46694,9 +46671,6 @@ components:
- authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_microsoft_entra.microsoftentraprovider
- authentik_providers_microsoft_entra.microsoftentraprovidermapping
- authentik_providers_rac.racprovider
- authentik_providers_rac.endpoint
- authentik_providers_rac.racpropertymapping
- authentik_providers_ssf.ssfprovider
- authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage
- authentik_stages_source.sourcestage

View File

@ -74,7 +74,7 @@ const interfaces = [
["user/UserInterface.ts", "user"],
["flow/FlowInterface.ts", "flow"],
["standalone/api-browser/index.ts", "standalone/api-browser"],
["enterprise/rac/index.ts", "enterprise/rac"],
["rac/index.ts", "rac"],
["standalone/loading/index.ts", "standalone/loading"],
["polyfill/poly.ts", "."],
];

8
web/package-lock.json generated
View File

@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2024.12.3-1739814462",
"@goauthentik/api": "^2024.12.3-1739965710",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@ -1814,9 +1814,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2024.12.3-1739814462",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739814462.tgz",
"integrity": "sha512-qWGsq7zP0rG1PfjZA+iimaX4cVkd1n2JA/WceTOKgBmqnomQSI7SJNkdSpD+Qdy76PI0UuQWN73PInq/3rmm5Q=="
"version": "2024.12.3-1739965710",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739965710.tgz",
"integrity": "sha512-16zoQWeJhAFSwttvqLRoXoQA43tMW1ZXDEihW6r8rtWtlxqPh7n36RtcWYraYiLcjmJskI90zdgz6k1kmY5AXw=="
},
"node_modules/@goauthentik/web": {
"resolved": "",

View File

@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2024.12.3-1739814462",
"@goauthentik/api": "^2024.12.3-1739965710",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",

View File

@ -6,7 +6,7 @@ const config: KnipConfig = {
"./src/user/UserInterface.ts",
"./src/flow/FlowInterface.ts",
"./src/standalone/api-browser/index.ts",
"./src/enterprise/rac/index.ts",
"./src/rac/index.ts",
"./src/standalone/loading/index.ts",
"./src/polyfill/poly.ts",
],

View File

@ -7,6 +7,7 @@ import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/Alert.js";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
@ -120,7 +121,12 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
}
renderForm(): TemplateResult {
const alertMsg = msg(
"Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.",
);
return html`<form class="pf-c-form pf-m-horizontal">
<ak-alert level="pf-m-info">${alertMsg}</ak-alert>
<ak-text-input
name="name"
value=${ifDefined(this.instance?.name)}

View File

@ -50,7 +50,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
pageDescription(): string {
return msg(
str`External applications that use ${this.brand.brandingTitle || "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
str`External applications that use ${this.brand?.brandingTitle ?? "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
);
}
pageIcon(): string {
@ -85,10 +85,6 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
];
}
renderSectionBefore(): TemplateResult {
return html`<ak-application-wizard-hint></ak-application-wizard-hint>`;
}
renderSidebarAfter(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
@ -160,12 +156,21 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
renderObjectCreate(): TemplateResult {
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Application")} </span>
<ak-application-form slot="form"> </ak-application-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>`;
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
<button
slot="trigger"
class="pf-c-button pf-m-primary"
data-ouia-component-id="start-application-wizard"
>
${msg("Create with Provider")}
</button>
</ak-application-wizard>
<ak-forms-modal .open=${getURLParam("createForm", false)}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Application")} </span>
<ak-application-form slot="form"> </ak-application-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>`;
}
}

View File

@ -30,7 +30,7 @@ export class ApplicationWizardStep extends WizardStep {
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes.
wizardTitle = msg("New application");
wizardDescription = msg("Create a new application");
wizardDescription = msg("Create a new application and configure a provider for it.");
canCancel = true;
// This should be overridden in the children for more precise targeting.

View File

@ -2,11 +2,14 @@ import "@goauthentik/admin/users/ServiceAccountForm";
import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserImpersonateForm";
import {
renderRecoveryEmailRequest,
renderRecoveryLinkRequest,
} from "@goauthentik/admin/users/UserListPage";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm";
import "@goauthentik/admin/users/UserRecoveryLinkForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
import { MessageLevel } from "@goauthentik/common/messages";
import { me } from "@goauthentik/common/users";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
@ -21,7 +24,6 @@ import "@goauthentik/elements/forms/DeleteBulkForm";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
@ -37,14 +39,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import {
CoreApi,
CoreUsersListTypeEnum,
Group,
ResponseError,
SessionUser,
User,
} from "@goauthentik/api";
import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api";
@customElement("ak-user-related-add")
export class RelatedUserAdd extends Form<{ users: number[] }> {
@ -301,60 +296,11 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
${msg("Set password")}
</button>
</ak-forms-modal>
${this.brand?.flowRecovery
${this.brand.flowRecovery
? html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryCreate({
id: item.pk,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: msg(
"Successfully generated recovery link",
),
description: rec.link,
});
})
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: msg(
"No recovery flow is configured.",
),
});
});
});
}}
>
${msg("Copy recovery link")}
</ak-action-button>
${renderRecoveryLinkRequest(item)}
${item.email
? html`<ak-forms-modal
.closeAfterSuccessfulSubmit=${false}
>
<span slot="submit">
${msg("Send link")}
</span>
<span slot="header">
${msg("Send recovery link to user")}
</span>
<ak-user-reset-email-form
slot="form"
.user=${item}
>
</ak-user-reset-email-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary"
>
${msg("Email recovery link")}
</button>
</ak-forms-modal>`
? renderRecoveryEmailRequest(item)
: html`<span
>${msg(
"Recovery link cannot be emailed, user has no email address saved.",
@ -363,7 +309,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
`
: html` <p>
${msg(
"To let a user directly reset a their password, configure a recovery flow on the currently active brand.",
"To let a user directly reset their password, configure a recovery flow on the currently active brand.",
)}
</p>`}
</div>

View File

@ -4,11 +4,10 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserImpersonateForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm";
import "@goauthentik/admin/users/UserRecoveryLinkForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
import { userTypeToLabel } from "@goauthentik/common/labels";
import { MessageLevel } from "@goauthentik/common/messages";
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import { getRelativeTime } from "@goauthentik/common/utils";
@ -23,12 +22,10 @@ import "@goauthentik/elements/TreeView";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg, str } from "@lit/localize";
@ -39,40 +36,24 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api";
import { CoreApi, SessionUser, User, UserPath } from "@goauthentik/api";
export const requestRecoveryLink = (user: User) =>
new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryCreate({
id: user.pk,
})
.then((rec) =>
writeToClipboard(rec.link).then((wroteToClipboard) =>
showMessage({
level: MessageLevel.success,
message: rec.link,
description: wroteToClipboard
? msg("A copy of this recovery link has been placed in your clipboard")
: "",
}),
),
)
.catch((ex: ResponseError) =>
ex.response.json().then(() =>
showMessage({
level: MessageLevel.error,
message: msg(
"The current brand must have a recovery flow configured to use a recovery link",
),
}),
),
);
export const renderRecoveryLinkRequest = (user: User) =>
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-link-recovery-request">
<span slot="submit"> ${msg("Create link")} </span>
<span slot="header"> ${msg("Create recovery link")} </span>
<ak-user-recovery-link-form slot="form" .user=${user}> </ak-user-recovery-link-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Create recovery link")}
</button>
</ak-forms-modal>`;
export const renderRecoveryEmailRequest = (user: User) =>
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
<span slot="submit"> ${msg("Send link")} </span>
<span slot="header"> ${msg("Send recovery link to user")} </span>
<ak-user-reset-email-form slot="form" .user=${user}> </ak-user-reset-email-form>
<ak-user-recovery-link-form slot="form" .user=${user} .withEmailStage=${true}>
</ak-user-recovery-link-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Email recovery link")}
</button>
@ -362,12 +343,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
</ak-forms-modal>
${this.brand.flowRecovery
? html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => requestRecoveryLink(item)}
>
${msg("Create recovery link")}
</ak-action-button>
${renderRecoveryLinkRequest(item)}
${item.email
? renderRecoveryEmailRequest(item)
: html`<span

View File

@ -0,0 +1,104 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-text-input";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
CoreApi,
CoreUsersRecoveryLinkCreateRequest,
Link,
Stage,
StagesAllListRequest,
StagesApi,
User,
} from "@goauthentik/api";
@customElement("ak-user-recovery-link-form")
export class UserRecoveryLinkForm extends Form<CoreUsersRecoveryLinkCreateRequest> {
@property({ attribute: false })
user!: User;
@property({ type: Boolean })
withEmailStage = false;
async send(data: CoreUsersRecoveryLinkCreateRequest): Promise<Link> {
data.id = this.user.pk;
const response = await new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryLinkCreate(data);
if (this.withEmailStage) {
this.successMessage = msg("Successfully sent email.");
} else {
const wroteToClipboard = await writeToClipboard(response.link);
if (wroteToClipboard) {
this.successMessage = msg(
`A copy of this recovery link has been placed in your clipboard: ${response.link}`,
);
} else {
this.successMessage = msg(
`authentik does not have access to your clipboard, please copy the recovery link manually: ${response.link}`,
);
}
}
return response;
}
renderEmailStageInput(): TemplateResult {
if (!this.withEmailStage) return html``;
return html`
<ak-form-element-horizontal name="emailStage" label=${msg("Email stage")} required>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesAllListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) => {
return groupBy(items, (stage) => stage.verboseNamePlural);
}}
.renderElement=${(stage: Stage): string => {
return stage.name;
}}
.value=${(stage: Stage | undefined): string | undefined => {
return stage?.pk;
}}
>
</ak-search-select>
</ak-form-element-horizontal>
`;
}
renderForm(): TemplateResult {
return html`
${this.renderEmailStageInput()}
<ak-text-input
name="tokenDuration"
label=${msg("Token duration")}
required
value="days=1"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Duration for generated token")}
</p>`}
>
</ak-text-input>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-user-recovery-link-form": UserRecoveryLinkForm;
}
}

View File

@ -1,70 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { groupBy } from "@goauthentik/common/utils";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
CoreApi,
CoreUsersRecoveryEmailCreateRequest,
Stage,
StagesAllListRequest,
StagesApi,
User,
} from "@goauthentik/api";
@customElement("ak-user-reset-email-form")
export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailCreateRequest> {
@property({ attribute: false })
user!: User;
getSuccessMessage(): string {
return msg("Successfully sent email.");
}
async send(data: CoreUsersRecoveryEmailCreateRequest): Promise<void> {
data.id = this.user.pk;
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailCreate(data);
}
renderForm(): TemplateResult {
return html`<ak-form-element-horizontal
label=${msg("Email stage")}
?required=${true}
name="emailStage"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesAllListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) => {
return groupBy(items, (stage) => stage.verboseNamePlural);
}}
.renderElement=${(stage: Stage): string => {
return stage.name;
}}
.value=${(stage: Stage | undefined): string | undefined => {
return stage?.pk;
}}
>
</ak-search-select>
</ak-form-element-horizontal>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-user-reset-email-form": UserResetEmailForm;
}
}

View File

@ -8,7 +8,7 @@ import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserImpersonateForm";
import {
renderRecoveryEmailRequest,
requestRecoveryLink,
renderRecoveryLinkRequest,
} from "@goauthentik/admin/users/UserListPage";
import "@goauthentik/admin/users/UserPasswordForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
@ -110,11 +110,8 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
.ak-button-collection > * {
flex: 1 0 100%;
}
#reset-password-button {
margin-right: 0;
}
#ak-email-recovery-request,
#ak-link-recovery-request .pf-c-button,
#update-password-request .pf-c-button,
#ak-email-recovery-request .pf-c-button {
margin: 0;
@ -248,18 +245,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-action-button
id="reset-password-button"
class="pf-m-secondary pf-m-block"
.apiRequest=${() => requestRecoveryLink(user)}
>
<pf-tooltip
position="top"
content=${msg("Create a link for this user to reset their password")}
>
${msg("Create Recovery Link")}
</pf-tooltip>
</ak-action-button>
${renderRecoveryLinkRequest(user)}
${user.email ? renderRecoveryEmailRequest(user) : nothing}
</div> `;
}

View File

@ -42,7 +42,7 @@ export class LibraryPageApplicationEmptyList extends AKElement {
renderNewAppButton() {
const href = paramURL("/core/applications", {
createForm: true,
createWizard: true,
});
return html`
<div class="pf-u-pt-lg">

View File

@ -116,8 +116,13 @@ export class LibraryPage extends AKElement {
@bound
launchRequest(event: LibraryPageSearchSelected) {
event.stopPropagation();
if (this.selectedApp?.launchUrl) {
if (!this.selectedApp?.launchUrl) {
return;
}
if (!this.selectedApp.openInNewTab) {
window.location.assign(this.selectedApp?.launchUrl);
} else {
window.open(this.selectedApp.launchUrl);
}
}

View File

@ -89,7 +89,7 @@ export async function findWizardTitle() {
async function passByPoliciesAndCommit() {
const title = await findWizardTitle();
// Expect to be on the Bindings panel
await expect(await title.getText()).toEqual("Configure Policy Bindings");
await expect(await title.getText()).toEqual("Configure Policy/User/Group Bindings");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await (await ApplicationWizardView.submitPage()).waitForDisplayed();

View File

@ -0,0 +1,171 @@
---
title: Release 2025.2
slug: "/releases/2025.2"
---
:::::note
2025.2 has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2025.2.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
:::::
## Highlights
- **SSF Provider <span class="badge badge--primary">Enterprise</span> <span class="badge badge--info">Preview</span>** Add support for Shared Signals Framework
TODO: Add preview banner to UI
- **RAC moved open source** Remote access is now available to everyone!
- **GeoIP distance and impossible travel checks** Add the ability to check for the distance a user has moved compared to a previous login, and if the user could have travelled the distance
- **Email OTP Stage** Allow users to use their email accounts as a one-time-password during authentication
- **Fine-grained permission for superuser toggle on groups** Setting the **Is superuser** toggle on a group now requires a separate permission.
## Breaking changes
- **Deprecated and frozen `:latest` container image tag after 2025.2**
Using the `:latest` tag with container images is not recommended as it can lead to unintentional updates and potentially broken setups.
The tag will not be removed, however it will also not be updated past 2025.2.
We strongly recommended the use of a specific version tag for authentik instances' container images like `:2025.2`.
## New features
- SSF Provider <span class="badge badge--primary">Enterprise</span> <span class="badge badge--info">Preview</span>
[Shared Signals Framework](#todo) allows applications to register a stream with authentik within which they can received events from authentik such as when a session was revoked or a credential was add/changed/deleted and execute actions based on these events.
This allows admins to integrate authentik with Apple Business/School Manager for federated Apple IDs. See the integration docs [here](#todo)
- RAC to open source
Remote access (RDP, VNC and SSH) has moved from enterprise to our free, open source code. We try our best to limit enterprise-specific functionality to features that would be non-essential to homelab users and far more valuable to enterprise use cases. We've had a variety of homelab users reach out with excellent use cases for RAC functionality, so while this will mean giving up some potential revenue, we think that opening up RAC to the community is the right thing to do!
- GeoIP distance and impossible travel checks
Add the ability to check for the distance a user has moved compared to a previous login, and add the option to check impossible travel distances based on client IP.
These options can be used to detect and prevent access from potentially stolen authentik sessions or stolen devices.
- Email OTP Stage
Admins now have the ability to configure the option for users to use their email as an authenticator. Users that already have an email address set on their account will be able to use that address to receive one-time-passwords. It is also possible to configure authentik to allow users to add additional email addresses as authenticators.
See [Email OTP Stage](#todo)
- Application Wizard is the default way to create applications
The default way of creating an application now allows admins to configure the provider and any kind of bindings without having to jump through different sections of the UI. The previous way of creating an application is and will stay available alongside the new and streamlined method.
- Fine-grained permission for superuser toggle on groups
Setting the **Is superuser** toggle on a group now requires a separate permission, making it much easier to allow for delegated management of groups without risking the ability for users to self-elevate permissions.
- Improved debugging experience
For people developing authentik or building very complex, custom integrations, configuring debugging in authentik is now documented [here](#todo)
## TODO
temp
## Upgrading
This release does not introduce any new requirements. You can follow the upgrade instructions below; for more detailed information about upgrading authentik, refer to our [Upgrade documentation](../../install-config/upgrade.mdx).
:::warning
When you upgrade, be aware that the version of the authentik instance and of any outposts must be the same. We recommended that you always upgrade any outposts at the same time you upgrade your authentik instance.
:::
### Docker Compose
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
```shell
wget -O docker-compose.yml https://goauthentik.io/version/2025.2/docker-compose.yml
docker compose up -d
```
The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name.
### Kubernetes
Upgrade the Helm Chart to the new version, using the following commands:
```shell
helm repo update
helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.2
```
## Minor changes/fixes
- admin: monitor worker version (#12463)
- api: cleanup owner permissions (#12598)
- blueprints: add REPL for blueprint YAML tags (#9223)
- blueprints: fix schema for meta models (#12421)
- core: add indexes on ExpiringModel (#12658)
- core: fix application entitlements not creatable with blueprints (#12673)
- core: fix error when creating new user with default path (#12609)
- core: fix generic sources not being fetchable by pk (#12896)
- core: fix permissions for admin device listing (#12787)
- core: search users' attributes (#12740)
- core: show last password change date (#12958)
- enterprise/providers: SSF (#12327)
- enterprise/providers/SSF: fix a couple of bugs after real world testing (#12987)
- enterprise/rac: Improve client connection status & bugfixes (#12684)
- events: make sure password set event has the correct IP (#12585)
- events: notification_cleanup: avoid unnecessary loop (#12417)
- flows: clear flow state before redirecting to final URL (#12788)
- flows: fix history containing other plans (#12655)
- flows: fix inspector permission check (#12907)
- flows: more tests (#11587)
- flows: show policy messages in reevaluate marker (#12855)
- flows/inspector: add button to open flow inspector (#12656)
- internal: fix missing trailing slash in outpost websocket (#12470)
- internal: fix URL generation for websocket connection (#12439)
- lifecycle: update python to 3.12.8 (#12783)
- lifecycle/migrate: don't migrate tenants if not enabled (#12850)
- outposts: fix version label (#12486)
- providers/oauth2: include scope in token response (#12921)
- providers/oauth2: support token revocation for public clients (#12704)
- providers/saml: fix handle Accept: application/xml for SAML Metadata endpoint (#12483) (#12518)
- providers/saml: fix invalid SAML Response when assertion and response are signed (#12611)
- providers/saml: provide generic metadata url when possible (#12413)
- rbac: exclude permissions for internal models (#12803)
- rbac: permissions endpoint: allow authenticated users (#12608)
- root: backport version bump (#12426)
- root: docker: ensure apt packages are up-to-date (#12683)
- root: expose CONN_MAX_AGE, CONN_HEALTH_CHECKS and DISABLE_SERVER_SIDE_CURSORS for PostgreSQL config (#10159)
- root: fix dev build version being invalid semver (#12472)
- root: redis, make sure tlscacert isn't an empty string (#12407)
- sources: allow uuid or slug to be used for retrieving a source (#12780)
- sources: allow uuid or slug to be used for retrieving a source (2024.12 fix) (#12772)
- sources/kerberos: authenticate with the user's username instead of the first username in authentik (#12497)
- sources/kerberos: handle principal expire time (#12748)
- sources/oauth: fix authentication only being sent in form body (#12713)
- sources/scim: fix user creation (duplicate userName) (#12547)
- stages/authenticator: add user field to devices (#12636)
- stages/prompt: always show policy messages (#12765)
- stages/redirect: fix query parameter when redirecting to flow (#12750)
- web, core: fix grammatical issue in stage bindings (#10799)
- web: fix build dev build (#12473)
- web: fix error handling bug in ApplicationWizard.RACProviderForm (#12640)
- web: Fix issue where Codemirror partially applies OneDark theme. (#12811)
- web: fix mobile scrolling bug (#12601)
- web: fix source selection and outpost integration health (#12530)
- web: fix source selection and outpost integration health (#12530)
- web: fixes broken docLinks - url missing s (#12789)
- web: housekeeping, optimizations and small fixes (#12450)
- web: improve notification and API drawers (#12659)
- web: misc fixes for admin and flow inspector (#12461)
- web: only load version context when authenticated (#12482)
- web: update gen-client-ts to OpenAPI 7.11.0 (#12756)
- web/admin: fix role changelog missing primary key filter (#12671)
- web/admin: improve user display view (#12988)
- web/admin: more cleanup and consistency (#12657)
- web/admin: Refine navigation (#12441)
- web/components: ak-number-input: add support for min (#12703)
- web/flows: fix `login` / `log in` inconsistency (#12526)
## API Changes
<!-- _Insert output of `make gen-diff` here_ -->

View File

@ -15,7 +15,7 @@ sidebar_label: Slack
The following placeholder will be used:
- You can use <kbd>slack.<em>company</em>></kbd> or <kbd><em>my-workspace</em>.slack.com</kbd> as the FQDN of your Slack instance.
- You can use <kbd>slack.<em>company</em></kbd> or <kbd><em>my-workspace</em>.slack.com</kbd> as the FQDN of your Slack instance.
- You can use <kbd>authentik.company</kbd> as the FQDN of the authentik installation.
:::note