// 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() } }