root: support running authentik in subpath (#8675)

* initial subpath support

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

* make outpost compatible

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

* fix static files somewhat

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

* fix web interface

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

* fix most static stuff

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

* fix most web links

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

* fix websocket

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

* fix URL for static files

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

* format web

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

* add root redirect for subpath

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

* update docs

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

* set cookie path

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

* Update internal/config/struct.go

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens L. <jens@beryju.org>

* fix sfe

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

* bump required version

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

* fix flow background

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

* fix lint and some more links

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

* format

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

* fix impersonate

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

* fix

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Jens L.
2024-11-26 15:38:23 +01:00
committed by GitHub
parent ee15dbf671
commit 5e72ec9c0c
43 changed files with 236 additions and 96 deletions

View File

@ -84,8 +84,8 @@ class CurrentBrandSerializer(PassiveSerializer):
matched_domain = CharField(source="domain")
branding_title = CharField()
branding_logo = CharField()
branding_favicon = CharField()
branding_logo = CharField(source="branding_logo_url")
branding_favicon = CharField(source="branding_favicon_url")
ui_footer_links = ListField(
child=FooterLinkSerializer(),
read_only=True,

View File

@ -10,6 +10,7 @@ from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.models import SerializerModel
LOGGER = get_logger()
@ -71,6 +72,18 @@ class Brand(SerializerModel):
)
attributes = models.JSONField(default=dict, blank=True)
def branding_logo_url(self) -> str:
"""Get branding_logo with the correct prefix"""
if self.branding_logo.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_logo
return self.branding_logo
def branding_favicon_url(self) -> str:
"""Get branding_favicon with the correct prefix"""
if self.branding_favicon.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon
@property
def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer

View File

@ -9,6 +9,9 @@
versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
api: {
base: "{{ base_url }}",
},
};
window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %}

View File

@ -9,8 +9,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">

View File

@ -4,7 +4,7 @@
{% load i18n %}
{% block head_before %}
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
@ -13,7 +13,7 @@
{% block head %}
<style>
:root {
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
@ -50,7 +50,7 @@
<div class="ak-login-container">
<main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo }}" alt="authentik Logo" />
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">

View File

@ -16,6 +16,7 @@ from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand
from authentik.core.models import UserTypes
from authentik.lib.config import CONFIG
from authentik.policies.denied import AccessDeniedResponse
@ -51,6 +52,7 @@ class InterfaceView(TemplateView):
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
return super().get_context_data(**kwargs)

View File

@ -6,8 +6,8 @@
<script src="{% versioned_script 'dist/enterprise/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 }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}">
{% include "base/header_js.html" %}
{% endblock %}

View File

@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Token
from authentik.core.types import UserSettingSerializer
from authentik.flows.challenge import FlowLayout
from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.utils.reflection import class_to_path
from authentik.policies.models import PolicyBindingModel
@ -177,9 +178,13 @@ class Flow(SerializerModel, PolicyBindingModel):
"""Get the URL to the background image. If the name is /static or starts with http
it is returned as-is"""
if not self.background:
return "/static/dist/assets/images/flow_background.jpg"
if self.background.name.startswith("http") or self.background.name.startswith("/static"):
return (
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
)
if self.background.name.startswith("http"):
return self.background.name
if self.background.name.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.background.name
return self.background.url
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)

View File

@ -9,8 +9,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">

View File

@ -135,6 +135,7 @@ web:
# No default here as it's set dynamically
# workers: 2
threads: 4
path: /
worker:
concurrency: 2

View File

@ -36,6 +36,7 @@ from authentik.lib.utils.http import authentik_user_agent
from authentik.lib.utils.reflection import get_env
LOGGER = get_logger()
_root_path = CONFIG.get("web.path", "/")
class SentryIgnoredException(Exception):
@ -90,7 +91,7 @@ def traces_sampler(sampling_context: dict) -> float:
path = sampling_context.get("asgi_scope", {}).get("path", "")
_type = sampling_context.get("asgi_scope", {}).get("type", "")
# Ignore all healthcheck routes
if path.startswith("/-/health") or path.startswith("/-/metrics"):
if path.startswith(f"{_root_path}-/health") or path.startswith(f"{_root_path}-/metrics"):
return 0
if _type == "websocket":
return 0

View File

@ -32,6 +32,8 @@ LOGIN_URL = "authentik_flows:default-authentication"
# Custom user model
AUTH_USER_MODEL = "authentik_core.User"
CSRF_COOKIE_PATH = LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH = CONFIG.get("web.path", "/")
CSRF_COOKIE_NAME = "authentik_csrf"
CSRF_HEADER_NAME = "HTTP_X_AUTHENTIK_CSRF"
LANGUAGE_COOKIE_NAME = "authentik_language"
@ -427,7 +429,7 @@ if _ERROR_REPORTING:
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATICFILES_DIRS = [BASE_DIR / Path("web")]
STATIC_URL = "/static/"
STATIC_URL = CONFIG.get("web.path", "/") + "static/"
STORAGES = {
"staticfiles": {

View File

@ -4,6 +4,7 @@ from django.urls import include, path
from structlog.stdlib import get_logger
from authentik.core.views import error
from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_apps
from authentik.root.monitoring import LiveView, MetricsView, ReadyView
@ -14,7 +15,7 @@ handler403 = error.ForbiddenView.as_view()
handler404 = error.NotFoundView.as_view()
handler500 = error.ServerErrorView.as_view()
urlpatterns = []
_urlpatterns = []
for _authentik_app in get_apps():
mountpoints = None
@ -35,7 +36,7 @@ for _authentik_app in get_apps():
namespace=namespace,
),
)
urlpatterns.append(_path)
_urlpatterns.append(_path)
LOGGER.debug(
"Mounted URLs",
app_name=_authentik_app.name,
@ -43,8 +44,10 @@ for _authentik_app in get_apps():
namespace=namespace,
)
urlpatterns += [
_urlpatterns += [
path("-/metrics/", MetricsView.as_view(), name="metrics"),
path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
]
urlpatterns = [path(CONFIG.get("web.path", "/")[1:], include(_urlpatterns))]

View File

@ -2,13 +2,16 @@
from importlib import import_module
from channels.routing import URLRouter
from django.urls import path
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_apps
LOGGER = get_logger()
websocket_urlpatterns = []
_websocket_urlpatterns = []
for _authentik_app in get_apps():
try:
api_urls = import_module(f"{_authentik_app.name}.urls")
@ -17,8 +20,15 @@ for _authentik_app in get_apps():
if not hasattr(api_urls, "websocket_urlpatterns"):
continue
urls: list = api_urls.websocket_urlpatterns
websocket_urlpatterns.extend(urls)
_websocket_urlpatterns.extend(urls)
LOGGER.debug(
"Mounted Websocket URLs",
app_name=_authentik_app.name,
)
websocket_urlpatterns = [
path(
CONFIG.get("web.path", "/")[1:],
URLRouter(_websocket_urlpatterns),
),
]

View File

@ -47,7 +47,7 @@ func checkServer() int {
h := &http.Client{
Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport),
}
url := fmt.Sprintf("http://%s/-/health/live/", config.Get().Listen.HTTP)
url := fmt.Sprintf("http://%s%s-/health/live/", config.Get().Listen.HTTP, config.Get().Web.Path)
res, err := h.Head(url)
if err != nil {
log.WithError(err).Warning("failed to send healthcheck request")

View File

@ -61,7 +61,7 @@ var rootCmd = &cobra.Command{
ex := common.Init()
defer common.Defer()
u, err := url.Parse(fmt.Sprintf("http://%s", config.Get().Listen.HTTP))
u, err := url.Parse(fmt.Sprintf("http://%s%s", config.Get().Listen.HTTP, config.Get().Web.Path))
if err != nil {
panic(err)
}

View File

@ -14,6 +14,7 @@ type Config struct {
// Config for both core and outposts
Debug bool `yaml:"debug" env:"AUTHENTIK_DEBUG, overwrite"`
Listen ListenConfig `yaml:"listen" env:", prefix=AUTHENTIK_LISTEN__"`
Web WebConfig `yaml:"web" env:", prefix=AUTHENTIK_WEB__"`
// Outpost specific config
// These are only relevant for proxy/ldap outposts, and cannot be set via YAML
@ -72,3 +73,7 @@ type OutpostConfig struct {
Discover bool `yaml:"discover" env:"DISCOVER, overwrite"`
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"DISABLE_EMBEDDED_OUTPOST, overwrite"`
}
type WebConfig struct {
Path string `yaml:"path" env:"PATH, overwrite"`
}

View File

@ -56,10 +56,10 @@ type APIController struct {
func NewAPIController(akURL url.URL, token string) *APIController {
rsp := sentry.StartSpan(context.Background(), "authentik.outposts.init")
config := api.NewConfiguration()
config.Host = akURL.Host
config.Scheme = akURL.Scheme
config.HTTPClient = &http.Client{
apiConfig := api.NewConfiguration()
apiConfig.Host = akURL.Host
apiConfig.Scheme = akURL.Scheme
apiConfig.HTTPClient = &http.Client{
Transport: web.NewUserAgentTransport(
constants.OutpostUserAgent(),
web.NewTracingTransport(
@ -68,10 +68,15 @@ func NewAPIController(akURL url.URL, token string) *APIController {
),
),
}
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
apiConfig.Servers = api.ServerConfigurations{
{
URL: fmt.Sprintf("%sapi/v3", akURL.Path),
},
}
apiConfig.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
// create the API client, with the transport
apiClient := api.NewAPIClient(config)
apiClient := api.NewAPIClient(apiConfig)
log := log.WithField("logger", "authentik.outpost.ak-api-controller")

View File

@ -17,7 +17,7 @@ import (
)
func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
pathTemplate := "%s://%s/ws/outpost/%s/?%s"
pathTemplate := "%s://%s%sws/outpost/%s/?%s"
query := akURL.Query()
query.Set("instance_uuid", ac.instanceUUID.String())
scheme := strings.ReplaceAll(akURL.Scheme, "http", "ws")
@ -37,7 +37,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
},
}
ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, outpostUUID, akURL.Query().Encode()), header)
ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, akURL.Path, outpostUUID, akURL.Query().Encode()), header)
if err != nil {
ac.logger.WithError(err).Warning("failed to connect websocket")
return err
@ -83,6 +83,7 @@ func (ac *APIController) reconnectWS() {
u := url.URL{
Host: ac.Client.GetConfig().Host,
Scheme: ac.Client.GetConfig().Scheme,
Path: strings.ReplaceAll(ac.Client.GetConfig().Servers[0].URL, "api/v3", ""),
}
attempt := 1
for {

View File

@ -46,7 +46,7 @@ func (ws *WebServer) runMetricsServer() {
).ServeHTTP(rw, r)
// Get upstream metrics
re, err := http.NewRequest("GET", fmt.Sprintf("%s/-/metrics/", ws.ul.String()), nil)
re, err := http.NewRequest("GET", fmt.Sprintf("%s%s-/metrics/", ws.upstreamURL.String(), config.Get().Web.Path), nil)
if err != nil {
l.WithError(err).Warning("failed to get upstream metrics")
return

View File

@ -9,14 +9,15 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"goauthentik.io/internal/config"
"goauthentik.io/internal/utils/sentry"
)
func (ws *WebServer) configureProxy() {
// Reverse proxy to the application server
director := func(req *http.Request) {
req.URL.Scheme = ws.ul.Scheme
req.URL.Host = ws.ul.Host
req.URL.Scheme = ws.upstreamURL.Scheme
req.URL.Host = ws.upstreamURL.Host
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
@ -32,7 +33,10 @@ func (ws *WebServer) configureProxy() {
}
rp.ErrorHandler = ws.proxyErrorHandler
rp.ModifyResponse = ws.proxyModifyResponse
ws.m.PathPrefix("/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
ws.mainRouter.PathPrefix(config.Get().Web.Path).Path("/-/health/live/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(204)
}))
ws.mainRouter.PathPrefix(config.Get().Web.Path).HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
if !ws.g.IsRunning() {
ws.proxyErrorHandler(rw, r, errors.New("authentik starting"))
return

View File

@ -14,46 +14,74 @@ import (
)
func (ws *WebServer) configureStatic() {
staticRouter := ws.lh.NewRoute().Subrouter()
// Setup routers
staticRouter := ws.loggingRouter.NewRoute().Subrouter()
staticRouter.Use(ws.staticHeaderMiddleware)
staticRouter.Use(web.DisableIndex)
indexLessRouter := staticRouter.NewRoute().Subrouter()
// Specifically disable index
indexLessRouter.Use(web.DisableIndex)
distFs := http.FileServer(http.Dir("./web/dist"))
authentikHandler := http.StripPrefix("/static/authentik/", http.FileServer(http.Dir("./web/authentik")))
// Root file paths, from which they should be accessed
staticRouter.PathPrefix("/static/dist/").Handler(http.StripPrefix("/static/dist/", distFs))
staticRouter.PathPrefix("/static/authentik/").Handler(authentikHandler)
pathStripper := func(handler http.Handler, paths ...string) http.Handler {
h := handler
for _, path := range paths {
h = http.StripPrefix(path, h)
}
return h
}
// Also serve assets folder in specific interfaces since fonts in patternfly are imported
// with a relative path
staticRouter.PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
helpHandler := http.FileServer(http.Dir("./website/help/"))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/dist/").Handler(pathStripper(
distFs,
"static/dist/",
config.Get().Web.Path,
))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/authentik/").Handler(pathStripper(
http.FileServer(http.Dir("./web/authentik")),
"static/authentik/",
config.Get().Web.Path,
))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
web.DisableIndex(http.StripPrefix(fmt.Sprintf("/if/flow/%s", vars["flow_slug"]), distFs)).ServeHTTP(rw, r)
pathStripper(
distFs,
"if/flow/"+vars["flow_slug"],
config.Get().Web.Path,
).ServeHTTP(rw, r)
})
staticRouter.PathPrefix("/if/admin/assets").Handler(http.StripPrefix("/if/admin", distFs))
staticRouter.PathPrefix("/if/user/assets").Handler(http.StripPrefix("/if/user", distFs))
staticRouter.PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/admin/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/admin", config.Get().Web.Path), distFs))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/user/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/user", config.Get().Web.Path), distFs))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
web.DisableIndex(http.StripPrefix(fmt.Sprintf("/if/rac/%s", vars["app_slug"]), distFs)).ServeHTTP(rw, r)
pathStripper(
distFs,
"if/rac/"+vars["app_slug"],
config.Get().Web.Path,
).ServeHTTP(rw, r)
})
// Media files, if backend is file
if config.Get().Storage.Media.Backend == "file" {
fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)))
staticRouter.PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
fsMedia.ServeHTTP(w, r)
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
fsMedia.ServeHTTP(w, r)
})
}
staticRouter.PathPrefix("/if/help/").Handler(http.StripPrefix("/if/help/", http.FileServer(http.Dir("./website/help/"))))
staticRouter.PathPrefix("/help").Handler(http.RedirectHandler("/if/help/", http.StatusMovedPermanently))
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper(
helpHandler,
config.Get().Web.Path,
"/if/help/",
))
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/help").Handler(http.RedirectHandler(fmt.Sprintf("%sif/help/", config.Get().Web.Path), http.StatusMovedPermanently))
// Static misc files
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
staticRouter.PathPrefix(config.Get().Web.Path).Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header()["Content-Type"] = []string{"text/plain"}
rw.WriteHeader(200)
_, err := rw.Write(staticWeb.RobotsTxt)
@ -61,7 +89,7 @@ func (ws *WebServer) configureStatic() {
ws.log.WithError(err).Warning("failed to write response")
}
})
ws.lh.Path("/.well-known/security.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
staticRouter.PathPrefix(config.Get().Web.Path).Path("/.well-known/security.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header()["Content-Type"] = []string{"text/plain"}
rw.WriteHeader(200)
_, err := rw.Write(staticWeb.SecurityTxt)

View File

@ -33,13 +33,13 @@ type WebServer struct {
ProxyServer *proxyv2.ProxyServer
BrandTLS *brand_tls.Watcher
g *gounicorn.GoUnicorn
gr bool
m *mux.Router
lh *mux.Router
log *log.Entry
uc *http.Client
ul *url.URL
g *gounicorn.GoUnicorn
gunicornReady bool
mainRouter *mux.Router
loggingRouter *mux.Router
log *log.Entry
upstreamClient *http.Client
upstreamURL *url.URL
}
const UnixSocketName = "authentik-core.sock"
@ -73,17 +73,22 @@ func NewWebServer() *WebServer {
u, _ := url.Parse("http://localhost:8000")
ws := &WebServer{
m: mainHandler,
lh: loggingHandler,
log: l,
gr: true,
uc: upstreamClient,
ul: u,
mainRouter: mainHandler,
loggingRouter: loggingHandler,
log: l,
gunicornReady: true,
upstreamClient: upstreamClient,
upstreamURL: u,
}
ws.configureStatic()
ws.configureProxy()
// Redirect for sub-folder
if sp := config.Get().Web.Path; sp != "/" {
ws.mainRouter.Path("/").Handler(http.RedirectHandler(sp, http.StatusFound))
}
hcUrl := fmt.Sprintf("%s%s-/health/live/", ws.upstreamURL.String(), config.Get().Web.Path)
ws.g = gounicorn.New(func() bool {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/-/health/live/", ws.ul.String()), nil)
req, err := http.NewRequest(http.MethodGet, hcUrl, nil)
if err != nil {
ws.log.WithError(err).Warning("failed to create request for healthcheck")
return false
@ -107,7 +112,7 @@ func (ws *WebServer) Start() {
func (ws *WebServer) attemptStartBackend() {
for {
if !ws.gr {
if !ws.gunicornReady {
return
}
err := ws.g.Start()
@ -135,7 +140,7 @@ func (ws *WebServer) Core() *gounicorn.GoUnicorn {
}
func (ws *WebServer) upstreamHttpClient() *http.Client {
return ws.uc
return ws.upstreamClient
}
func (ws *WebServer) Shutdown() {
@ -160,7 +165,7 @@ func (ws *WebServer) listenPlain() {
func (ws *WebServer) serve(listener net.Listener) {
srv := &http.Server{
Handler: ws.m,
Handler: ws.mainRouter,
}
// See https://golang.org/pkg/net/http/#Server.Shutdown

View File

@ -20,6 +20,9 @@ interface GlobalAuthentik {
brand: {
branding_logo: string;
};
api: {
base: string;
};
}
function ak(): GlobalAuthentik {
@ -41,7 +44,7 @@ class SimpleFlowExecutor {
}
get apiURL() {
return `/api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
start() {

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import {
@ -112,7 +113,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
// prettier-ignore
const sidebarContent: SidebarEntry[] = [
["/if/user/", msg("User interface"), { "?isAbsoluteLink": true, "?highlight": true }],
[`${globalAK().api.base}if/user/`, msg("User interface"), { "?isAbsoluteLink": true, "?highlight": true }],
[null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global";
import "@goauthentik/components/ak-text-input";
import { Form } from "@goauthentik/elements/forms/Form";
@ -20,7 +21,7 @@ export class UserImpersonateForm extends Form<ImpersonationRequest> {
impersonationRequest: data,
})
.then(() => {
window.location.href = "/";
window.location.href = globalAK().api.base;
});
}

View File

@ -68,7 +68,7 @@ export function getMetaContent(key: string): string {
}
export const DEFAULT_CONFIG = new Configuration({
basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3",
basePath: `${globalAK().api.base}api/v3`,
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
},

View File

@ -11,6 +11,9 @@ export interface GlobalAuthentik {
versionFamily: string;
versionSubdomain: string;
build: string;
api: {
base: string;
};
}
export interface AuthentikWindow {
@ -35,6 +38,9 @@ export function globalAK(): GlobalAuthentik {
versionFamily: "",
versionSubdomain: "",
build: "",
api: {
base: process.env.AK_API_BASE_PATH || window.location.origin,
},
};
}
return ak;

View File

@ -1,4 +1,5 @@
import { EVENT_MESSAGE, EVENT_WS_MESSAGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { MessageLevel } from "@goauthentik/common/messages";
import { msg } from "@lit/localize";
@ -21,9 +22,8 @@ export class WebsocketClient {
connect(): void {
if (navigator.webdriver) return;
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host
}/ws/client/`;
const apiURL = new URL(globalAK().api.base);
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${apiURL.host}${apiURL.pathname}ws/client/`;
this.messageSocket = new WebSocket(wsUrl);
this.messageSocket.addEventListener("open", () => {
console.debug(`authentik/ws: connected to ${wsUrl}`);

View File

@ -1,3 +1,4 @@
import { globalAK } from "@goauthentik/common/global";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
@ -75,7 +76,9 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
: "pf-m-gold"}"
>
${message}
<a href="/if/admin/#/enterprise/licenses"> ${msg("Click here for more info.")} </a>
<a href="${globalAK().api.base}if/admin/#/enterprise/licenses"
>${msg("Click here for more info.")}</a
>
</div>`;
}

View File

@ -1,5 +1,6 @@
import { RequestInfo } from "@goauthentik/common/api/middleware";
import { EVENT_API_DRAWER_TOGGLE, EVENT_REQUEST_POST } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { AKElement } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize";
@ -86,7 +87,9 @@ export class APIDrawer extends AKElement {
<h1 class="pf-c-notification-drawer__header-title">
${msg("API Requests")}
</h1>
<a href="/api/v3/" target="_blank">${msg("Open API Browser")}</a>
<a href="${globalAK().api.base}api/v3/" target="_blank"
>${msg("Open API Browser")}</a
>
</div>
<div class="pf-c-notification-drawer__header-action">
<div class="pf-c-notification-drawer__header-action-close">

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { actionToLabel } from "@goauthentik/common/labels";
import { MessageLevel } from "@goauthentik/common/messages";
import { me } from "@goauthentik/common/users";
@ -100,7 +101,7 @@ export class NotificationDrawer extends AKElement {
html`
<a
class="pf-c-dropdown__toggle pf-m-plain"
href="/if/admin/#/events/log/${item.event?.pk}"
href="${globalAK().api.base}if/admin/#/events/log/${item.event?.pk}"
>
<pf-tooltip position="top" content=${msg("Show details")}>
<i class="fas fa-share-square"></i>

View File

@ -1,3 +1,4 @@
import { globalAK } from "@goauthentik/common/global";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
@ -35,7 +36,11 @@ export class SidebarUser extends AKElement {
render(): TemplateResult {
return html`
<a href="/if/user/#/settings" class="pf-c-nav__link user-avatar" id="user-settings">
<a
href="${globalAK().api.base}if/user/#/settings"
class="pf-c-nav__link user-avatar"
id="user-settings"
>
${until(
me().then((u) => {
return html`<img
@ -47,7 +52,11 @@ export class SidebarUser extends AKElement {
html``,
)}
</a>
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout">
<a
href="${globalAK().api.base}flows/-/default/invalidation/"
class="pf-c-nav__link user-logout"
id="logout"
>
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</a>
`;

View File

@ -461,6 +461,7 @@ export class FlowExecutor extends Interface implements StageHost {
await import("@goauthentik/flow/FlowInspector");
return html`<ak-flow-inspector
class="pf-c-drawer__panel pf-m-width-33"
.flowSlug=${this.flowSlug}
></ak-flow-inspector>`;
}

View File

@ -19,7 +19,8 @@ import { FlowInspection, FlowsApi, ResponseError, Stage } from "@goauthentik/api
@customElement("ak-flow-inspector")
export class FlowInspector extends AKElement {
flowSlug: string;
@property()
flowSlug?: string;
@property({ attribute: false })
state?: FlowInspection;
@ -55,7 +56,6 @@ export class FlowInspector extends AKElement {
constructor() {
super();
this.flowSlug = window.location.pathname.split("/")[3];
window.addEventListener(EVENT_FLOW_ADVANCE, this.advanceHandler as EventListener);
}
@ -67,7 +67,7 @@ export class FlowInspector extends AKElement {
advanceHandler = (): void => {
new FlowsApi(DEFAULT_CONFIG)
.flowsInspectorGet({
flowSlug: this.flowSlug,
flowSlug: this.flowSlug || "",
})
.then((state) => {
this.error = undefined;

View File

@ -1,3 +1,4 @@
import { globalAK } from "@goauthentik/common/global";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
@ -47,7 +48,9 @@ export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
str`You've logged out of ${this.challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`,
)}
</p>
<a href="/" class="pf-c-button pf-m-primary"> ${msg("Go back to overview")} </a>
<a href="${globalAK().api.base}" class="pf-c-button pf-m-primary">
${msg("Go back to overview")}
</a>
${this.challenge.invalidationFlowUrl
? html`
<a

View File

@ -1,4 +1,5 @@
import { PFSize } from "@goauthentik/common/enums.js";
import { globalAK } from "@goauthentik/common/global";
import { truncateWords } from "@goauthentik/common/utils";
import "@goauthentik/elements/AppIcon";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
@ -77,7 +78,8 @@ export class LibraryApplication extends AKElement {
? html`
<a
class="pf-c-button pf-m-control pf-m-small pf-m-block"
href="/if/admin/#/core/applications/${application?.slug}"
href="${globalAK().api
.base}if/admin/#/core/applications/${application?.slug}"
>
<i class="fas fa-edit"></i>&nbsp;${msg("Edit")}
</a>

View File

@ -1,4 +1,4 @@
import { docLink } from "@goauthentik/common/global";
import { docLink, globalAK } from "@goauthentik/common/global";
import { AKElement } from "@goauthentik/elements/Base";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
@ -49,7 +49,7 @@ export class LibraryPageApplicationEmptyList extends AKElement {
<a
aria-disabled="false"
class="cta pf-c-button pf-m-secondary"
href="/if/admin/${href}"
href="${globalAK().api.base}if/admin/${href}"
>${msg("Create a new application")}</a
>
</div>

View File

@ -4,6 +4,7 @@ import {
EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_WS_MESSAGE,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { configureSentry } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
@ -207,7 +208,7 @@ class UserInterfacePresentation extends AKElement {
${this.renderSettings()}
<div class="pf-c-page__header-tools-item">
<a
href="/flows/-/default/invalidation/"
href="${globalAK().api.base}flows/-/default/invalidation/"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Sign out")}>
@ -349,7 +350,7 @@ class UserInterfacePresentation extends AKElement {
return html`<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="/if/admin/"
href="${globalAK().api.base}if/admin/"
>
${msg("Admin interface")}
</a>`;

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { MessageLevel } from "@goauthentik/common/messages";
import { refreshMe } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
@ -173,7 +174,10 @@ export class UserSettingsFlowExecutor
`authentik/user/flows: unsupported stage type ${this.challenge.component}`,
);
return html`
<a href="/if/flow/${this.flowSlug}/" class="pf-c-button pf-m-primary">
<a
href="${globalAK().api.base}if/flow/${this.flowSlug}/"
class="pf-c-button pf-m-primary"
>
${msg("Open settings")}
</a>
`;

View File

@ -1,3 +1,4 @@
import { globalAK } from "@goauthentik/common/global";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { PromptStage } from "@goauthentik/flow/stages/prompt/PromptStage";
@ -50,7 +51,8 @@ export class UserSettingsPromptStage extends PromptStage {
${this.host.brand?.flowUnenrollment
? html` <a
class="pf-c-button pf-m-danger"
href="/if/flow/${this.host.brand.flowUnenrollment}/"
href="${globalAK().api.base}if/flow/${this.host.brand
.flowUnenrollment}/"
>
${msg("Delete account")}
</a>`

View File

@ -345,6 +345,16 @@ Configure Celery worker concurrency for authentik worker (see https://docs.celer
Defaults to 2.
### `AUTHENTIK_WEB__PATH`
:::info
Requires authentik 2024.8
:::
Configure the path under which authentik is serverd. For example to access authentik under `https://my.domain/authentik/`, set this to `/authentik/`. Value _must_ contain both a leading and trailing slash.
Defaults to `/`.
## System settings <span class="badge badge--version">authentik 2024.2+</span>
Additional settings are configurable using the Admin interface, under **System** -> **Settings** or using the API.

View File

@ -51,6 +51,8 @@ server {
add_header Strict-Transport-Security "max-age=63072000" always;
# Proxy site
# Location can be set to a subpath if desired, see documentation linked below:
# https://goauthentik.io/docs/installation/configuration#authentik_web__path
location / {
proxy_pass https://authentik;
proxy_http_version 1.1;