Skip to main content

JWT token authentication

JSON Web Tokens provide secure, stateless authentication for web applications and APIs with automatic refresh token rotation for enhanced security.

15 min
Access token
7 days
Refresh token
RS256
Algorithm
Rotation
Auto refresh

Authentication flow

  1. Login: User provides email/password
  2. Verification: Server validates credentials
  3. Token generation: Server creates access and refresh tokens
  4. Client storage: Tokens stored securely (httpOnly cookies recommended)
  5. API requests: Access token sent in Authorization header
  6. Token refresh: Before expiry, use refresh token for new access token
  7. Rotation: Old refresh token invalidated, new one issued

Implementation examples

1. User login

// POST /api/v1/public/login
const response = await fetch('https://aim.example.com/api/v1/public/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'user@example.com',
    password: 'SecurePassword123!'
  })
});

const data = await response.json();
// Response
{
  "user": {
    "id": "user_123",
    "email": "user@example.com",
    "role": "member",
    "organization_id": "org_456"
  },
  "tokens": {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 900,  // 15 minutes
    "refresh_expires_in": 604800  // 7 days
  }
}

2. Using the access token

// Include token in Authorization header
const agents = await fetch('https://aim.example.com/api/v1/agents', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});

// Token payload structure
{
  "sub": "user_123",              // User ID
  "email": "user@example.com",
  "role": "member",
  "org": "org_456",
  "permissions": [
    "agent:read",
    "agent:create",
    "agent:verify"
  ],
  "iat": 1704067200,              // Issued at
  "exp": 1704068100,              // Expires at (15 min)
  "iss": "https://aim.example.com",
  "aud": "aim-api"
}

3. Refresh token rotation

// POST /api/v1/auth/refresh
const response = await fetch('https://aim.example.com/api/v1/auth/refresh', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${refreshToken}`
  }
});

const data = await response.json();
// Response with NEW tokens (rotation)
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",  // New
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", // New (rotated)
  "expires_in": 900,
  "refresh_expires_in": 604800
}

// Old refresh token is now invalid!

4. Auto-refresh implementation

class AuthService {
  constructor() {
    this.accessToken = null;
    this.refreshToken = null;
    this.refreshTimer = null;
  }

  async login(email, password) {
    const response = await fetch('/api/v1/public/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });

    const data = await response.json();
    this.setTokens(data.tokens);
    return data;
  }

  setTokens(tokens) {
    this.accessToken = tokens.access_token;
    this.refreshToken = tokens.refresh_token;

    // Schedule refresh 1 minute before expiry
    const refreshIn = (tokens.expires_in - 60) * 1000;
    this.scheduleRefresh(refreshIn);
  }

  scheduleRefresh(delay) {
    // Clear existing timer
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
    }

    this.refreshTimer = setTimeout(() => {
      this.refreshAccessToken();
    }, delay);
  }

  async refreshAccessToken() {
    try {
      const response = await fetch('/api/v1/auth/refresh', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.refreshToken}`
        }
      });

      if (response.ok) {
        const tokens = await response.json();
        this.setTokens(tokens);
      } else {
        // Refresh failed - redirect to login
        this.logout();
      }
    } catch (error) {
      console.error('Token refresh failed:', error);
      this.logout();
    }
  }

  async makeAuthenticatedRequest(url, options = {}) {
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${this.accessToken}`
      }
    });

    // Handle 401 - try refresh once
    if (response.status === 401) {
      await this.refreshAccessToken();
      // Retry request with new token
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${this.accessToken}`
        }
      });
    }

    return response;
  }

  logout() {
    this.accessToken = null;
    this.refreshToken = null;
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
    }
    // Redirect to login page
    window.location.href = '/login';
  }
}

Security best practices

Token storage

  • httpOnly cookies: Prevents XSS access
  • Secure flag: HTTPS only transmission
  • SameSite: CSRF protection
  • localStorage: Vulnerable to XSS

Token handling

  • • Short access token lifetime (15 min)
  • • Automatic refresh before expiry
  • • One-time use refresh tokens
  • • Invalidate tokens on logout
  • • Monitor for token reuse attacks
  • • Implement token blacklisting

Server-side token validation

// Go example - Fiber middleware
func JWTMiddleware(jwtService *auth.JWTService) fiber.Handler {
  return func(c fiber.Ctx) error {
    // Extract token from Authorization header
    authHeader := c.Get("Authorization")
    if authHeader == "" {
      return c.Status(401).JSON(fiber.Map{
        "error": "Missing authorization header"
      })
    }

    // Remove "Bearer " prefix
    token := strings.Replace(authHeader, "Bearer ", "", 1)

    // Validate token
    claims, err := jwtService.ValidateToken(token)
    if err != nil {
      return c.Status(401).JSON(fiber.Map{
        "error": "Invalid or expired token"
      })
    }

    // Check token expiration
    if time.Now().Unix() > claims.ExpiresAt {
      return c.Status(401).JSON(fiber.Map{
        "error": "Token expired"
      })
    }

    // Store user info in context
    c.Locals("user_id", claims.Subject)
    c.Locals("role", claims.Role)
    c.Locals("permissions", claims.Permissions)

    return c.Next()
  }
}

JWT claims reference

ClaimPurposeExample
subSubject (User ID)"user_123"
emailUser email"user@example.com"
roleUser role"admin"
iatIssued at (Unix timestamp)1704067200
expExpiration (Unix timestamp)1704068100
issIssuer"https://aim.example.com"
audAudience"aim-api"
jtiJWT ID (unique)"jwt_abc123"