Add Authelia as centralized authentication and OIDC provider

Authelia runs as a managed native service on Wild Central, providing
two integration patterns for network services:

- Forward-auth via HAProxy for apps without native SSO (Lua auth-request
  script intercepts requests, redirects unauthenticated users to login portal)
- OIDC provider for apps with native support (Gitea, Grafana, etc.)

Backend: new internal/authelia/ package with service manager, config
generation, file-based user management (argon2id), and OIDC client
management. API endpoints for status, config, users CRUD, and OIDC
clients CRUD.

HAProxy: config generator extended with lua-load, auth-request
directives, and Authelia backend. Auth directives scoped to session
cookie domain — only subdomains of the auth portal's parent are
eligible for forward-auth.

Frontend: Authentication page with enable/disable, user management,
OIDC client management (add/edit/delete), and protected domains
toggles. Advanced subsystem page for raw config view. Dashboard
service card and sidebar entries.
This commit is contained in:
2026-07-12 11:47:37 +00:00
parent 24bb976652
commit d796a79f79
27 changed files with 3097 additions and 11 deletions

View File

@@ -833,3 +833,213 @@ line3
t.Errorf("expected 0 broken services when no markers, got %d", len(broken))
}
}
// --- Authelia forward-auth tests ---
func TestGenerateWithOpts_AuthLuaLoad(t *testing.T) {
m := NewManager("")
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
AuthEnabled: true,
AuthBackend: "127.0.0.1:9091",
AuthDomain: "auth.payne.io",
})
if !strings.Contains(cfg, "lua-load /etc/haproxy/lua/haproxy-auth-request.lua") {
t.Error("config should contain lua-load directive when auth is enabled")
}
}
func TestGenerateWithOpts_NoAuthLuaLoadWhenDisabled(t *testing.T) {
m := NewManager("")
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
AuthEnabled: false,
})
if strings.Contains(cfg, "lua-load") {
t.Error("config should NOT contain lua-load when auth is disabled")
}
}
func TestGenerateWithOpts_AuthBackend(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "app.payne.io",
Domain: "app.payne.io",
Routes: []HTTPRouteBackend{
{Backend: "127.0.0.1:8080", AuthEnabled: true},
},
},
}
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
HTTPRoutes: httpRoutes,
CertsDir: "/tmp/certs/",
AuthEnabled: true,
AuthBackend: "127.0.0.1:9091",
AuthDomain: "auth.payne.io",
})
if !strings.Contains(cfg, "backend be_authelia") {
t.Error("config should contain Authelia backend")
}
if !strings.Contains(cfg, "server s0 127.0.0.1:9091") {
t.Error("config should contain Authelia server address")
}
}
func TestGenerateWithOpts_AuthInterceptDirective(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "app.payne.io",
Domain: "app.payne.io",
Routes: []HTTPRouteBackend{
{Backend: "127.0.0.1:8080", AuthEnabled: true},
},
},
}
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
HTTPRoutes: httpRoutes,
CertsDir: "/tmp/certs/",
AuthEnabled: true,
AuthBackend: "127.0.0.1:9091",
AuthDomain: "auth.payne.io",
})
if !strings.Contains(cfg, "lua.auth-request be_authelia") {
t.Error("config should contain auth-intercept directive for protected route")
}
if !strings.Contains(cfg, "auth.payne.io/?rd=") {
t.Error("config should contain redirect to auth portal")
}
}
func TestGenerateWithOpts_AuthDomainExempt(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "auth.payne.io",
Domain: "auth.payne.io",
Routes: []HTTPRouteBackend{
{Backend: "127.0.0.1:9091", AuthEnabled: true},
},
},
{
Name: "app.payne.io",
Domain: "app.payne.io",
Routes: []HTTPRouteBackend{
{Backend: "127.0.0.1:8080", AuthEnabled: true},
},
},
}
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
HTTPRoutes: httpRoutes,
CertsDir: "/tmp/certs/",
AuthEnabled: true,
AuthBackend: "127.0.0.1:9091",
AuthDomain: "auth.payne.io",
})
// Count auth-intercept directives — should be 1 (for app), not 2 (auth domain exempt)
count := strings.Count(cfg, "lua.auth-request")
if count != 1 {
t.Errorf("expected 1 auth-intercept directive (auth domain exempt), got %d", count)
}
// The auth comment should only appear for app.payne.io
if !strings.Contains(cfg, "# auth: app.payne.io") {
t.Error("should have auth comment for app.payne.io")
}
if strings.Contains(cfg, "# auth: auth.payne.io") {
t.Error("should NOT have auth comment for auth.payne.io (exempt)")
}
}
func TestGenerateWithOpts_NoAuthForUnprotectedRoutes(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "public.payne.io",
Domain: "public.payne.io",
Routes: []HTTPRouteBackend{
{Backend: "127.0.0.1:8080", AuthEnabled: false},
},
},
}
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
HTTPRoutes: httpRoutes,
CertsDir: "/tmp/certs/",
AuthEnabled: true,
AuthBackend: "127.0.0.1:9091",
AuthDomain: "auth.payne.io",
})
if strings.Contains(cfg, "lua.auth-request") {
t.Error("should NOT contain auth-intercept for unprotected routes")
}
}
func TestGenerateWithOpts_AuthDisabledNoDirectives(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "app.payne.io",
Domain: "app.payne.io",
Routes: []HTTPRouteBackend{
{Backend: "127.0.0.1:8080", AuthEnabled: true},
},
},
}
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
HTTPRoutes: httpRoutes,
CertsDir: "/tmp/certs/",
AuthEnabled: false,
})
if strings.Contains(cfg, "lua.auth-request") {
t.Error("should NOT contain auth directives when auth is globally disabled")
}
if strings.Contains(cfg, "be_authelia") {
t.Error("should NOT contain authelia backend when auth is globally disabled")
}
}
func TestGenerateWithOpts_AuthWithHeaders(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "app.payne.io",
Domain: "app.payne.io",
Routes: []HTTPRouteBackend{
{
Backend: "127.0.0.1:8080",
AuthEnabled: true,
Headers: &domains.HeaderConfig{
Request: map[string]string{"X-Custom": "value"},
},
},
},
},
}
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
HTTPRoutes: httpRoutes,
CertsDir: "/tmp/certs/",
AuthEnabled: true,
AuthBackend: "127.0.0.1:9091",
AuthDomain: "auth.payne.io",
})
// Should have both auth and headers
if !strings.Contains(cfg, "lua.auth-request") {
t.Error("should contain auth-intercept")
}
if !strings.Contains(cfg, "X-Custom") {
t.Error("should still contain custom headers alongside auth")
}
}