Compare commits

...

24 Commits

Author SHA1 Message Date
ed83c2b0b1 release: 2025.2.0-rc3 2025-02-23 16:02:45 +01:00
af780deb27 core: add darkreader-lock (cherry-pick #13183) (#13184)
core: add darkreader-lock (#13183)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-23 04:53:09 +01:00
a4be38567f web/admin: fix default selection for binding policy (cherry-pick #13180) (#13182)
web/admin: fix default selection for binding policy (#13180)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-23 04:20:04 +01:00
39aafbb34a web/flow: grab focus to uid input field (cherry-pick #13177) (#13178)
web/flow: grab focus to uid input field (#13177)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-23 00:52:37 +01:00
07eb5fe533 web/flow: update default flow background (cherry-pick #13175) (#13176)
web/flow: update default flow background (#13175)

* web/flow: update default flow background



* Optimised images with calibre/image-actions

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-02-22 23:37:10 +01:00
301a89dd92 web/admin: only show message when not editing an application (cherry-pick #13165) (#13168)
web/admin: only show message when not editing an application (#13165)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 23:37:14 +01:00
cd6d0a47f3 web/user: fix race condition in user settings flow executor (cherry-pick #13163) (#13169)
web/user: fix race condition in user settings flow executor (#13163)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 23:36:59 +01:00
8a23eaef1e web/user: fix RAC launch not opening when clicking icon (cherry-pick #13164) (#13166)
web/user: fix RAC launch not opening when clicking icon (#13164)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 19:21:40 +01:00
8f285fbcc5 web: Indicate when caps-lock is active during password input. (cherry-pick #12733) (#13160)
web: Indicate when caps-lock is active during password input. (#12733)

Determining the state of the caps-lock key can be tricky as we're
dependant on a user-provided input to set a value. Thus, our initial
state defaults to not display any warning until the first keystroke.

- Revise to better use lit-html.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-02-21 17:09:35 +01:00
5d391424f7 web/user: fix post MFA creation link being invalid (cherry-pick #13157) (#13159)
web/user: fix post MFA creation link being invalid (#13157)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 17:01:09 +01:00
2de11f8a69 release: 2025.2.0-rc2 2025-02-20 23:47:15 +01:00
b2dcf94aba policies/geoip: fix math in impossible travel (cherry-pick #13141) (#13145)
policies/geoip: fix math in impossible travel (#13141)

* policies/geoip: fix math in impossible travel



* fix threshold



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 23:46:21 +01:00
adb532fc5d enterprise/stages/source: fix Source stage not executing authentication/enrollment flow (cherry-pick #12875) (#13146)
enterprise/stages/source: fix Source stage not executing authentication/enrollment flow (#12875)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 23:45:43 +01:00
5d3b35d1ba revert: rbac: exclude permissions for internal models (#12803) (cherry-pick #13138) (#13140)
revert: rbac: exclude permissions for internal models (#12803) (#13138)

Revert "rbac: exclude permissions for internal models (#12803)"

This reverts commit e08ccf4ca0.

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 16:07:21 +01:00
433a94d9ee web/flows: fix error on interactive Captcha stage when retrying captcha (cherry-pick #13119) (#13139)
web/flows: fix error on interactive Captcha stage when retrying captcha (#13119)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 15:12:03 +01:00
f28d622d10 cmd: set version in outposts (cherry-pick #13116) (#13122)
cmd: set version in outposts (#13116)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 18:20:28 +01:00
50a68c22c5 sources/oauth: add group sync for azure_ad (cherry-pick #12894) (#13123)
sources/oauth: add group sync for azure_ad (#12894)

* sources/oauth: add group sync for azure_ad



* make group sync optional



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 18:20:16 +01:00
13c99c8546 web/user: fix opening application with Enter not respecting new tab setting (cherry-pick #13115) (#13118)
web/user: fix opening application with Enter not respecting new tab setting (#13115)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 17:57:18 +01:00
7243add30f web/admin: update Application Wizard button placement (cherry-pick #12771) (#13121)
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.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2025-02-19 17:57:03 +01:00
6611a64a62 web: bump API Client version (cherry-pick #13113) (#13114)
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:48 +01:00
5262f61483 providers/rac: move to open source (cherry-pick #13015) (#13112)
providers/rac: move to open source (#13015)

* move RAC to open source

* move web out of enterprise



* remove enterprise license requirements from RAC

* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-02-19 13:16:18 +01:00
9dcbb4af9e release: 2025.2.0-rc1 2025-02-19 02:36:48 +01:00
0665bfac58 website/docs: add 2025.2 release notes (cherry-pick #13002) (#13108)
website/docs: add 2025.2 release notes (#13002)

* website/docs: add 2025.2 release notes



* make compile



* ffs



* ffs



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 02:12:47 +01:00
790e0c4d80 core: clear expired database sessions (cherry-pick #13105) (#13106)
core: clear expired database sessions (#13105)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-18 23:22:21 +01:00
84 changed files with 1118 additions and 690 deletions

View File

@ -1,16 +1,16 @@
[bumpversion]
current_version = 2024.12.3
current_version = 2025.2.0-rc3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
serialize =
serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch}
message = release: {new_version}
tag_name = version/{new_version}
[bumpversion:part:rc_t]
values =
values =
rc
final
optional_value = final

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.12.3"
__version__ = "2025.2.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

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

@ -35,8 +35,7 @@ from authentik.flows.planner import (
FlowPlanner,
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values
@ -47,8 +46,9 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
LOGGER = get_logger()
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
class MessageStage(StageView):
@ -219,28 +219,28 @@ class SourceFlowManager:
}
)
flow_context.update(self.policy_context)
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
plan.context.update(flow_context)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
if stages:
for stage in stages:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
flow_slug = token.flow.slug
token.delete()
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow_slug,
)
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
if not flow:
# We only check for the flow token here if we don't have a flow, otherwise we rely on
# SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add
# stages that deal with this token to return to another flow
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info(
"Replacing source flow with overridden flow", flow=token.flow.slug
)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
plan.context.update(flow_context)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
if stages:
for stage in stages:
plan.append_stage(stage)
redirect = plan.to_redirect(self.request, token.flow)
token.delete()
return redirect
return bad_request_message(
self.request,
_("Configured flow does not exist."),
@ -259,6 +259,8 @@ class SourceFlowManager:
if stages:
for stage in stages:
plan.append_stage(stage)
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
plan.append_stage(stage)
return plan.to_redirect(self.request, flow)
def handle_auth(
@ -295,6 +297,8 @@ class SourceFlowManager:
# When request isn't authenticated we jump straight to auth
if not self.request.user.is_authenticated:
return self.handle_auth(connection)
# When an override flow token exists we actually still use a flow for link
# to continue the existing flow we came from
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
return self._prepare_flow(None, connection)
connection.save()

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

@ -8,6 +8,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
<meta name="darkreader-lock">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">

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

@ -9,13 +9,16 @@ from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Source, User
from authentik.core.sources.flow_manager import SESSION_KEY_OVERRIDE_FLOW_TOKEN
from authentik.core.sources.flow_manager import (
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
SESSION_KEY_SOURCE_FLOW_STAGES,
)
from authentik.core.types import UILoginButton
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.models import FlowToken
from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
from authentik.flows.stage import ChallengeStageView
from authentik.flows.stage import ChallengeStageView, StageView
from authentik.lib.utils.time import timedelta_from_string
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
@ -49,6 +52,7 @@ class SourceStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
resume_token = self.create_flow_token()
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
return self.login_button.challenge
def create_flow_token(self) -> FlowToken:
@ -77,3 +81,19 @@ class SourceStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return self.executor.stage_ok()
class SourceStageFinal(StageView):
"""Dynamic stage injected in the source flow manager. This is injected in the
flow the source flow manager picks (authentication or enrollment), and will run at the end.
This stage uses the override flow token to resume execution of the initial flow the
source stage is bound to."""
def dispatch(self):
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
response = plan.to_redirect(self.request, token.flow)
token.delete()
return response

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

@ -128,7 +128,7 @@ class GeoIPPolicy(Policy):
(geoip_data["lat"], geoip_data["long"]),
)
if self.check_history_distance and dist.km >= (
self.history_max_distance_km - self.distance_tolerance_km
self.history_max_distance_km + self.distance_tolerance_km
):
return PolicyResult(
False, _("Distance from previous authentication is larger than threshold.")
@ -139,7 +139,7 @@ class GeoIPPolicy(Policy):
# clamped to be at least 1 hour
rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1)
if self.check_impossible_travel and dist.km >= (
(MAX_DISTANCE_HOUR_KM * rel_time_hours) - self.distance_tolerance_km
(MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km
):
return PolicyResult(False, _("Distance is further than possible."))
return PolicyResult(True)

View File

@ -148,10 +148,10 @@ class PasswordPolicy(Policy):
user_inputs.append(request.user.email)
if request.http_request:
user_inputs.append(request.http_request.brand.branding_title)
# Only calculate result for the first 100 characters, as with over 100 char
# Only calculate result for the first 72 characters, as with over 100 char
# long passwords we can be reasonably sure that they'll surpass the score anyways
# See https://github.com/dropbox/zxcvbn#runtime-latency
results = zxcvbn(password[:100], user_inputs)
results = zxcvbn(password[:72], user_inputs)
LOGGER.debug("password failed", check="zxcvbn", score=results["score"])
result = PolicyResult(results["score"] > self.zxcvbn_score_threshold)
if not result.passing:

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

@ -2,7 +2,7 @@
from django.apps import apps
from django.contrib.auth.models import Permission
from django.db.models import Q, QuerySet
from django.db.models import QuerySet
from django_filters.filters import ModelChoiceFilter
from django_filters.filterset import FilterSet
from django_filters.rest_framework import DjangoFilterBackend
@ -18,7 +18,6 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.blueprints.v1.importer import excluded_models
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User
from authentik.lib.validators import RequiredTogetherValidator
@ -106,13 +105,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
]
def get_queryset(self) -> QuerySet:
query = Q()
for model in excluded_models():
query |= Q(
content_type__app_label=model._meta.app_label,
content_type__model=model._meta.model_name,
return (
Permission.objects.all()
.select_related("content_type")
.filter(
content_type__app_label__startswith="authentik",
)
return Permission.objects.all().select_related("content_type").exclude(query)
)
class PermissionAssignSerializer(PassiveSerializer):

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

@ -2,6 +2,7 @@
from typing import Any
from requests import RequestException
from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
@ -21,10 +22,35 @@ class AzureADOAuthRedirect(OAuthRedirect):
}
class AzureADClient(UserprofileHeaderAuthClient):
"""Fetch AzureAD group information"""
def get_profile_info(self, token):
profile_data = super().get_profile_info(token)
if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes:
return profile_data
group_response = self.session.request(
"get",
"https://graph.microsoft.com/v1.0/me/memberOf",
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
)
try:
group_response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to fetch user profile",
exc=exc,
response=exc.response.text if exc.response else str(exc),
)
return None
profile_data["raw_groups"] = group_response.json()
return profile_data
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
"""AzureAD OAuth2 Callback"""
client_class = UserprofileHeaderAuthClient
client_class = AzureADClient
def get_user_id(self, info: dict[str, str]) -> str:
# Default try to get `id` for the Graph API endpoint
@ -53,8 +79,24 @@ class AzureADType(SourceType):
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
# Format group info
groups = []
group_id_dict = {}
for group in info.get("raw_groups", {}).get("value", []):
if group["@odata.type"] != "#microsoft.graph.group":
continue
groups.append(group["id"])
group_id_dict[group["id"]] = group
info["raw_groups"] = group_id_dict
return {
"username": info.get("userPrincipalName"),
"email": mail,
"name": info.get("displayName"),
"groups": groups,
}
def get_base_group_properties(self, source, group_id, **kwargs):
raw_group = kwargs["info"]["raw_groups"][group_id]
return {
"name": raw_group["displayName"],
}

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.12.3 Blueprint schema",
"title": "authentik 2025.2.0 Blueprint schema",
"required": [
"version",
"entries"
@ -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

@ -10,6 +10,7 @@ import (
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -24,7 +25,8 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -10,6 +10,7 @@ import (
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -27,7 +28,8 @@ Optionally, you can set these:
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -23,7 +24,8 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -23,7 +24,8 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0}
restart: unless-stopped
command: server
environment:
@ -54,7 +54,7 @@ services:
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0}
restart: unless-stopped
command: worker
environment:

View File

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

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2024.12.3
Default: 2025.2.0
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

View File

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

View File

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

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.12.3
version: 2025.2.0
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -39482,6 +39482,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 +39523,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 +46625,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 +46697,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,
@ -21,7 +22,7 @@ import "@goauthentik/elements/forms/SearchSelect";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -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">
${this.instance ? nothing : html`<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

@ -31,9 +31,9 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
@property({ type: Array })
allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.policy,
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
PolicyBindingCheckTarget.policy,
];
@property({ type: Array })

View File

@ -58,9 +58,9 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
@property({ type: Array })
allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.policy,
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
PolicyBindingCheckTarget.policy,
];
@property({ type: Array })

View File

@ -105,6 +105,22 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Maximum distance")}
name="historyMaxDistanceKm"
>
<input
type="number"
min="1"
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"Maximum distance a login attempt is allowed from in kilometers.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Distance tolerance")}
name="distanceToleranceKm"
@ -133,27 +149,6 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
${msg("Amount of previous login events to check against.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Maximum distance")}
name="historyMaxDistanceKm"
>
<input
type="number"
min="1"
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"Maximum distance a login attempt is allowed from in kilometers.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Distance settings (Impossible travel)")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="checkImpossibleTravel">
<label class="pf-c-switch">
<input

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 KiB

After

Width:  |  Height:  |  Size: 628 KiB

View File

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

View File

@ -13,6 +13,7 @@ export interface GlobalAuthentik {
build: string;
api: {
base: string;
relBase: string;
};
}
@ -27,6 +28,7 @@ export function globalAK(): GlobalAuthentik {
ak.brand = CurrentBrandFromJSON(ak.brand);
ak.config = ConfigFromJSON(ak.config);
}
const apiBase = new URL(process.env.AK_API_BASE_PATH || window.location.origin);
if (!ak) {
return {
config: ConfigFromJSON({
@ -39,7 +41,8 @@ export function globalAK(): GlobalAuthentik {
versionSubdomain: "",
build: "",
api: {
base: process.env.AK_API_BASE_PATH || window.location.origin,
base: apiBase.toString(),
relBase: apiBase.pathname,
},
};
}

View File

@ -45,6 +45,8 @@ html > form > input {
left: -2000px;
}
/*#region Icons*/
.pf-icon {
display: inline-block;
font-style: normal;
@ -54,6 +56,18 @@ html > form > input {
vertical-align: middle;
}
.pf-c-form-control {
--pf-c-form-control--m-caps-lock--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23aaabac' viewBox='0 0 56 56' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 20.7812 37.6211 L 35.2421 37.6211 C 38.5233 37.6211 40.2577 35.6992 40.2577 32.6055 L 40.2577 28.4570 L 49.1404 28.4570 C 51.0859 28.4570 52.6329 27.3086 52.6329 25.5039 C 52.6329 24.4024 52.0703 23.5351 51.0158 22.6211 L 30.9062 4.8789 C 29.9452 4.0351 29.0546 3.4727 27.9999 3.4727 C 26.9687 3.4727 26.0780 4.0351 25.1171 4.8789 L 4.9843 22.6445 C 3.8828 23.6055 3.3671 24.4024 3.3671 25.5039 C 3.3671 27.3086 4.9140 28.4570 6.8828 28.4570 L 15.7421 28.4570 L 15.7421 32.6055 C 15.7421 35.6992 17.4999 37.6211 20.7812 37.6211 Z M 21.1562 34.0820 C 20.2655 34.0820 19.6562 33.4961 19.6562 32.6055 L 19.6562 25.7149 C 19.6562 25.1524 19.4452 24.9180 18.8828 24.9180 L 8.6640 24.9180 C 8.4999 24.9180 8.4296 24.8476 8.4296 24.7305 C 8.4296 24.6367 8.4530 24.5430 8.5702 24.4492 L 27.5077 7.9961 C 27.7187 7.8086 27.8359 7.7383 27.9999 7.7383 C 28.1640 7.7383 28.3046 7.8086 28.4921 7.9961 L 47.4532 24.4492 C 47.5703 24.5430 47.5939 24.6367 47.5939 24.7305 C 47.5939 24.8476 47.4998 24.9180 47.3356 24.9180 L 37.1406 24.9180 C 36.5780 24.9180 36.3671 25.1524 36.3671 25.7149 L 36.3671 32.6055 C 36.3671 33.4727 35.7109 34.0820 34.8671 34.0820 Z M 19.7733 52.5273 L 36.0624 52.5273 C 38.7577 52.5273 40.3046 51.0273 40.3046 48.3086 L 40.3046 44.9336 C 40.3046 42.2148 38.7577 40.6680 36.0624 40.6680 L 19.7733 40.6680 C 17.0546 40.6680 15.5077 42.2383 15.5077 44.9336 L 15.5077 48.3086 C 15.5077 51.0039 17.0546 52.5273 19.7733 52.5273 Z M 20.3124 49.2227 C 19.4921 49.2227 19.0468 48.8008 19.0468 47.9805 L 19.0468 45.2617 C 19.0468 44.4414 19.4921 43.9727 20.3124 43.9727 L 35.5233 43.9727 C 36.3202 43.9727 36.7655 44.4414 36.7655 45.2617 L 36.7655 47.9805 C 36.7655 48.8008 36.3202 49.2227 35.5233 49.2227 Z'/%3E%3C/svg%3E");
}
.pf-c-form-control.pf-m-icon.pf-m-caps-lock {
--pf-c-form-control--m-icon--BackgroundUrl: var(
--pf-c-form-control--m-caps-lock--BackgroundUrl
);
}
/*#endregion*/
.pf-c-page__header {
z-index: 0;
background-color: var(--ak-dark-background-light);

View File

@ -3,6 +3,7 @@ import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context";
import type { LitElement } from "lit";
import { state } from "lit/decorators.js";
import type { CurrentBrand } from "@goauthentik/api";
@ -12,6 +13,7 @@ export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
) {
abstract class WithBrandProvider extends superclass {
@consume({ context: authentikBrandContext, subscribe })
@state()
public brand!: CurrentBrand;
}
return WithBrandProvider;

View File

@ -0,0 +1,27 @@
/**
* @fileoverview Utilities for DOM element interaction, focus management, and event handling.
*/
/**
* Recursively check if the target element or any of its children are active (i.e. "focused").
*
* @param targetElement The element to check if it is active.
* @param containerElement The container element to check if the target element is active within.
*/
export function isActiveElement(
targetElement: Element | null,
containerElement: Element | null,
): boolean {
// Does the container element even exist?
if (!containerElement) return false;
// Does the container element have a shadow root?
if (!("shadowRoot" in containerElement)) return false;
if (containerElement.shadowRoot === null) return false;
// Is the target element the active element?
if (containerElement.shadowRoot.activeElement === targetElement) return true;
// Let's check the children of the container element...
return isActiveElement(containerElement.shadowRoot.activeElement, containerElement);
}

View File

@ -1,36 +1,93 @@
import { AKElement } from "@goauthentik/elements/Base.js";
import { bound } from "@goauthentik/elements/decorators/bound";
import "@goauthentik/elements/forms/FormElement";
import { isActiveElement } from "@goauthentik/elements/utils/focus";
import { msg } from "@lit/localize";
import { html, nothing, render } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Ref, createRef, ref } from "lit/directives/ref.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
/**
* A configuration object for the visibility states of the password input.
*/
interface VisibilityProps {
icon: string;
label: string;
}
/**
* Enum-like object for the visibility states of the password input.
*/
const Visibility = {
Reveal: {
icon: "fa-eye",
label: msg("Show password"),
},
Mask: {
icon: "fa-eye-slash",
label: msg("Hide password"),
},
} as const satisfies Record<string, VisibilityProps>;
@customElement("ak-flow-input-password")
export class InputPassword extends AKElement {
static get styles() {
return [PFBase, PFInputGroup, PFFormControl, PFButton];
}
//#region Properties
/**
* The ID of the input field.
*
* @attr
*/
@property({ type: String, attribute: "input-id" })
inputId = "ak-stage-password-input";
/**
* The name of the input field.
*
* @attr
*/
@property({ type: String })
name = "password";
/**
* The label for the input field.
*
* @attr
*/
@property({ type: String })
label = msg("Password");
/**
* The placeholder text for the input field.
*
* @attr
*/
@property({ type: String })
placeholder = msg("Please enter your password");
/**
* The initial value of the input field.
*
* @attr
*/
@property({ type: String, attribute: "prefill" })
passwordPrefill = "";
initialValue = "";
/**
* The errors for the input field.
*/
@property({ type: Object })
errors: Record<string, string> = {};
@ -41,113 +98,220 @@ export class InputPassword extends AKElement {
@property({ type: String })
invalid?: string;
/**
* Whether to allow the user to toggle the visibility of the password.
*
* @attr
*/
@property({ type: Boolean, attribute: "allow-show-password" })
allowShowPassword = false;
/**
* Whether the password is currently visible.
*
* @attr
*/
@property({ type: Boolean, attribute: "password-visible" })
passwordVisible = false;
/**
* Automatically grab focus after rendering.
*
* @attr
*/
@property({ type: Boolean, attribute: "grab-focus" })
grabFocus = false;
timer?: number;
//#endregion
input?: HTMLInputElement;
//#region Refs
cleanup(): void {
if (this.timer) {
console.debug("authentik/stages/password: cleared focus timer");
window.clearInterval(this.timer);
this.timer = undefined;
inputRef: Ref<HTMLInputElement> = createRef();
toggleVisibilityRef: Ref<HTMLButtonElement> = createRef();
//#endregion
//#region State
/**
* Whether the caps lock key is enabled.
*/
@state()
capsLock = false;
//#endregion
//#region Listeners
/**
* Toggle the visibility of the password field.
*
* Directly affects the DOM, so no `.requestUpdate()` required. Effect is immediately visible.
*
* @param event The event that triggered the visibility toggle.
*/
@bound
togglePasswordVisibility(event?: PointerEvent) {
event?.stopPropagation();
event?.preventDefault();
const input = this.inputRef.value;
if (!input) {
console.warn("ak-flow-password-input: unable to identify input field");
return;
}
input.type = input.type === "password" ? "text" : "password";
this.syncVisibilityToggle(input);
}
// Must support both older browsers and shadyDom; we'll keep using this in-line, but it'll still
// be in the scope of the parent element, not an independent shadowDOM.
/**
* Listen for key events, synchronizing the caps lock indicators.
*/
@bound
capsLockListener(event: KeyboardEvent) {
this.capsLock = event.getModifierState("CapsLock");
}
//#region Lifecycle
/**
* Interval ID for the focus observer.
*
* @see {@linkcode observeInputFocus}
*/
inputFocusIntervalID?: ReturnType<typeof setInterval>;
/**
* Periodically attempt to focus the input field until it is focused.
*
* This is some-what of a crude way to get autofocus, but in most cases
* the `autofocus` attribute isn't enough, due to timing within shadow doms and such.
*/
observeInputFocus(): void {
if (!this.grabFocus) {
return;
}
this.inputFocusIntervalID = setInterval(() => {
const input = this.inputRef.value;
if (!input) return;
if (isActiveElement(input, document.activeElement)) {
console.debug("authentik/stages/password: cleared focus observer");
clearInterval(this.inputFocusIntervalID);
}
input.focus();
}, 10);
console.debug("authentik/stages/password: started focus observer");
}
connectedCallback() {
super.connectedCallback();
this.observeInputFocus();
addEventListener("keydown", this.capsLockListener);
addEventListener("keyup", this.capsLockListener);
}
disconnectedCallback() {
if (this.inputFocusIntervalID) {
clearInterval(this.inputFocusIntervalID);
}
super.disconnectedCallback();
removeEventListener("keydown", this.capsLockListener);
removeEventListener("keyup", this.capsLockListener);
}
//#endregion
//#region Render
/**
* Create the render root for the password input.
*
* Must support both older browsers and shadyDom; we'll keep using this in-line,
* but it'll still be in the scope of the parent element, not an independent shadowDOM.
*/
createRenderRoot() {
return this;
}
// State is saved in the DOM, and read from the DOM. Directly affects the DOM,
// so no `.requestUpdate()` required. Effect is immediately visible.
togglePasswordVisibility(ev: PointerEvent) {
const passwordField = this.renderRoot.querySelector(`#${this.inputId}`) as HTMLInputElement;
ev.stopPropagation();
ev.preventDefault();
/**
* Render the password visibility toggle button.
*
* In the unlikely event that we want to make "show password" the _default_ behavior,
* this effect handler is broken out into its own method.
*
* The current behavior in the main {@linkcode render} method assumes the field is of type "password."
*
* To have this effect, er, take effect, call it in an {@linkcode updated} method.
*
* @param input The password field to render the visibility features for.
*/
syncVisibilityToggle(input: HTMLInputElement | undefined = this.inputRef.value): void {
if (!input) return;
if (!passwordField) {
throw new Error("ak-flow-password-input: unable to identify input field");
}
const toggleElement = this.toggleVisibilityRef.value;
passwordField.type = passwordField.type === "password" ? "text" : "password";
this.renderPasswordVisibilityFeatures(passwordField);
}
if (!toggleElement) return;
// In the unlikely event that we want to make "show password" the _default_ behavior, this
// effect handler is broken out into its own method. The current behavior in the main
// `.render()` method assumes the field is of type "password." To have this effect, er, take
// effect, call it in an `.updated()` method.
renderPasswordVisibilityFeatures(passwordField: HTMLInputElement) {
const toggleId = `#${this.inputId}-visibility-toggle`;
const visibilityToggle = this.renderRoot.querySelector(toggleId) as HTMLButtonElement;
if (!visibilityToggle) {
return;
}
const show = passwordField.type === "password";
visibilityToggle?.setAttribute(
const masked = input.type === "password";
toggleElement.setAttribute(
"aria-label",
show ? msg("Show password") : msg("Hide password"),
);
visibilityToggle?.querySelector("i")?.remove();
render(
show
? html`<i class="fas fa-eye" aria-hidden="true"></i>`
: html`<i class="fas fa-eye-slash" aria-hidden="true"></i>`,
visibilityToggle,
msg(masked ? Visibility.Reveal.label : Visibility.Mask.label),
);
const iconElement = toggleElement.querySelector("i")!;
iconElement.classList.remove(Visibility.Mask.icon, Visibility.Reveal.icon);
iconElement.classList.add(masked ? Visibility.Reveal.icon : Visibility.Mask.icon);
}
renderInput(): HTMLInputElement {
this.input = document.createElement("input");
this.input.id = `${this.inputId}`;
this.input.type = "password";
this.input.name = this.name;
this.input.placeholder = this.placeholder;
this.input.autofocus = this.grabFocus;
this.input.autocomplete = "current-password";
this.input.classList.add("pf-c-form-control");
this.input.required = true;
this.input.value = this.passwordPrefill ?? "";
if (this.invalid) {
this.input.setAttribute("aria-invalid", this.invalid);
}
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
// isn't enough, due to timing within shadow doms and such.
renderVisibilityToggle() {
if (!this.allowShowPassword) return nothing;
if (this.grabFocus) {
this.timer = window.setInterval(() => {
if (!this.input) {
return;
}
// Because activeElement behaves differently with shadow dom
// we need to recursively check
const rootEl = document.activeElement;
const isActive = (el: Element | null): boolean => {
if (!rootEl) return false;
if (!("shadowRoot" in rootEl)) return false;
if (rootEl.shadowRoot === null) return false;
if (rootEl.shadowRoot.activeElement === el) return true;
return isActive(rootEl.shadowRoot.activeElement);
};
if (isActive(this.input)) {
this.cleanup();
}
this.input.focus();
}, 10);
console.debug("authentik/stages/password: started focus timer");
}
return this.input;
const { label, icon } = this.passwordVisible ? Visibility.Mask : Visibility.Reveal;
return html`<button
${ref(this.toggleVisibilityRef)}
aria-label=${msg(label)}
@click=${this.togglePasswordVisibility}
class="pf-c-button pf-m-control"
type="button"
>
<i class="fas ${icon}" aria-hidden="true"></i>
</button>`;
}
renderHelperText() {
if (!this.capsLock) return nothing;
return html`<div
class="pf-c-form__helper-text"
id="helper-text-form-caps-lock-helper"
aria-live="polite"
>
<div class="pf-c-helper-text">
<div class="pf-c-helper-text__item pf-m-warning">
<span class="pf-c-helper-text__item-icon">
<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
</span>
<span class="pf-c-helper-text__item-text">${msg("Caps Lock is enabled.")}</span>
</div>
</div>
</div>`;
}
render() {
@ -157,22 +321,34 @@ export class InputPassword extends AKElement {
class="pf-c-form__group"
.errors=${this.errors}
>
<div class="pf-c-input-group">
${this.renderInput()}
${this.allowShowPassword
? html` <button
id="${this.inputId}-visibility-toggle"
class="pf-c-button pf-m-control ak-stage-password-toggle-visibility"
type="button"
aria-label=${msg("Show password")}
@click=${(ev: PointerEvent) => this.togglePasswordVisibility(ev)}
>
<i class="fas fa-eye" aria-hidden="true"></i>
</button>`
: nothing}
<div class="pf-c-form__group-control">
<div class="pf-c-input-group">
<input
type=${this.passwordVisible ? "text" : "password"}
id=${this.inputId}
name=${this.name}
placeholder=${this.placeholder}
autocomplete="current-password"
class="${classMap({
"pf-c-form-control": true,
"pf-m-icon": true,
"pf-m-caps-lock": this.capsLock,
})}"
required
aria-invalid=${ifDefined(this.invalid)}
value=${this.initialValue}
${ref(this.inputRef)}
/>
${this.renderVisibilityToggle()}
</div>
${this.renderHelperText()}
</div>
</ak-form-element>`;
}
//#endregion
}
declare global {

View File

@ -161,7 +161,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
super.disconnectedCallback();
}
get captchaDocumentContainer() {
get captchaDocumentContainer(): HTMLDivElement {
if (this._captchaDocumentContainer) {
return this._captchaDocumentContainer;
}
@ -170,7 +170,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
return this._captchaDocumentContainer;
}
get captchaFrame() {
get captchaFrame(): HTMLIFrameElement {
if (this._captchaFrame) {
return this._captchaFrame;
}
@ -326,7 +326,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
.exhaustive();
}
updated(changedProperties: PropertyValues<this>) {
firstUpdated(changedProperties: PropertyValues<this>) {
if (!(changedProperties.has("challenge") && this.challenge !== undefined)) {
return;
}

View File

@ -99,6 +99,19 @@ export class LibraryApplication extends AKElement {
if (this.application?.launchUrl === "goauthentik.io://providers/rac/launch") {
return html`<ak-library-rac-endpoint-launch .app=${this.application}>
</ak-library-rac-endpoint-launch>
<div class="pf-c-card__header">
<a
@click=${() => {
this.racEndpointLaunch?.onClick();
}}
>
<ak-app-icon
size=${PFSize.Large}
name=${this.application.name}
icon=${ifDefined(this.application.metaIcon || undefined)}
></ak-app-icon>
</a>
</div>
<div class="pf-c-card__title">
<a
@click=${() => {
@ -109,13 +122,25 @@ export class LibraryApplication extends AKElement {
</a>
</div>`;
}
return html`<div class="pf-c-card__title">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>${this.application.name}</a
>
</div>`;
return html`<div class="pf-c-card__header">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>
<ak-app-icon
size=${PFSize.Large}
name=${this.application.name}
icon=${ifDefined(this.application.metaIcon || undefined)}
></ak-app-icon>
</a>
</div>
<div class="pf-c-card__title">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>${this.application.name}</a
>
</div>`;
}
render(): TemplateResult {
@ -135,18 +160,6 @@ export class LibraryApplication extends AKElement {
class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}"
style=${styleMap(styles)}
>
<div class="pf-c-card__header">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>
<ak-app-icon
size=${PFSize.Large}
name=${this.application.name}
icon=${ifDefined(this.application.metaIcon || undefined)}
></ak-app-icon>
</a>
</div>
${this.renderLaunch()}
<div class="expander"></div>
${expandable ? this.renderExpansion(this.application) : nothing}

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

@ -32,7 +32,7 @@ export class UserSettingsPassword extends AKElement {
<div class="pf-c-card__body">
<a
href="${ifDefined(this.configureUrl)}${AndNext(
`${globalAK().api.base}if/user/#/settings;${JSON.stringify({ page: "page-details" })}`,
`${globalAK().api.relBase}if/user/#/settings;${JSON.stringify({ page: "page-details" })}`,
)}"
class="pf-c-button pf-m-primary"
>

View File

@ -10,7 +10,7 @@ import { StageHost } from "@goauthentik/flow/stages/base";
import "@goauthentik/user/user-settings/details/stages/prompt/PromptStage";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
@ -83,12 +83,14 @@ export class UserSettingsFlowExecutor
});
}
firstUpdated(): void {
this.flowSlug = this.brand?.flowUserSettings;
if (!this.flowSlug) {
return;
updated(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("brand") && this.brand) {
this.flowSlug = this.brand?.flowUserSettings;
if (!this.flowSlug) {
return;
}
this.nextChallenge();
}
this.nextChallenge();
}
async nextChallenge(): Promise<void> {
@ -161,7 +163,7 @@ export class UserSettingsFlowExecutor
// Flow has finished, so let's load while in the background we can restart the flow
this.loading = true;
console.debug("authentik/user/flows: redirect to '/', restarting flow.");
this.firstUpdated();
this.nextChallenge();
this.globalRefresh();
showMessage({
level: MessageLevel.success,

View File

@ -74,7 +74,7 @@ export class MFADevicesPage extends Table<Device> {
return html`<li>
<a
href="${ifDefined(stage.configureUrl)}${AndNext(
`${globalAK().api.base}if/user/#/settings;${JSON.stringify({
`${globalAK().api.relBase}if/user/#/settings;${JSON.stringify({
page: "page-mfa",
})}`,
)}"

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_ -->