110 lines
3.6 KiB
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
|
|
}
|