web: use OAuth

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-11-04 22:13:27 +01:00
parent d3cbe26106
commit c0bb1f7347
No known key found for this signature in database
16 changed files with 133 additions and 26 deletions

View file

@ -15,6 +15,7 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View from django.views.generic import View
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.views import APIView from rest_framework.views import APIView
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
@ -22,6 +23,7 @@ from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
from authentik.api.authentication import TokenAuthentication
from authentik.core.models import Application from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME
@ -103,6 +105,10 @@ class FlowExecutorView(APIView):
"""Flow executor, passing requests to Stage Views""" """Flow executor, passing requests to Stage Views"""
permission_classes = [AllowAny] permission_classes = [AllowAny]
authentication_classes = [
TokenAuthentication,
SessionAuthentication,
]
flow: Flow flow: Flow

View file

@ -165,10 +165,7 @@ REST_FRAMEWORK = {
"rest_framework.parsers.JSONParser", "rest_framework.parsers.JSONParser",
], ],
"DEFAULT_PERMISSION_CLASSES": ("authentik.rbac.permissions.ObjectPermissions",), "DEFAULT_PERMISSION_CLASSES": ("authentik.rbac.permissions.ObjectPermissions",),
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": ("authentik.api.authentication.TokenAuthentication",),
"authentik.api.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_RENDERER_CLASSES": [ "DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer", "rest_framework.renderers.JSONRenderer",
], ],

View file

@ -62,6 +62,7 @@
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"lit": "^2.8.0", "lit": "^2.8.0",
"mermaid": "^10.6.1", "mermaid": "^10.6.1",
"oidc-client-ts": "^2.4.0",
"rapidoc": "^9.3.4", "rapidoc": "^9.3.4",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",

View file

@ -1,4 +1,5 @@
import { ROUTES } from "@goauthentik/admin/Routes"; import { ROUTES } from "@goauthentik/admin/Routes";
import { OAuthInterface } from "@goauthentik/app/common/oauth/interface";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
@ -7,7 +8,6 @@ import {
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
@ -33,7 +33,7 @@ import { AdminApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api";
import "./AdminSidebar"; import "./AdminSidebar";
@customElement("ak-interface-admin") @customElement("ak-interface-admin")
export class AdminInterface extends Interface { export class AdminInterface extends OAuthInterface {
@property({ type: Boolean }) @property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@ -92,7 +92,8 @@ export class AdminInterface extends Interface {
}); });
} }
async firstUpdated(): Promise<void> { async firstUpdated(_changedProperties: Map<PropertyKey, unknown>): Promise<void> {
super.firstUpdated(_changedProperties);
configureSentry(true); configureSentry(true);
this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
this.user = await me(); this.user = await me();

View file

@ -1,4 +1,6 @@
import "@goauthentik/admin/admin-overview/AdminOverviewPage"; 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 { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { html } from "lit"; 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("^#.*")).redirect("/administration/overview"), new Route(new RegExp("^#.*")).redirect("/administration/overview"),
new Route(new RegExp("^/library$")).redirect("/if/user/", true), new Route(new RegExp("^/library$")).redirect("/if/user/", true),
new Route(new RegExp("^/oauth-callback/(?<rest>.*)$"), async (args) => {
return html`<ak-oauth-callback params=${args.rest}></ak-oauth-callback>`;
}),
new Route(new RegExp("^/oauth-signout$"), async () => {
return html`<ak-oauth-signout></ak-oauth-signout>`;
}),
// statically imported since this is the default route // statically imported since this is the default route
new Route(new RegExp("^/administration/overview$"), async () => { new Route(new RegExp("^/administration/overview$"), async () => {
return html`<ak-admin-overview></ak-admin-overview>`; return html`<ak-admin-overview></ak-admin-overview>`;

View file

@ -1,3 +1,4 @@
import { TokenMiddleware } from "@goauthentik/app/common/oauth/middleware";
import { import {
CSRFMiddleware, CSRFMiddleware,
EventMiddleware, EventMiddleware,
@ -73,6 +74,7 @@ export const DEFAULT_CONFIG = new Configuration({
"sentry-trace": getMetaContent("sentry-trace"), "sentry-trace": getMetaContent("sentry-trace"),
}, },
middleware: [ middleware: [
new TokenMiddleware(),
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(globalAK().tenant), new LoggingMiddleware(globalAK().tenant),

View file

@ -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<void> {
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();
}
}

View file

@ -0,0 +1,7 @@
export class state {
url: string;
constructor() {
this.url = window.location.href;
}
}

View file

@ -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<PropertyKey, unknown>): Promise<void> {
await this.ensureLoggedIn();
await super.firstUpdated(_changedProperties);
}
}

View file

@ -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<FetchParams | void> {
const user = await new UserManager(settings).getUser();
if (user !== null) {
// @ts-ignore
context.init.headers["Authorization"] = `Bearer ${user.access_token}`;
}
return Promise.resolve(context);
}
}

View file

@ -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,
};

View file

@ -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<void> {
const client = new UserManager(settings);
await client.signoutRedirect();
}
}

View file

@ -1,7 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api"; import { CoreApi, SessionUser } from "@goauthentik/api";
let globalMePromise: Promise<SessionUser> | undefined; let globalMePromise: Promise<SessionUser> | undefined;
@ -33,7 +33,7 @@ export function me(): Promise<SessionUser> {
} }
return user; return user;
}) })
.catch((ex: ResponseError) => { .catch(() => {
const defaultUser: SessionUser = { const defaultUser: SessionUser = {
user: { user: {
pk: -1, pk: -1,
@ -48,14 +48,6 @@ export function me(): Promise<SessionUser> {
systemPermissions: [], 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; return defaultUser;
}); });
} }

View file

@ -47,7 +47,7 @@ export class SidebarUser extends AKElement {
html``, html``,
)} )}
</a> </a>
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout"> <a href="#/oauth-signout" class="pf-c-nav__link user-logout" id="logout">
<i class="fas fa-sign-out-alt" aria-hidden="true"></i> <i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</a> </a>
`; `;

View file

@ -1,3 +1,5 @@
import "@goauthentik/app/common/oauth/callback";
import "@goauthentik/app/common/oauth/signout";
import { Route } from "@goauthentik/elements/router/Route"; import { Route } from "@goauthentik/elements/router/Route";
import "@goauthentik/user/LibraryPage/LibraryPage"; import "@goauthentik/user/LibraryPage/LibraryPage";
@ -7,6 +9,12 @@ export const ROUTES: Route[] = [
// Prevent infinite Shell loops // Prevent infinite Shell loops
new Route(new RegExp("^/$")).redirect("/library"), new Route(new RegExp("^/$")).redirect("/library"),
new Route(new RegExp("^#.*")).redirect("/library"), new Route(new RegExp("^#.*")).redirect("/library"),
new Route(new RegExp("^/oauth-callback/(?<rest>.*)$"), async (args) => {
return html`<ak-oauth-callback params=${args.rest}></ak-oauth-callback>`;
}),
new Route(new RegExp("^/oauth-signout$"), async () => {
return html`<ak-oauth-signout></ak-oauth-signout>`;
}),
new Route(new RegExp("^/library$"), async () => html`<ak-library></ak-library>`), new Route(new RegExp("^/library$"), async () => html`<ak-library></ak-library>`),
new Route(new RegExp("^/settings$"), async () => { new Route(new RegExp("^/settings$"), async () => {
await import("@goauthentik/user/user-settings/UserSettingsPage"); await import("@goauthentik/user/user-settings/UserSettingsPage");

View file

@ -1,3 +1,4 @@
import { OAuthInterface } from "@goauthentik/app/common/oauth/interface";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
@ -9,7 +10,6 @@ import { UserDisplay } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; 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"; import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
@customElement("ak-interface-user") @customElement("ak-interface-user")
export class UserInterface extends Interface { export class UserInterface extends OAuthInterface {
@property({ type: Boolean }) @property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@ -141,12 +141,13 @@ export class UserInterface extends Interface {
}); });
}); });
window.addEventListener(EVENT_WS_MESSAGE, () => { window.addEventListener(EVENT_WS_MESSAGE, () => {
this.firstUpdated(); this.firstUpdated(new Map());
}); });
configureSentry(true); configureSentry(true);
} }
async firstUpdated(): Promise<void> { async firstUpdated(_changedProperties: Map<PropertyKey, unknown>): Promise<void> {
super.firstUpdated(_changedProperties);
this.me = await me(); this.me = await me();
const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({ const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false, seen: false,
@ -275,10 +276,7 @@ export class UserInterface extends Interface {
</div>` </div>`
: html``} : html``}
<div class="pf-c-page__header-tools-item"> <div class="pf-c-page__header-tools-item">
<a <a href="#/oauth-signou" class="pf-c-button pf-m-plain">
href="/flows/-/default/invalidation/"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Sign out")}> <pf-tooltip position="top" content=${msg("Sign out")}>
<i class="fas fa-sign-out-alt" aria-hidden="true"></i> <i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</pf-tooltip> </pf-tooltip>