go-api/authentik.go
Snider d760e77e49 feat(authentik): add header extraction middleware and WithAuthentik option
Add permissive forward-auth middleware that extracts user identity from
X-authentik-* headers when TrustedProxy is enabled. Headers are ignored
when TrustedProxy is false to prevent spoofing from untrusted sources.

- GetUser(c) helper retrieves AuthentikUser from Gin context
- authentikMiddleware splits groups/entitlements on pipe delimiter
- /health and /swagger bypass header extraction
- WithAuthentik option wires middleware into the Engine

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 16:38:13 +00:00

120 lines
3.3 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api
import (
"strings"
"github.com/gin-gonic/gin"
)
// AuthentikConfig holds settings for the Authentik forward-auth integration.
type AuthentikConfig struct {
// Issuer is the OIDC issuer URL (e.g. https://auth.example.com/application/o/my-app/).
Issuer string
// ClientID is the OAuth2 client identifier.
ClientID string
// TrustedProxy enables reading X-authentik-* headers set by a reverse proxy.
// When false, headers are ignored to prevent spoofing from untrusted sources.
TrustedProxy bool
// PublicPaths lists additional paths that do not require authentication.
// /health and /swagger are always public.
PublicPaths []string
}
// AuthentikUser represents an authenticated user extracted from Authentik
// forward-auth headers or a validated JWT.
type AuthentikUser struct {
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
UID string `json:"uid"`
Groups []string `json:"groups,omitempty"`
Entitlements []string `json:"entitlements,omitempty"`
JWT string `json:"-"`
}
// HasGroup reports whether the user belongs to the named group.
func (u *AuthentikUser) HasGroup(group string) bool {
for _, g := range u.Groups {
if g == group {
return true
}
}
return false
}
// authentikUserKey is the Gin context key used to store the authenticated user.
const authentikUserKey = "authentik_user"
// GetUser retrieves the AuthentikUser from the Gin context.
// Returns nil when no user has been set (unauthenticated request or
// middleware not active).
func GetUser(c *gin.Context) *AuthentikUser {
val, exists := c.Get(authentikUserKey)
if !exists {
return nil
}
user, ok := val.(*AuthentikUser)
if !ok {
return nil
}
return user
}
// authentikMiddleware returns Gin middleware that extracts user identity from
// X-authentik-* headers set by a trusted reverse proxy (e.g. Traefik with
// Authentik forward-auth).
//
// The middleware is PERMISSIVE: it populates the context when headers are
// present but never rejects unauthenticated requests. Downstream handlers
// use GetUser to check authentication.
func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc {
// Build the set of public paths that skip header extraction entirely.
public := map[string]bool{
"/health": true,
"/swagger": true,
}
for _, p := range cfg.PublicPaths {
public[p] = true
}
return func(c *gin.Context) {
// Skip public paths.
path := c.Request.URL.Path
for p := range public {
if strings.HasPrefix(path, p) {
c.Next()
return
}
}
// Only read headers when the proxy is trusted.
if cfg.TrustedProxy {
username := c.GetHeader("X-authentik-username")
if username != "" {
user := &AuthentikUser{
Username: username,
Email: c.GetHeader("X-authentik-email"),
Name: c.GetHeader("X-authentik-name"),
UID: c.GetHeader("X-authentik-uid"),
JWT: c.GetHeader("X-authentik-jwt"),
}
if groups := c.GetHeader("X-authentik-groups"); groups != "" {
user.Groups = strings.Split(groups, "|")
}
if ent := c.GetHeader("X-authentik-entitlements"); ent != "" {
user.Entitlements = strings.Split(ent, "|")
}
c.Set(authentikUserKey, user)
}
}
c.Next()
}
}