Перейти к содержанию

JWT Architecture - Архитектура аутентификации токенов

Architectural Overview

Saga использует dual token system с разделением админских и пользовательских токенов для четкого разграничения прав доступа.

Ключевые принципы JWT системы:

  • Dual Token System: AdminClaims (admin endpoints) vs JWTClaims (user endpoints)
  • Stateless Authentication: JWT токены самодостаточны, не требуют session storage
  • HS256 Algorithm: HMAC with SHA-256 для подписи токенов
  • Clean Architecture: Сервисы изолированы, легко тестируемы
  • UnifiedConfig Integration: Все настройки через централизованную конфигурацию

Components Architecture

Backend Components

JWT System Architecture:

┌─────────────────────────────────────────────────────────┐
│              JWT Generation Layer                        │
│  ┌───────────────────────────────────────────────────┐  │
│  │  jwt_mvp.go                                       │  │
│  │  - GenerateJWTToken(email, isAdmin)               │  │
│  │  - CreateAdminToken(email, permissions)           │  │
│  │  - CreateUserToken(userID, email, wallet)         │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│            JWT Validation Layer                          │
│  ┌───────────────────────────────────────────────────┐  │
│  │  jwt_validator.go                                 │  │
│  │  - ValidateJWTToken(tokenString)                  │  │
│  │  - ExtractClaims(token)                           │  │
│  │  - VerifySignature(token, secret)                 │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│              Middleware Layer                            │
│  ┌───────────────────────────────────────────────────┐  │
│  │  admin_auth_middleware.go                         │  │
│  │  - RequireAdminAuth() - checks AdminClaims        │  │
│  ├───────────────────────────────────────────────────┤  │
│  │  auth_middleware.go                               │  │
│  │  - RequireAuth() - checks JWTClaims               │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│                Protected Endpoints                       │
│  ┌──────────────────────┐  ┌──────────────────────┐    │
│  │  /api/admin/*        │  │  /api/user/*         │    │
│  │  AdminClaims required│  │  JWTClaims required  │    │
│  └──────────────────────┘  └──────────────────────┘    │
└─────────────────────────────────────────────────────────┘

ile Structure

Core JWT Files:

  • backend/auth/service/jwt_mvp.go - Token generation service (311 lines)
  • backend/shared/http/jwt_validator.go - Token validation logic (188 lines)
  • backend/auth/service/token_manager.go - Claims management (245 lines)
  • backend/cmd/jwt-helper/main.go - CLI utility for token generation (567 lines)

Middleware Files:

  • backend/shared/middleware/admin_auth_middleware.go - Admin authentication (156 lines)
  • backend/shared/middleware/auth_middleware.go - User authentication (143 lines)

Configuration:

  • config/auth.yaml - JWT settings (secret, expiration, issuer)
  • .env - JWT_SECRET environment variable

Token Types and Claims

1. AdminClaims (Admin Tokens)

Structure:

type AdminClaims struct {
    jwt.StandardClaims
    Email       string   `json:"email"`
    Permissions []string `json:"permissions,omitempty"`
    Roles       []string `json:"roles,omitempty"`
}

JWT Payload Example:

{
  "email": "admin@saga-test.com",
  "permissions": [
    "approve_withdrawals",
    "view_all_users",
    "manage_investments"
  ],
  "roles": ["admin", "super_admin"],
  "exp": 1696531200,
  "iat": 1696444800,
  "iss": "saga-defi-platform"
}

Usage:

  • Protected endpoints: /api/admin/*
  • Withdrawal approvals: POST /api/admin/withdrawals/:id/approve
  • User management: GET /api/admin/users
  • Investment oversight: GET /api/admin/investments

Generation:

func (s *JWTService) CreateAdminToken(email string) (string, error) {
    claims := AdminClaims{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
            IssuedAt:  time.Now().Unix(),
            Issuer:    cfg.GetJWTIssuer(),
        },
        Email: email,
        Permissions: []string{"approve_withdrawals", "view_all_users"},
        Roles: []string{"admin"},
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(cfg.GetJWTSecret()))
}

2. JWTClaims (User Tokens)

Structure:

type JWTClaims struct {
    jwt.StandardClaims
    UserID      int64  `json:"user_id"`
    Email       string `json:"email"`
    WalletAddr  string `json:"wallet_address,omitempty"`
}

JWT Payload Example:

{
  "user_id": 123,
  "email": "user@saga-test.com",
  "wallet_address": "0x742d35Cc9Cf3C4C3a3F5d7B5f",
  "exp": 1696531200,
  "iat": 1696444800,
  "iss": "saga-defi-platform"
}

Usage:

  • Protected endpoints: /api/user/*
  • Balance queries: GET /api/user/balance
  • Investments: POST /api/user/investments
  • Withdrawals: POST /api/user/withdrawals

Generation:

func (s *JWTService) CreateUserToken(userID int64, email, walletAddr string) (string, error) {
    claims := JWTClaims{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
            IssuedAt:  time.Now().Unix(),
            Issuer:    cfg.GetJWTIssuer(),
        },
        UserID:     userID,
        Email:      email,
        WalletAddr: walletAddr,
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(cfg.GetJWTSecret()))
}

Security Architecture

1. Token Signing (HS256)

Algorithm: HMAC with SHA-256

Why HS256:

  • ✅ Symmetric encryption (fast, efficient)
  • ✅ Sufficient security for internal APIs
  • ✅ No public/private key management
  • ✅ Widely supported and battle-tested

Signature Process:

1. Create header:
   {
     "alg": "HS256",
     "typ": "JWT"
   }

2. Create payload (claims):
   {
     "email": "admin@saga-test.com",
     "exp": 1696531200,
     ...
   }

3. Encode header and payload:
   base64url(header).base64url(payload)

4. Sign with secret:
   HMACSHA256(
     base64url(header) + "." + base64url(payload),
     JWT_SECRET
   )

5. Final token:
   header.payload.signature

Secret Management:

# config/auth.yaml
jwt:
  secret: "${JWT_SECRET}"  # From environment
  expiration_hours: 24
  issuer: "saga-defi-platform"

Security Requirements:

  • JWT_SECRET минимум 32 символа
  • ✅ Хранится в .env файле (not in code)
  • ✅ Different secrets для dev/staging/production
  • ✅ Regular rotation (recommended: quarterly)

2. Token Expiration

Lifetime: 24 hours (configurable via UnifiedConfig)

Expiration Flow:

Token Creation (t=0):
  iat: 1696444800 (issued at)
  exp: 1696531200 (expires at = iat + 24h)
Valid Period (0-24h):
  ✅ Token accepted
  ✅ User can make requests
Expiration (t=24h):
  ❌ Token rejected
  ❌ User must re-authenticate
Grace Period (NONE):
  ❌ No refresh tokens
  ❌ Hard cutoff at expiration

Implementation:

func ValidateExpiration(claims jwt.StandardClaims) error {
    now := time.Now().Unix()

    if claims.ExpiresAt < now {
        return errors.New("token expired")
    }

    // Optional: Reject tokens issued in future
    if claims.IssuedAt > now + 60 {
        return errors.New("token issued in future")
    }

    return nil
}

3. Token Validation Process

Comprehensive Validation:

func (v *JWTValidator) ValidateToken(tokenString string) (*jwt.Token, error) {
    // 1. Parse token
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // 2. Verify signing method
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }

        // 3. Return secret for signature verification
        return []byte(cfg.GetJWTSecret()), nil
    })

    if err != nil {
        return nil, fmt.Errorf("token parse failed: %w", err)
    }

    // 4. Verify token is valid
    if !token.Valid {
        return nil, errors.New("invalid token")
    }

    // 5. Extract and validate claims
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, errors.New("invalid claims format")
    }

    // 6. Verify expiration
    if exp, ok := claims["exp"].(float64); ok {
        if time.Now().Unix() > int64(exp) {
            return nil, errors.New("token expired")
        }
    }

    // 7. Verify issuer
    if iss, ok := claims["iss"].(string); ok {
        if iss != cfg.GetJWTIssuer() {
            return nil, errors.New("invalid issuer")
        }
    }

    return token, nil
}

Middleware Implementation

Admin Authentication Middleware

File: backend/shared/middleware/admin_auth_middleware.go

func RequireAdminAuth(cfg *config.UnifiedConfig) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 1. Extract token from Authorization header
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                http_helpers.SendError(w, http.StatusUnauthorized, "Missing authorization header")
                return
            }

            tokenString := strings.TrimPrefix(authHeader, "Bearer ")

            // 2. Validate token
            validator := jwt_validator.NewJWTValidator(cfg)
            token, err := validator.ValidateToken(tokenString)
            if err != nil {
                http_helpers.SendError(w, http.StatusUnauthorized, "Invalid token: "+err.Error())
                return
            }

            // 3. Extract AdminClaims
            claims, ok := token.Claims.(jwt.MapClaims)
            if !ok {
                http_helpers.SendError(w, http.StatusUnauthorized, "Invalid claims")
                return
            }

            adminEmail, ok := claims["email"].(string)
            if !ok {
                http_helpers.SendError(w, http.StatusUnauthorized, "Missing email claim")
                return
            }

            // 4. Verify admin status (from UnifiedConfig)
            if !cfg.IsAdmin(adminEmail) {
                http_helpers.SendError(w, http.StatusForbidden, "Not an admin")
                return
            }

            // 5. Add admin email to request context
            ctx := context.WithValue(r.Context(), "admin_email", adminEmail)

            // 6. Continue to handler
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Protected Routes:

// In routing setup
adminRouter := chi.NewRouter()
adminRouter.Use(middleware.RequireAdminAuth(cfg))

// All routes below require AdminClaims
adminRouter.Get("/users", adminHandler.ListUsers)
adminRouter.Post("/withdrawals/{id}/approve", adminHandler.ApproveWithdrawal)
adminRouter.Get("/investments", adminHandler.ListInvestments)

User Authentication Middleware

File: backend/shared/middleware/auth_middleware.go

func RequireAuth(cfg *config.UnifiedConfig) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 1. Extract token
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                http_helpers.SendError(w, http.StatusUnauthorized, "Missing authorization header")
                return
            }

            tokenString := strings.TrimPrefix(authHeader, "Bearer ")

            // 2. Validate token
            validator := jwt_validator.NewJWTValidator(cfg)
            token, err := validator.ValidateToken(tokenString)
            if err != nil {
                http_helpers.SendError(w, http.StatusUnauthorized, "Invalid token: "+err.Error())
                return
            }

            // 3. Extract JWTClaims
            claims, ok := token.Claims.(jwt.MapClaims)
            if !ok {
                http_helpers.SendError(w, http.StatusUnauthorized, "Invalid claims")
                return
            }

            // 4. Extract user data
            userID, ok := claims["user_id"].(float64)
            if !ok {
                http_helpers.SendError(w, http.StatusUnauthorized, "Missing user_id claim")
                return
            }

            email, _ := claims["email"].(string)
            walletAddr, _ := claims["wallet_address"].(string)

            // 5. Add user data to context
            ctx := context.WithValue(r.Context(), "user_id", int64(userID))
            ctx = context.WithValue(ctx, "email", email)
            ctx = context.WithValue(ctx, "wallet_address", walletAddr)

            // 6. Continue to handler
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Protected Routes:

// In routing setup
userRouter := chi.NewRouter()
userRouter.Use(middleware.RequireAuth(cfg))

// All routes below require JWTClaims
userRouter.Get("/balance", userHandler.GetBalance)
userRouter.Post("/investments", userHandler.CreateInvestment)
userRouter.Post("/withdrawals", userHandler.CreateWithdrawal)

Testing JWT System

1. CLI Utility (jwt-helper)

File: backend/cmd/jwt-helper/main.go

Generate Admin Token:

# Using Makefile (recommended)
make jwt-admin EMAIL=admin@saga-test.com

# Direct CLI (after build)
cd backend && ./bin/jwt-helper admin admin@saga-test.com

Generate User Token:

# Using Makefile (recommended)
make jwt-user EMAIL=user@saga-test.com

# Direct CLI (requires database)
cd backend && ./bin/jwt-helper user user@saga-test.com

Validate Token:

# Using Makefile
make jwt-validate TOKEN=<jwt_token>

# Direct CLI
cd backend && ./bin/jwt-helper validate <jwt_token>

Introspect Token:

# Decode and display token contents
cd backend && ./bin/jwt-helper introspect <jwt_token>

# Output:
{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "email": "admin@saga-test.com",
    "permissions": ["approve_withdrawals"],
    "exp": 1696531200,
    ...
  },
  "signature": "valid"
}

2. Integration Tests

Test Admin Authentication:

func TestAdminAuthMiddleware(t *testing.T) {
    cfg := config.LoadTestConfig()

    // Create admin token
    jwtService := jwt_service.NewJWTService(cfg)
    adminToken, err := jwtService.CreateAdminToken("admin@saga-test.com")
    require.NoError(t, err)

    // Test protected endpoint
    req := httptest.NewRequest("GET", "/api/admin/users", nil)
    req.Header.Set("Authorization", "Bearer "+adminToken)

    rr := httptest.NewRecorder()
    handler := middleware.RequireAdminAuth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        adminEmail := r.Context().Value("admin_email").(string)
        assert.Equal(t, "admin@saga-test.com", adminEmail)
        w.WriteHeader(http.StatusOK)
    }))

    handler.ServeHTTP(rr, req)
    assert.Equal(t, http.StatusOK, rr.Code)
}

Test User Authentication:

func TestUserAuthMiddleware(t *testing.T) {
    cfg := config.LoadTestConfig()

    // Create user token
    jwtService := jwt_service.NewJWTService(cfg)
    userToken, err := jwtService.CreateUserToken(123, "user@saga-test.com", "0x742d35...")
    require.NoError(t, err)

    // Test protected endpoint
    req := httptest.NewRequest("GET", "/api/user/balance", nil)
    req.Header.Set("Authorization", "Bearer "+userToken)

    rr := httptest.NewRecorder()
    handler := middleware.RequireAuth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := r.Context().Value("user_id").(int64)
        assert.Equal(t, int64(123), userID)
        w.WriteHeader(http.StatusOK)
    }))

    handler.ServeHTTP(rr, req)
    assert.Equal(t, http.StatusOK, rr.Code)
}

Test Token Expiration:

func TestTokenExpiration(t *testing.T) {
    cfg := config.LoadTestConfig()
    jwtService := jwt_service.NewJWTService(cfg)

    // Create token with short expiration
    claims := jwt_service.AdminClaims{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(1 * time.Second).Unix(),
            IssuedAt:  time.Now().Unix(),
        },
        Email: "admin@saga-test.com",
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(cfg.GetJWTSecret()))
    require.NoError(t, err)

    // Validate immediately (should pass)
    validator := jwt_validator.NewJWTValidator(cfg)
    _, err = validator.ValidateToken(tokenString)
    assert.NoError(t, err)

    // Wait for expiration
    time.Sleep(2 * time.Second)

    // Validate after expiration (should fail)
    _, err = validator.ValidateToken(tokenString)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "expired")
}

3. E2E Tests (Playwright)

Admin E2E Authentication:

import { test, expect } from '@playwright/test';
import { generateAdminJWT } from '../utils/admin-jwt-helper';

test('Admin can access protected endpoints with JWT', async ({ page }) => {
  // Generate admin token
  const adminToken = await generateAdminJWT('admin@saga-test.com');

  // Set Authorization header
  await page.setExtraHTTPHeaders({
    'Authorization': `Bearer ${adminToken}`
  });

  // Navigate to admin page
  await page.goto('http://admin.saga.local:8080/');

  // Verify admin access
  await expect(page.locator('text=Admin Dashboard')).toBeVisible();

  // Test protected API call
  const response = await page.request.get('http://localhost:8080/api/admin/users', {
    headers: {
      'Authorization': `Bearer ${adminToken}`
    }
  });

  expect(response.status()).toBe(200);
});

User E2E Authentication:

test('User can access user endpoints with JWT', async ({ page }) => {
  // Create user and get token
  const userToken = await createTestUserAndGetToken('user@saga-test.com');

  // Set Authorization header
  await page.setExtraHTTPHeaders({
    'Authorization': `Bearer ${userToken}`
  });

  // Navigate to user page
  await page.goto('http://app.saga.local:8080/');

  // Verify user access
  await expect(page.locator('text=My Balance')).toBeVisible();

  // Test protected API call
  const response = await page.request.get('http://localhost:8080/api/user/balance', {
    headers: {
      'Authorization': `Bearer ${userToken}`
    }
  });

  expect(response.status()).toBe(200);
});

Configuration Management

UnifiedConfig Integration

JWT Configuration Structure:

# config/auth.yaml
jwt:
  secret: "${JWT_SECRET}"
  expiration_hours: 24
  issuer: "saga-defi-platform"
  algorithm: "HS256"

admins:
  - email: "admin@saga-test.com"
    permissions:
      - "approve_withdrawals"
      - "view_all_users"
      - "manage_investments"
  - email: "super@saga-test.com"
    permissions:
      - "*"  # All permissions

Go Configuration Access:

type UnifiedConfig interface {
    // JWT settings
    GetJWTSecret() string
    GetJWTExpirationHours() int
    GetJWTIssuer() string

    // Admin management
    IsAdmin(email string) bool
    GetAdminPermissions(email string) []string
}

// Usage
func (s *JWTService) NewJWTService(cfg *config.UnifiedConfig) *JWTService {
    return &JWTService{
        secret:     cfg.GetJWTSecret(),
        expiration: time.Duration(cfg.GetJWTExpirationHours()) * time.Hour,
        issuer:     cfg.GetJWTIssuer(),
    }
}

nvironment Variables

Required Variables:

# .env file
JWT_SECRET=your-super-secret-minimum-32-characters-long-key

Development Setup:

# Generate random secret (recommended)
openssl rand -base64 32 > .jwt-secret
export JWT_SECRET=$(cat .jwt-secret)

# Or use test secret (dev only!)
export JWT_SECRET="test-jwt-secret-for-smoke-tests-minimum-32-chars-required"

Production Requirements:

  • ✅ Strong random secret (32+ bytes)
  • ✅ Stored in secure vault (AWS Secrets Manager, HashiCorp Vault)
  • ✅ Regular rotation schedule
  • ✅ Different secrets per environment

rontend Integration

React Context (Web3AuthContext)

Token Storage:

// frontend/user-app/src/lib/web3-auth-context.tsx
export const Web3AuthContext = createContext<Web3AuthContextType | undefined>(undefined);

export const Web3AuthProvider: React.FC<Props> = ({ children }) => {
  const [authToken, setAuthToken] = useState<string | null>(() => {
    // Load from localStorage on init
    return localStorage.getItem('authToken');
  });

  const login = async (walletAddress: string, signature: string) => {
    // Authenticate with backend
    const response = await fetch('/api/auth/wallet/authenticate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ address: walletAddress, signature })
    });

    const { token } = await response.json();

    // Store token
    localStorage.setItem('authToken', token);
    setAuthToken(token);
  };

  const logout = () => {
    localStorage.removeItem('authToken');
    setAuthToken(null);
  };

  return (
    <Web3AuthContext.Provider value={{ authToken, login, logout }}>
      {children}
    </Web3AuthContext.Provider>
  );
};

API Requests with JWT

Authenticated Fetch:

// frontend/user-app/src/lib/services/auth-service.ts
export const authenticatedFetch = async (url: string, options: RequestInit = {}) => {
  const authToken = localStorage.getItem('authToken');

  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': authToken ? `Bearer ${authToken}` : '',
      'Content-Type': 'application/json',
    },
  });

  // Handle 401 (expired token)
  if (response.status === 401) {
    localStorage.removeItem('authToken');
    window.location.href = '/login';
    throw new Error('Authentication expired');
  }

  return response;
};

// Usage
const getUserBalance = async () => {
  const response = await authenticatedFetch('/api/user/balance');
  return response.json();
};

Monitoring and Logging

JWT Event Logging

Structured Logging:

import "github.com/aiseeq/saga/backend/shared/logging"

func (s *JWTService) CreateUserToken(userID int64, email string) (string, error) {
    logger := logging.GetGlobalCanonicalLogger()

    token, err := s.generateToken(userID, email)
    if err != nil {
        logger.ErrorStructured("JWT generation failed",
            "user_id", userID,
            "email", email,
            "error", err.Error(),
        )
        return "", err
    }

    logger.InfoStructured("JWT token created",
        "user_id", userID,
        "email", email,
        "expires_at", time.Now().Add(s.expiration).Unix(),
    )

    return token, nil
}

Validation Logging:

func (v *JWTValidator) ValidateToken(tokenString string) error {
    logger := logging.GetGlobalCanonicalLogger()

    token, err := jwt.Parse(tokenString, v.keyFunc)
    if err != nil {
        logger.WarnStructured("JWT validation failed",
            "error", err.Error(),
            "token_prefix", tokenString[:20], // First 20 chars only
        )
        return err
    }

    claims := token.Claims.(jwt.MapClaims)
    logger.InfoStructured("JWT validated successfully",
        "email", claims["email"],
        "expires_at", claims["exp"],
    )

    return nil
}

Metrics Collection

Key Metrics:

  • Total tokens generated (counter)
  • Token validation success/failure rate (gauge)
  • Token expiration events (counter)
  • Average token lifetime (histogram)

Prometheus Example:

var (
    tokensGenerated = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "jwt_tokens_generated_total",
            Help: "Total number of JWT tokens generated",
        },
        []string{"type"}, // admin or user
    )

    tokenValidations = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "jwt_validations_total",
            Help: "Total number of JWT validations",
        },
        []string{"result"}, // success or failure
    )
)

func (s *JWTService) CreateAdminToken(email string) (string, error) {
    token, err := s.generateToken(email, true)
    if err != nil {
        tokensGenerated.WithLabelValues("admin").Inc()
    }
    return token, err
}

Architecture:

Development:

Configuration:

Security Best Practices

Token Security Checklist:

  • ✅ JWT_SECRET минимум 32 символа
  • ✅ Secret хранится в environment variables (not code)
  • ✅ Different secrets для dev/staging/production
  • ✅ Token expiration установлен (24 hours)
  • ✅ HTTPS enforced для production
  • ✅ Tokens not logged (только prefixes)
  • ✅ Signature algorithm verified (HS256 only)
  • ✅ Claims validated (exp, iss, email)
  • ✅ Admin status checked против UnifiedConfig
  • ✅ Monitoring и alerting настроены

Common Pitfalls:

Hardcoding JWT_SECRET → Use environment variables ❌ No expiration → Always set exp claim ❌ Logging full tokens → Only log token prefixes ❌ Accepting any algorithm → Verify alg header ❌ No admin validation → Check against UnifiedConfig.IsAdmin() ❌ Storing tokens in cookies without HttpOnly → Use localStorage or HttpOnly cookies




📋 Метаданные

Версия: 2.4.82

Обновлено: 2025-10-21

Статус: Published