// Package auth provides local JWT-based authentication replacing Keycloak OIDC. // Tokens are signed with HMAC-SHA256 using a secret configured via VEYLANT_AUTH_JWT_SECRET. package auth import ( "context" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/veylant/ia-gateway/internal/middleware" ) // LocalJWTVerifier implements middleware.TokenVerifier using self-signed HS256 tokens. type LocalJWTVerifier struct { secret []byte } // NewLocalJWTVerifier creates a verifier that validates tokens signed with secret. func NewLocalJWTVerifier(secret string) *LocalJWTVerifier { return &LocalJWTVerifier{secret: []byte(secret)} } // veylantClaims is the JWT payload structure for tokens we issue. type veylantClaims struct { Email string `json:"email"` TenantID string `json:"tenant_id"` Roles []string `json:"roles"` Department string `json:"department,omitempty"` Name string `json:"name,omitempty"` jwt.RegisteredClaims } // Verify implements middleware.TokenVerifier. func (v *LocalJWTVerifier) Verify(_ context.Context, rawToken string) (*middleware.UserClaims, error) { token, err := jwt.ParseWithClaims(rawToken, &veylantClaims{}, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return v.secret, nil }) if err != nil { return nil, fmt.Errorf("token verification failed: %w", err) } claims, ok := token.Claims.(*veylantClaims) if !ok || !token.Valid { return nil, errors.New("invalid token claims") } return &middleware.UserClaims{ UserID: claims.Subject, TenantID: claims.TenantID, Email: claims.Email, Roles: claims.Roles, Department: claims.Department, }, nil } // GenerateToken creates a signed JWT for the given claims. func GenerateToken(claims *middleware.UserClaims, name, secret string, ttlHours int) (string, error) { now := time.Now() jwtClaims := veylantClaims{ Email: claims.Email, TenantID: claims.TenantID, Roles: claims.Roles, Department: claims.Department, Name: name, RegisteredClaims: jwt.RegisteredClaims{ Subject: claims.UserID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(ttlHours) * time.Hour)), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaims) return token.SignedString([]byte(secret)) }