monorepo/cloud/maplepress-backend/pkg/security/jwt/jwt.go

110 lines
3.6 KiB
Go

package jwt
import (
"fmt"
"log"
"time"
"github.com/golang-jwt/jwt/v5"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/validator"
)
// Provider provides interface for JWT token generation and validation
type Provider interface {
GenerateToken(sessionID string, duration time.Duration) (string, time.Time, error)
GenerateTokenPair(sessionID string, accessDuration time.Duration, refreshDuration time.Duration) (accessToken string, accessExpiry time.Time, refreshToken string, refreshExpiry time.Time, err error)
ValidateToken(tokenString string) (sessionID string, err error)
}
type provider struct {
secret []byte
}
// NewProvider creates a new JWT provider with security validation
func NewProvider(cfg *config.Config) Provider {
// Validate JWT secret security before creating provider
v := validator.NewCredentialValidator()
if err := v.ValidateJWTSecret(cfg.App.JWTSecret, cfg.App.Environment); err != nil {
// Log detailed error with remediation steps
log.Printf("[SECURITY ERROR] %s", err.Error())
// In production, this is a fatal error that should prevent startup
if cfg.App.Environment == "production" {
panic(fmt.Sprintf("SECURITY: Invalid JWT secret in production environment: %s", err.Error()))
}
// In development, log warning but allow to continue
log.Printf("[WARNING] Continuing with weak JWT secret in %s environment. This is NOT safe for production!", cfg.App.Environment)
}
return &provider{
secret: []byte(cfg.App.JWTSecret),
}
}
// GenerateToken generates a single JWT token
func (p *provider) GenerateToken(sessionID string, duration time.Duration) (string, time.Time, error) {
expiresAt := time.Now().Add(duration)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"session_id": sessionID,
"exp": expiresAt.Unix(),
})
tokenString, err := token.SignedString(p.secret)
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to sign token: %w", err)
}
return tokenString, expiresAt, nil
}
// GenerateTokenPair generates both access token and refresh token
func (p *provider) GenerateTokenPair(sessionID string, accessDuration time.Duration, refreshDuration time.Duration) (string, time.Time, string, time.Time, error) {
// Generate access token
accessToken, accessExpiry, err := p.GenerateToken(sessionID, accessDuration)
if err != nil {
return "", time.Time{}, "", time.Time{}, fmt.Errorf("failed to generate access token: %w", err)
}
// Generate refresh token
refreshToken, refreshExpiry, err := p.GenerateToken(sessionID, refreshDuration)
if err != nil {
return "", time.Time{}, "", time.Time{}, fmt.Errorf("failed to generate refresh token: %w", err)
}
return accessToken, accessExpiry, refreshToken, refreshExpiry, nil
}
// ValidateToken validates a JWT token and returns the session ID
func (p *provider) ValidateToken(tokenString string) (string, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verify the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return p.secret, nil
})
if err != nil {
return "", fmt.Errorf("failed to parse token: %w", err)
}
if !token.Valid {
return "", fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", fmt.Errorf("invalid token claims")
}
sessionID, ok := claims["session_id"].(string)
if !ok {
return "", fmt.Errorf("session_id not found in token")
}
return sessionID, nil
}