chore: remove unused internal/auth package
JWT validation moved to individual services (Option B). Gateway no longer needs auth module.
This commit is contained in:
@@ -1,198 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validator validates JWT tokens from Authentik.
|
|
||||||
type Validator struct {
|
|
||||||
issuer string
|
|
||||||
audience string
|
|
||||||
jwksURL string
|
|
||||||
client *http.Client
|
|
||||||
|
|
||||||
mu sync.RWMutex
|
|
||||||
keyset *keySet
|
|
||||||
lastFetch time.Time
|
|
||||||
cacheTTL time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type keySet struct {
|
|
||||||
Keys map[string]interface{} `json:"keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Claims holds Authentik JWT claims.
|
|
||||||
type Claims struct {
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
Permissions []string `json:"permissions"`
|
|
||||||
Groups []string `json:"groups"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewValidator creates a validator for an Authentik app.
|
|
||||||
// appSlug is the OAuth2 provider slug (e.g., "gateway", "sqs", "memory").
|
|
||||||
func NewValidator(appSlug string) *Validator {
|
|
||||||
issuer := fmt.Sprintf("https://authentik.riotpiao.com/application/o/%s/", appSlug)
|
|
||||||
return &Validator{
|
|
||||||
issuer: issuer,
|
|
||||||
audience: appSlug,
|
|
||||||
jwksURL: issuer + "jwks/",
|
|
||||||
client: &http.Client{Timeout: 10 * time.Second},
|
|
||||||
cacheTTL: 15 * time.Minute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateToken extracts and validates a JWT from the Authorization header.
|
|
||||||
// Returns the claims if valid, or an error if invalid/missing.
|
|
||||||
func (v *Validator) ValidateToken(authHeader string) (*Claims, error) {
|
|
||||||
// Extract token from "Bearer <token>"
|
|
||||||
if authHeader == "" {
|
|
||||||
return nil, fmt.Errorf("authorization header missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(authHeader, " ", 2)
|
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
||||||
return nil, fmt.Errorf("invalid authorization header format")
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenString := parts[1]
|
|
||||||
|
|
||||||
// Ensure JWKS is fresh
|
|
||||||
if err := v.ensureKeys(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to fetch JWKS: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JWT with custom key func
|
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
// Verify algorithm is RS256 only
|
|
||||||
if token.Method.Alg() != "RS256" {
|
|
||||||
return nil, fmt.Errorf("unexpected algorithm: %v", token.Header["alg"])
|
|
||||||
}
|
|
||||||
|
|
||||||
kid, ok := token.Header["kid"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("kid not in token header")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get key from cache
|
|
||||||
v.mu.RLock()
|
|
||||||
keys := v.keyset.Keys
|
|
||||||
v.mu.RUnlock()
|
|
||||||
|
|
||||||
rawKey, exists := keys[kid]
|
|
||||||
if !exists {
|
|
||||||
// Try refreshing JWKS (key rotation)
|
|
||||||
if err := v.fetchKeys(); err == nil {
|
|
||||||
v.mu.RLock()
|
|
||||||
rawKey, exists = v.keyset.Keys[kid]
|
|
||||||
v.mu.RUnlock()
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("key %s not found", kid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawKey, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("token validation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !token.Valid {
|
|
||||||
return nil, fmt.Errorf("token invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(*Claims)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid claims")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify issuer
|
|
||||||
if claims.Issuer != v.issuer {
|
|
||||||
return nil, fmt.Errorf("issuer mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify audience (check if audience is in the claims)
|
|
||||||
if len(claims.Audience) == 0 {
|
|
||||||
return nil, fmt.Errorf("no audience in token")
|
|
||||||
}
|
|
||||||
audFound := false
|
|
||||||
for _, aud := range claims.Audience {
|
|
||||||
if aud == v.audience {
|
|
||||||
audFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !audFound {
|
|
||||||
return nil, fmt.Errorf("audience mismatch: expected %s", v.audience)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify exp, nbf, iat
|
|
||||||
now := time.Now().Unix()
|
|
||||||
if claims.ExpiresAt != nil && claims.ExpiresAt.Unix() < now {
|
|
||||||
return nil, fmt.Errorf("token expired")
|
|
||||||
}
|
|
||||||
if claims.NotBefore != nil && claims.NotBefore.Unix() > now+60 {
|
|
||||||
return nil, fmt.Errorf("token not yet valid")
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission checks if claims contain the required permission.
|
|
||||||
// Wildcard "*" grants all permissions.
|
|
||||||
func (v *Validator) HasPermission(claims *Claims, permission string) bool {
|
|
||||||
for _, p := range claims.Permissions {
|
|
||||||
if p == "*" || p == permission {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureKeys refreshes JWKS if cache is stale.
|
|
||||||
func (v *Validator) ensureKeys() error {
|
|
||||||
v.mu.RLock()
|
|
||||||
cacheValid := v.keyset != nil && time.Since(v.lastFetch) < v.cacheTTL
|
|
||||||
v.mu.RUnlock()
|
|
||||||
|
|
||||||
if cacheValid {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.fetchKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchKeys fetches JWKS from Authentik.
|
|
||||||
func (v *Validator) fetchKeys() error {
|
|
||||||
resp, err := v.client.Get(v.jwksURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("GET %s failed: %w", v.jwksURL, err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("JWKS endpoint returned %d: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
var ks keySet
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&ks); err != nil {
|
|
||||||
return fmt.Errorf("decode JWKS failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.mu.Lock()
|
|
||||||
v.keyset = &ks
|
|
||||||
v.lastFetch = time.Now()
|
|
||||||
v.mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user