mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
flows: refresh unauthenticated tabs (#18621)
* flows: implement signaling Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add flag Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better flag configuration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Update web/src/flow/FlowExecutor.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Jens L. <jens@beryju.org> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"""authentik core signals"""
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
@@ -17,6 +19,8 @@ from authentik.core.models import (
|
||||
User,
|
||||
default_token_duration,
|
||||
)
|
||||
from authentik.flows.apps import RefreshOtherFlowsAfterAuthentication
|
||||
from authentik.root.ws.consumer import build_device_group
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
@@ -47,6 +51,16 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
|
||||
if session:
|
||||
session.save()
|
||||
|
||||
if not RefreshOtherFlowsAfterAuthentication().get():
|
||||
return
|
||||
layer = get_channel_layer()
|
||||
device_cookie = request.COOKIES.get("authentik_device")
|
||||
if device_cookie:
|
||||
async_to_sync(layer.group_send)(
|
||||
build_device_group(device_cookie),
|
||||
{"type": "event.session.authenticated"},
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=AuthenticatedSession)
|
||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||
|
||||
@@ -28,8 +28,8 @@ from authentik.core.views.interface import (
|
||||
)
|
||||
from authentik.flows.views.interface import FlowInterfaceView
|
||||
from authentik.root.asgi_middleware import AuthMiddlewareStack
|
||||
from authentik.root.messages.consumer import MessageConsumer
|
||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||
from authentik.root.ws.consumer import MessageConsumer
|
||||
from authentik.tenants.channels import TenantsAwareMiddleware
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -4,6 +4,7 @@ from prometheus_client import Gauge, Histogram
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
GAUGE_FLOWS_CACHED = Gauge(
|
||||
"authentik_flows_cached",
|
||||
@@ -22,6 +23,12 @@ HIST_FLOWS_PLAN_TIME = Histogram(
|
||||
)
|
||||
|
||||
|
||||
class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others"):
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
|
||||
|
||||
class AuthentikFlowsConfig(ManagedAppConfig):
|
||||
"""authentik flows app config"""
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""websocket Message consumer"""
|
||||
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from django.core.cache import cache
|
||||
|
||||
from authentik.root.messages.storage import CACHE_PREFIX
|
||||
|
||||
|
||||
class MessageConsumer(JsonWebsocketConsumer):
|
||||
"""Consumer which sends django.contrib.messages Messages over WS.
|
||||
channel_name is saved into cache with user_id, and when a add_message is called"""
|
||||
|
||||
session_key: str
|
||||
|
||||
def connect(self):
|
||||
self.accept()
|
||||
self.session_key = self.scope["session"].session_key
|
||||
if not self.session_key:
|
||||
return
|
||||
cache.set(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}", True, None)
|
||||
|
||||
def disconnect(self, code):
|
||||
cache.delete(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}")
|
||||
|
||||
def event_update(self, event: dict):
|
||||
"""Event handler which is called by Messages Storage backend"""
|
||||
self.send_json(event)
|
||||
@@ -254,7 +254,7 @@ SESSION_COOKIE_AGE = timedelta_from_string(
|
||||
).total_seconds()
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
|
||||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||
MESSAGE_STORAGE = "authentik.root.ws.storage.ChannelsStorage"
|
||||
|
||||
MIDDLEWARE_FIRST = [
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""websocket Message consumer"""
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from django.core.cache import cache
|
||||
from django.db import connection
|
||||
|
||||
from authentik.root.ws.storage import CACHE_PREFIX
|
||||
|
||||
|
||||
def build_session_group(session_key: str):
|
||||
return sha256(
|
||||
f"{connection.schema_name}/group_client_session_{str(session_key)}".encode()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def build_device_group(session_key: str):
|
||||
return sha256(
|
||||
f"{connection.schema_name}/group_client_device_{str(session_key)}".encode()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
class MessageConsumer(JsonWebsocketConsumer):
|
||||
"""Consumer which sends django.contrib.messages Messages over WS.
|
||||
channel_name is saved into cache with user_id, and when a add_message is called"""
|
||||
|
||||
session_key: str
|
||||
device_cookie: str | None = None
|
||||
|
||||
def connect(self):
|
||||
self.accept()
|
||||
self.session_key = self.scope["session"].session_key
|
||||
if self.session_key:
|
||||
cache.set(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}", True, None)
|
||||
if device_cookie := self.scope["cookies"]["authentik_device"]:
|
||||
self.device_cookie = device_cookie
|
||||
async_to_sync(self.channel_layer.group_add)(
|
||||
build_device_group(self.device_cookie), self.channel_name
|
||||
)
|
||||
|
||||
def disconnect(self, code):
|
||||
if self.session_key:
|
||||
cache.delete(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}")
|
||||
if self.device_cookie:
|
||||
print("removing from group", build_session_group(self.session_key))
|
||||
async_to_sync(self.channel_layer.group_discard)(
|
||||
build_device_group(self.device_cookie), self.channel_name
|
||||
)
|
||||
|
||||
def event_message(self, event: dict):
|
||||
"""Event handler which is called by Messages Storage backend"""
|
||||
self.send_json(event)
|
||||
|
||||
def event_session_authenticated(self, event: dict):
|
||||
"""Event handler post user authentication"""
|
||||
self.send_json({"message_type": "session.authenticated"})
|
||||
@@ -31,7 +31,7 @@ class ChannelsStorage(SessionStorage):
|
||||
async_to_sync(self.channel.send)(
|
||||
uid,
|
||||
{
|
||||
"type": "event.update",
|
||||
"type": "event.message",
|
||||
"message_type": "message",
|
||||
"level": message.level_tag,
|
||||
"tags": message.tags,
|
||||
+12
@@ -35696,7 +35696,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
readOnly: true
|
||||
required:
|
||||
@@ -48756,7 +48759,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
PatchedSourceStageRequest:
|
||||
type: object
|
||||
@@ -53189,7 +53195,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
required:
|
||||
- flags
|
||||
@@ -53251,7 +53260,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
required:
|
||||
- flags
|
||||
|
||||
@@ -8,7 +8,6 @@ import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#elements/utils/TimeDeltaHelp";
|
||||
import "./AdminSettingsFooterLinks.js";
|
||||
import "#elements/CodeMirror";
|
||||
|
||||
import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.js";
|
||||
|
||||
@@ -18,8 +17,6 @@ import { Form } from "#elements/forms/Form";
|
||||
|
||||
import { AdminApi, FooterLink, Settings, SettingsRequest } from "@goauthentik/api";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@@ -248,13 +245,33 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
value="${settings.defaultTokenLength ?? 60}"
|
||||
help=${msg("Default length of generated tokens")}
|
||||
></ak-number-input>
|
||||
<ak-form-element-horizontal label=${msg("Flags")} name="flags" required>
|
||||
<ak-codemirror mode="yaml" value="${YAML.stringify(settings?.flags ?? {})}">
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Modify flags to opt into new authentik behaviours early.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group
|
||||
label=${msg("Flags")}
|
||||
description=${msg(
|
||||
"Flags allow you to enable new functionality and behaviour in authentik early.",
|
||||
)}
|
||||
>
|
||||
<div class="pf-c-form">
|
||||
<ak-switch-input
|
||||
name="flags.policiesBufferedAccessView"
|
||||
?checked=${settings?.flags.policiesBufferedAccessView ?? false}
|
||||
label=${msg("Buffer PolicyAccessVew requests")}
|
||||
help=${msg(
|
||||
"When enabled, parallel requests for application authorization will be buffered instead of conflicting with other flows.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="flags.flowsRefreshOthers"
|
||||
?checked=${settings?.flags.flowsRefreshOthers ?? false}
|
||||
label=${msg("Refresh other flow tabs upon authentication")}
|
||||
help=${msg(
|
||||
"When enabled, other flow tabs in a session will refresh upon a successful authentication.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const DefaultBrand = {
|
||||
defaultLocale: "",
|
||||
flags: {
|
||||
policiesBufferedAccessView: false,
|
||||
flowsRefreshOthers: false,
|
||||
},
|
||||
} as const satisfies CurrentBrand;
|
||||
|
||||
|
||||
@@ -11,12 +11,16 @@ import "#flow/stages/RedirectStage";
|
||||
import Styles from "./FlowExecutor.css" with { type: "bundled-text" };
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_FLOW_ADVANCE, EVENT_FLOW_INSPECTOR_TOGGLE } from "#common/constants";
|
||||
import {
|
||||
EVENT_FLOW_ADVANCE,
|
||||
EVENT_FLOW_INSPECTOR_TOGGLE,
|
||||
EVENT_WS_MESSAGE,
|
||||
} from "#common/constants";
|
||||
import { pluckErrorDetail } from "#common/errors/network";
|
||||
import { globalAK } from "#common/global";
|
||||
import { configureSentry } from "#common/sentry/index";
|
||||
import { applyBackgroundImageProperty } from "#common/theme";
|
||||
import { WebsocketClient } from "#common/ws";
|
||||
import { WebsocketClient, WSMessage } from "#common/ws";
|
||||
|
||||
import { Interface } from "#elements/Interface";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
@@ -154,16 +158,25 @@ export class FlowExecutor
|
||||
});
|
||||
}
|
||||
|
||||
#websocketHandler = (e: CustomEvent<WSMessage>) => {
|
||||
if (e.detail.message_type === "session.authenticated") {
|
||||
console.debug("authentik/ws: Reloading after session authenticated event");
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
window.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, this.#toggleInspector);
|
||||
window.addEventListener(EVENT_WS_MESSAGE, this.#websocketHandler as EventListener);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
window.removeEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, this.#toggleInspector);
|
||||
window.removeEventListener(EVENT_WS_MESSAGE, this.#websocketHandler as EventListener);
|
||||
|
||||
WebsocketClient.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user