package auth import ( "context" "crypto/rand" "encoding/hex" "fmt" "strings" "time" "github.com/bigtux/ophion/internal/security" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" "github.com/redis/go-redis/v9" ) // ═══════════════════════════════════════════════════════════ // 🔐 AUTH CONFIG // ═══════════════════════════════════════════════════════════ type AuthConfig struct { JWTSecret []byte JWTExpiration time.Duration RefreshExpiration time.Duration Issuer string } type AuthService struct { config AuthConfig redis *redis.Client rateLimiter *security.RateLimiter loginTracker *security.LoginAttemptTracker apiKeyStore APIKeyStore } // APIKeyStore interface para storage de API keys type APIKeyStore interface { ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error) UpdateLastUsed(ctx context.Context, keyID string) error } type APIKeyInfo struct { ID string OrgID string Scopes []string Name string } // NewAuthService cria serviço de autenticação func NewAuthService(config AuthConfig, redis *redis.Client, apiKeyStore APIKeyStore) *AuthService { return &AuthService{ config: config, redis: redis, rateLimiter: security.NewRateLimiter(security.AuthRateLimit, time.Minute), loginTracker: security.NewLoginAttemptTracker(), apiKeyStore: apiKeyStore, } } // ═══════════════════════════════════════════════════════════ // 🎫 JWT TOKENS // ═══════════════════════════════════════════════════════════ type TokenClaims struct { jwt.RegisteredClaims UserID string `json:"uid"` OrgID string `json:"oid"` Email string `json:"email"` Role string `json:"role"` Scopes []string `json:"scopes,omitempty"` TokenID string `json:"jti"` } // GenerateTokenPair gera access + refresh tokens func (s *AuthService) GenerateTokenPair(userID, orgID, email, role string) (accessToken, refreshToken string, err error) { now := time.Now() tokenID := generateTokenID() // Access Token (curta duração) accessClaims := TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ Issuer: s.config.Issuer, Subject: userID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(s.config.JWTExpiration)), ID: tokenID, }, UserID: userID, OrgID: orgID, Email: email, Role: role, TokenID: tokenID, } accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.JWTSecret) if err != nil { return "", "", fmt.Errorf("failed to sign access token: %w", err) } // Refresh Token (longa duração) refreshID := generateTokenID() refreshClaims := jwt.RegisteredClaims{ Issuer: s.config.Issuer, Subject: userID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(s.config.RefreshExpiration)), ID: refreshID, } refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.config.JWTSecret) if err != nil { return "", "", fmt.Errorf("failed to sign refresh token: %w", err) } // Armazenar refresh token no Redis (permite revogação) if s.redis != nil { ctx := context.Background() key := fmt.Sprintf("refresh_token:%s", refreshID) s.redis.Set(ctx, key, userID, s.config.RefreshExpiration) } return accessToken, refreshToken, nil } // ValidateAccessToken valida access token func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return s.config.JWTSecret, nil }) if err != nil { return nil, fmt.Errorf("invalid token: %w", err) } claims, ok := token.Claims.(*TokenClaims) if !ok || !token.Valid { return nil, fmt.Errorf("invalid token claims") } // Verificar se token foi revogado if s.redis != nil { ctx := context.Background() revoked, _ := s.redis.Get(ctx, fmt.Sprintf("revoked_token:%s", claims.TokenID)).Result() if revoked != "" { return nil, fmt.Errorf("token revoked") } } return claims, nil } // RevokeToken revoga um token func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error { if s.redis == nil { return nil } ctx := context.Background() return s.redis.Set(ctx, fmt.Sprintf("revoked_token:%s", tokenID), "1", expiration).Err() } // ═══════════════════════════════════════════════════════════ // 🛡️ MIDDLEWARE // ═══════════════════════════════════════════════════════════ // AuthMiddleware middleware de autenticação func (s *AuthService) AuthMiddleware() fiber.Handler { return func(c *fiber.Ctx) error { // Obter IP real (considerando proxies) ip := c.IP() if forwarded := c.Get("X-Forwarded-For"); forwarded != "" { ip = strings.Split(forwarded, ",")[0] } // Rate limiting por IP if !s.rateLimiter.Allow(ip) { return c.Status(429).JSON(fiber.Map{ "error": "Too many requests", "message": "Rate limit exceeded. Try again later.", }) } authHeader := c.Get("Authorization") if authHeader == "" { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Missing authorization header", }) } token := strings.TrimPrefix(authHeader, "Bearer ") // API Key authentication if strings.HasPrefix(token, security.APIKeyPrefix) { return s.authenticateAPIKey(c, token) } // JWT authentication return s.authenticateJWT(c, token) } } // authenticateAPIKey valida API key func (s *AuthService) authenticateAPIKey(c *fiber.Ctx, apiKey string) error { // Validar formato if !security.ValidateAPIKeyFormat(apiKey) { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Invalid API key format", }) } // Hash da key para busca keyHash := security.HashAPIKey(apiKey) // Buscar no storage keyInfo, err := s.apiKeyStore.ValidateKey(c.Context(), keyHash) if err != nil || keyInfo == nil { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Invalid API key", }) } // Atualizar last_used (async) go s.apiKeyStore.UpdateLastUsed(context.Background(), keyInfo.ID) // Setar contexto c.Locals("auth_type", "api_key") c.Locals("org_id", keyInfo.OrgID) c.Locals("api_key_id", keyInfo.ID) c.Locals("scopes", keyInfo.Scopes) return c.Next() } // authenticateJWT valida JWT token func (s *AuthService) authenticateJWT(c *fiber.Ctx, token string) error { claims, err := s.ValidateAccessToken(token) if err != nil { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": err.Error(), }) } // Setar contexto c.Locals("auth_type", "jwt") c.Locals("user_id", claims.UserID) c.Locals("org_id", claims.OrgID) c.Locals("email", claims.Email) c.Locals("role", claims.Role) c.Locals("token_id", claims.TokenID) return c.Next() } // RequireScopes middleware que exige scopes específicos func RequireScopes(required ...string) fiber.Handler { return func(c *fiber.Ctx) error { scopes, ok := c.Locals("scopes").([]string) if !ok { // JWT tokens têm acesso total por padrão if c.Locals("auth_type") == "jwt" { return c.Next() } return c.Status(403).JSON(fiber.Map{ "error": "Forbidden", "message": "Missing required scopes", }) } scopeMap := make(map[string]bool) for _, s := range scopes { scopeMap[s] = true } for _, req := range required { if !scopeMap[req] && !scopeMap["*"] { return c.Status(403).JSON(fiber.Map{ "error": "Forbidden", "message": fmt.Sprintf("Missing required scope: %s", req), }) } } return c.Next() } } // RequireRole middleware que exige role específico func RequireRole(roles ...string) fiber.Handler { return func(c *fiber.Ctx) error { userRole, ok := c.Locals("role").(string) if !ok { return c.Status(403).JSON(fiber.Map{ "error": "Forbidden", "message": "Role not found", }) } for _, r := range roles { if userRole == r { return c.Next() } } // Admin tem acesso a tudo if userRole == "admin" { return c.Next() } return c.Status(403).JSON(fiber.Map{ "error": "Forbidden", "message": "Insufficient permissions", }) } } // ═══════════════════════════════════════════════════════════ // 🔐 LOGIN PROTECTION // ═══════════════════════════════════════════════════════════ // CheckLoginAllowed verifica se login é permitido (brute force protection) func (s *AuthService) CheckLoginAllowed(identifier string) (bool, time.Duration, error) { locked, remaining := s.loginTracker.IsLocked(identifier) return !locked, remaining, nil } // RecordLoginAttempt registra tentativa de login func (s *AuthService) RecordLoginAttempt(identifier string, success bool) (blocked bool, remaining int) { if success { s.loginTracker.RecordSuccess(identifier) return false, 0 } return s.loginTracker.RecordFailure(identifier) } // ═══════════════════════════════════════════════════════════ // 🔧 HELPERS // ═══════════════════════════════════════════════════════════ func generateTokenID() string { bytes := make([]byte, 16) rand.Read(bytes) return hex.EncodeToString(bytes) } // GetUserID obtém user ID do contexto func GetUserID(c *fiber.Ctx) string { if id, ok := c.Locals("user_id").(string); ok { return id } return "" } // GetOrgID obtém org ID do contexto func GetOrgID(c *fiber.Ctx) string { if id, ok := c.Locals("org_id").(string); ok { return id } return "" } // GetRole obtém role do contexto func GetRole(c *fiber.Ctx) string { if role, ok := c.Locals("role").(string); ok { return role } return "" }