diff --git a/authentik/flows/views/executor.py b/authentik/flows/views/executor.py index 773650c00..179a16d29 100644 --- a/authentik/flows/views/executor.py +++ b/authentik/flows/views/executor.py @@ -15,6 +15,7 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.generic import View from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema +from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import AllowAny from rest_framework.views import APIView from sentry_sdk import capture_exception @@ -22,6 +23,7 @@ from sentry_sdk.api import set_tag from sentry_sdk.hub import Hub from structlog.stdlib import BoundLogger, get_logger +from authentik.api.authentication import TokenAuthentication from authentik.core.models import Application from authentik.events.models import Event, EventAction, cleanse_dict from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME @@ -103,6 +105,10 @@ class FlowExecutorView(APIView): """Flow executor, passing requests to Stage Views""" permission_classes = [AllowAny] + authentication_classes = [ + TokenAuthentication, + SessionAuthentication, + ] flow: Flow diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 634e1bba8..00cf23cbb 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -165,10 +165,7 @@ REST_FRAMEWORK = { "rest_framework.parsers.JSONParser", ], "DEFAULT_PERMISSION_CLASSES": ("authentik.rbac.permissions.ObjectPermissions",), - "DEFAULT_AUTHENTICATION_CLASSES": ( - "authentik.api.authentication.TokenAuthentication", - "rest_framework.authentication.SessionAuthentication", - ), + "DEFAULT_AUTHENTICATION_CLASSES": ("authentik.api.authentication.TokenAuthentication",), "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", ], diff --git a/web/package.json b/web/package.json index 666077e8f..5537a6572 100644 --- a/web/package.json +++ b/web/package.json @@ -62,6 +62,7 @@ "fuse.js": "^7.0.0", "lit": "^2.8.0", "mermaid": "^10.6.1", + "oidc-client-ts": "^2.4.0", "rapidoc": "^9.3.4", "style-mod": "^4.1.0", "webcomponent-qr-code": "^1.2.0", diff --git a/web/src/admin/AdminInterface/AdminInterface.ts b/web/src/admin/AdminInterface/AdminInterface.ts index 834c98f37..4485a5b2a 100644 --- a/web/src/admin/AdminInterface/AdminInterface.ts +++ b/web/src/admin/AdminInterface/AdminInterface.ts @@ -1,4 +1,5 @@ import { ROUTES } from "@goauthentik/admin/Routes"; +import { OAuthInterface } from "@goauthentik/app/common/oauth/interface"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_API_DRAWER_TOGGLE, @@ -7,7 +8,6 @@ import { import { configureSentry } from "@goauthentik/common/sentry"; import { me } from "@goauthentik/common/users"; import { WebsocketClient } from "@goauthentik/common/ws"; -import { Interface } from "@goauthentik/elements/Base"; import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; import "@goauthentik/elements/messages/MessageContainer"; @@ -33,7 +33,7 @@ import { AdminApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api"; import "./AdminSidebar"; @customElement("ak-interface-admin") -export class AdminInterface extends Interface { +export class AdminInterface extends OAuthInterface { @property({ type: Boolean }) notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); @@ -92,7 +92,8 @@ export class AdminInterface extends Interface { }); } - async firstUpdated(): Promise { + async firstUpdated(_changedProperties: Map): Promise { + super.firstUpdated(_changedProperties); configureSentry(true); this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); this.user = await me(); diff --git a/web/src/admin/Routes.ts b/web/src/admin/Routes.ts index 1c7a9739e..9ee971ad5 100644 --- a/web/src/admin/Routes.ts +++ b/web/src/admin/Routes.ts @@ -1,4 +1,6 @@ import "@goauthentik/admin/admin-overview/AdminOverviewPage"; +import "@goauthentik/app/common/oauth/callback"; +import "@goauthentik/app/common/oauth/signout"; import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; import { html } from "lit"; @@ -8,6 +10,12 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^/$")).redirect("/administration/overview"), new Route(new RegExp("^#.*")).redirect("/administration/overview"), new Route(new RegExp("^/library$")).redirect("/if/user/", true), + new Route(new RegExp("^/oauth-callback/(?.*)$"), async (args) => { + return html``; + }), + new Route(new RegExp("^/oauth-signout$"), async () => { + return html``; + }), // statically imported since this is the default route new Route(new RegExp("^/administration/overview$"), async () => { return html``; diff --git a/web/src/common/api/config.ts b/web/src/common/api/config.ts index 8de76d840..428a80c23 100644 --- a/web/src/common/api/config.ts +++ b/web/src/common/api/config.ts @@ -1,3 +1,4 @@ +import { TokenMiddleware } from "@goauthentik/app/common/oauth/middleware"; import { CSRFMiddleware, EventMiddleware, @@ -73,6 +74,7 @@ export const DEFAULT_CONFIG = new Configuration({ "sentry-trace": getMetaContent("sentry-trace"), }, middleware: [ + new TokenMiddleware(), new CSRFMiddleware(), new EventMiddleware(), new LoggingMiddleware(globalAK().tenant), diff --git a/web/src/common/oauth/callback.ts b/web/src/common/oauth/callback.ts new file mode 100644 index 000000000..830d4af76 --- /dev/null +++ b/web/src/common/oauth/callback.ts @@ -0,0 +1,20 @@ +import { state } from "@goauthentik/app/common/oauth/constants"; +import { settings } from "@goauthentik/app/common/oauth/settings"; +import { refreshMe } from "@goauthentik/app/common/users"; +import { User, UserManager } from "oidc-client-ts"; + +import { LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-oauth-callback") +export class OAuthCallback extends LitElement { + @property() + params?: string; + async firstUpdated(): Promise { + const client = new UserManager(settings); + const user = (await client.signinCallback(`#${this.params}`)) as User; + const st = user.state as state; + window.location.assign(st.url); + refreshMe(); + } +} diff --git a/web/src/common/oauth/constants.ts b/web/src/common/oauth/constants.ts new file mode 100644 index 000000000..726f87f21 --- /dev/null +++ b/web/src/common/oauth/constants.ts @@ -0,0 +1,7 @@ +export class state { + url: string; + + constructor() { + this.url = window.location.href; + } +} diff --git a/web/src/common/oauth/interface.ts b/web/src/common/oauth/interface.ts new file mode 100644 index 000000000..1b4248017 --- /dev/null +++ b/web/src/common/oauth/interface.ts @@ -0,0 +1,26 @@ +import { state } from "@goauthentik/app/common/oauth/constants"; +import { settings } from "@goauthentik/app/common/oauth/settings"; +import { Interface } from "@goauthentik/app/elements/Base"; +import { UserManager } from "oidc-client-ts"; + +export class OAuthInterface extends Interface { + private async ensureLoggedIn() { + const client = new UserManager(settings); + const user = await client.getUser(); + if (user !== null) { + return; + } + if (window.location.href.startsWith(settings.redirect_uri)) { + return; + } + const s = new state(); + await client.signinRedirect({ + state: s, + }); + } + + async firstUpdated(_changedProperties: Map): Promise { + await this.ensureLoggedIn(); + await super.firstUpdated(_changedProperties); + } +} diff --git a/web/src/common/oauth/middleware.ts b/web/src/common/oauth/middleware.ts new file mode 100644 index 000000000..70f7454f9 --- /dev/null +++ b/web/src/common/oauth/middleware.ts @@ -0,0 +1,15 @@ +import { settings } from "@goauthentik/app/common/oauth/settings"; +import { UserManager } from "oidc-client-ts"; + +import { FetchParams, Middleware, RequestContext } from "@goauthentik/api"; + +export class TokenMiddleware implements Middleware { + async pre?(context: RequestContext): Promise { + const user = await new UserManager(settings).getUser(); + if (user !== null) { + // @ts-ignore + context.init.headers["Authorization"] = `Bearer ${user.access_token}`; + } + return Promise.resolve(context); + } +} diff --git a/web/src/common/oauth/settings.ts b/web/src/common/oauth/settings.ts new file mode 100644 index 000000000..6924a1de2 --- /dev/null +++ b/web/src/common/oauth/settings.ts @@ -0,0 +1,13 @@ +import { Log, OidcClientSettings, UserManagerSettings } from "oidc-client-ts"; + +Log.setLogger(console); +Log.setLevel(Log.DEBUG); + +export const settings: OidcClientSettings & UserManagerSettings = { + authority: `${window.location.origin}/application/o/authentik-admin-interface/`, + redirect_uri: `${window.location.origin}/if/admin/#/oauth-callback/`, + client_id: "authentik-admin-interface", + scope: "openid profile email goauthentik.io/api", + response_mode: "fragment", + automaticSilentRenew: true, +}; diff --git a/web/src/common/oauth/signout.ts b/web/src/common/oauth/signout.ts new file mode 100644 index 000000000..9bd06060a --- /dev/null +++ b/web/src/common/oauth/signout.ts @@ -0,0 +1,13 @@ +import { settings } from "@goauthentik/app/common/oauth/settings"; +import { UserManager } from "oidc-client-ts"; + +import { LitElement } from "lit"; +import { customElement } from "lit/decorators.js"; + +@customElement("ak-oauth-signout") +export class OAuthSignout extends LitElement { + async firstUpdated(): Promise { + const client = new UserManager(settings); + await client.signoutRedirect(); + } +} diff --git a/web/src/common/users.ts b/web/src/common/users.ts index 293047572..28a75c9ad 100644 --- a/web/src/common/users.ts +++ b/web/src/common/users.ts @@ -1,7 +1,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; -import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api"; +import { CoreApi, SessionUser } from "@goauthentik/api"; let globalMePromise: Promise | undefined; @@ -33,7 +33,7 @@ export function me(): Promise { } return user; }) - .catch((ex: ResponseError) => { + .catch(() => { const defaultUser: SessionUser = { user: { pk: -1, @@ -48,14 +48,6 @@ export function me(): Promise { systemPermissions: [], }, }; - if (ex.response?.status === 401 || ex.response?.status === 403) { - const relativeUrl = window.location - .toString() - .substring(window.location.origin.length); - window.location.assign( - `/flows/-/default/authentication/?next=${encodeURIComponent(relativeUrl)}`, - ); - } return defaultUser; }); } diff --git a/web/src/elements/sidebar/SidebarUser.ts b/web/src/elements/sidebar/SidebarUser.ts index 97cdad27b..b322d8861 100644 --- a/web/src/elements/sidebar/SidebarUser.ts +++ b/web/src/elements/sidebar/SidebarUser.ts @@ -47,7 +47,7 @@ export class SidebarUser extends AKElement { html``, )} - + `; diff --git a/web/src/user/Routes.ts b/web/src/user/Routes.ts index 72738e182..cb4f5f7f6 100644 --- a/web/src/user/Routes.ts +++ b/web/src/user/Routes.ts @@ -1,3 +1,5 @@ +import "@goauthentik/app/common/oauth/callback"; +import "@goauthentik/app/common/oauth/signout"; import { Route } from "@goauthentik/elements/router/Route"; import "@goauthentik/user/LibraryPage/LibraryPage"; @@ -7,6 +9,12 @@ export const ROUTES: Route[] = [ // Prevent infinite Shell loops new Route(new RegExp("^/$")).redirect("/library"), new Route(new RegExp("^#.*")).redirect("/library"), + new Route(new RegExp("^/oauth-callback/(?.*)$"), async (args) => { + return html``; + }), + new Route(new RegExp("^/oauth-signout$"), async () => { + return html``; + }), new Route(new RegExp("^/library$"), async () => html``), new Route(new RegExp("^/settings$"), async () => { await import("@goauthentik/user/user-settings/UserSettingsPage"); diff --git a/web/src/user/UserInterface.ts b/web/src/user/UserInterface.ts index 09b10d632..1ebc12ced 100644 --- a/web/src/user/UserInterface.ts +++ b/web/src/user/UserInterface.ts @@ -1,3 +1,4 @@ +import { OAuthInterface } from "@goauthentik/app/common/oauth/interface"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_API_DRAWER_TOGGLE, @@ -9,7 +10,6 @@ import { UserDisplay } from "@goauthentik/common/ui/config"; import { me } from "@goauthentik/common/users"; import { first } from "@goauthentik/common/utils"; import { WebsocketClient } from "@goauthentik/common/ws"; -import { Interface } from "@goauthentik/elements/Base"; import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; @@ -41,7 +41,7 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api"; @customElement("ak-interface-user") -export class UserInterface extends Interface { +export class UserInterface extends OAuthInterface { @property({ type: Boolean }) notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); @@ -141,12 +141,13 @@ export class UserInterface extends Interface { }); }); window.addEventListener(EVENT_WS_MESSAGE, () => { - this.firstUpdated(); + this.firstUpdated(new Map()); }); configureSentry(true); } - async firstUpdated(): Promise { + async firstUpdated(_changedProperties: Map): Promise { + super.firstUpdated(_changedProperties); this.me = await me(); const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({ seen: false, @@ -275,10 +276,7 @@ export class UserInterface extends Interface { ` : html``}