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 }