veylant/internal/auth/jwt.go
2026-03-06 18:38:04 +01:00

81 lines
2.4 KiB
Go

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