Token Management

Comprehensive guide to JWT tokens, refresh tokens, token rotation, and secure token management in Khadem applications.

JWT Tokens

Stateless authentication with JSON Web Tokens

Refresh Tokens

Long-lived tokens for session extension

Token Security

Best practices and security measures

Understanding JWT Tokens

JSON Web Tokens (JWT) are self-contained tokens that carry user information and claims. They consist of three parts: header, payload, and signature.
dart

import 'package:khadem/khadem.dart';

/// JWT Token Structure
///
/// A JWT consists of three Base64-encoded parts separated by dots:
/// header.payload.signature
///
/// Example JWT:
/// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
/// eyJ1c2VyX2lkIjoxLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJleHAiOjE3MDk4MjQyMDB9.
/// SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

class JWTTokenExample {
  static void demonstrateStructure() {
    // Header (algorithm and type)
    final header = {
      'alg': 'HS256',  // HMAC with SHA-256
      'typ': 'JWT',    // Token type
    };

    // Payload (claims)
    final payload = {
      'user_id': 1,
      'email': 'user@example.com',
      'name': 'John Doe',
      'role': 'user',
      'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000, // Issued at
      'exp': DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000, // Expires
    };

    // Signature
    // HMACSHA256(
    //   base64UrlEncode(header) + "." + base64UrlEncode(payload),
    //   secret_key
    // )

    print('JWT Header: $header');
    print('JWT Payload: $payload');
    print('JWT is signed with secret key and cannot be tampered with');
  }

  // Standard JWT claims
  static const standardClaims = {
    'iss': 'Issuer',           // Who issued the token
    'sub': 'Subject',          // Subject identifier (user ID)
    'aud': 'Audience',         // Intended recipient
    'exp': 'Expiration',       // Expiration time
    'nbf': 'Not Before',       // Not valid before this time
    'iat': 'Issued At',        // When token was issued
    'jti': 'JWT ID',           // Unique token identifier
  };
}

JWT Components:

  • Header: Algorithm and token type (e.g., {"alg": "HS256", "typ": "JWT"})
  • Payload: User data and claims (user ID, email, expiration, etc.)
  • Signature: Cryptographic signature for verification

Generating Access and Refresh Tokens

When a user logs in, Khadem generates both an access token (short-lived) and a refresh token (long-lived) for secure authentication.
dart

import 'package:khadem/khadem.dart';

class AuthController {
  /// Login and generate tokens
  static Future<void> login(Request req, Response res) async {
    final data = await req.validate({
      'email': 'required|email',
      'password': 'required|min:6',
    });

    // Authenticate user
    final auth = AuthManager(guard: 'api');
    final authResponse = await auth.attempt(data);

    // AuthResponse contains:
    // - accessToken: Short-lived JWT for API requests
    // - refreshToken: Long-lived token for getting new access tokens
    // - tokenType: 'Bearer'
    // - expiresIn: Access token lifetime in seconds
    // - user: User data

    res.sendJson({
      'success': true,
      'data': {
        'access_token': authResponse.accessToken,
        'refresh_token': authResponse.refreshToken,
        'token_type': authResponse.tokenType,
        'expires_in': authResponse.expiresIn, // e.g., 3600 (1 hour)
        'user': authResponse.user,
      }
    });
  }

  /// Example response:
  /// {
  ///   "success": true,
  ///   "data": {
  ///     "access_token": "eyJhbGci...", // Valid for 1 hour
  ///     "refresh_token": "eyJhbGci...", // Valid for 2 weeks
  ///     "token_type": "Bearer",
  ///     "expires_in": 3600,
  ///     "user": {
  ///       "id": 1,
  ///       "email": "user@example.com",
  ///       "name": "John Doe"
  ///     }
  ///   }
  /// }
}

// Token configuration
const authConfig = {
  'jwt': {
    'secret': env('JWT_SECRET'), // Strong secret key
    'algo': 'HS256',             // Algorithm
    'ttl': 3600,                 // Access token: 1 hour
    'refresh_ttl': 1209600,      // Refresh token: 2 weeks
  },
};

Access Token

  • Short-lived (15 min - 1 hour)
  • Used for API requests
  • Sent in Authorization header
  • Contains user claims

Refresh Token

  • Long-lived (1 week - 1 month)
  • Used to get new access tokens
  • Stored securely (httpOnly cookie)
  • Single-use or rotates on use

Refreshing Access Tokens

When an access token expires, use the refresh token to obtain a new access token without requiring the user to log in again.
dart

import 'package:khadem/khadem.dart';

class TokenController {
  /// Refresh access token endpoint
  static Future<void> refresh(Request req, Response res) async {
    final refreshToken = req.input('refresh_token');

    if (refreshToken == null || refreshToken.isEmpty) {
      return res.status(400).sendJson({
        'error': 'Refresh token required',
      });
    }

    try {
      final auth = AuthManager();
      final authResponse = await auth.refresh(refreshToken);

      // Returns new access token and optionally new refresh token
      res.sendJson({
        'success': true,
        'data': {
          'access_token': authResponse.accessToken,
          'refresh_token': authResponse.refreshToken, // New rotated refresh token
          'token_type': authResponse.tokenType,
          'expires_in': authResponse.expiresIn,
        }
      });
    } on AuthException catch (e) {
      // Refresh token invalid or expired
      res.status(401).sendJson({
        'error': 'Invalid refresh token',
        'message': e.message,
      });
    }
  }

  /// Check if access token is about to expire
  static Future<void> checkExpiration(Request req, Response res) async {
    final token = req.header('authorization')?.replaceFirst('Bearer ', '');

    if (token == null) {
      return res.status(401).sendJson({'error': 'Token required'});
    }

    // Decode token payload (without verification for expiration check)
    final payload = _decodePayload(token);
    final exp = payload['exp'] as int;
    final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
    final timeLeft = exp - now;

    res.sendJson({
      'expires_in': timeLeft,
      'expires_at': DateTime.fromMillisecondsSinceEpoch(exp * 1000).toIso8601String(),
      'should_refresh': timeLeft < 300, // Less than 5 minutes left
    });
  }

  static Map<String, dynamic> _decodePayload(String token) {
    // Decode JWT payload (implementation depends on your JWT library)
    final parts = token.split('.');
    if (parts.length != 3) throw AuthException('Invalid token format');

    final payload = base64Url.decode(base64Url.normalize(parts[1]));
    return jsonDecode(utf8.decode(payload));
  }
}
dart

// JavaScript/TypeScript client-side refresh implementation

class AuthClient {
  private accessToken: string | null = null;
  private refreshToken: string | null = null;
  private refreshPromise: Promise<string> | null = null;

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

    const data = await response.json();

    this.accessToken = data.data.access_token;
    this.refreshToken = data.data.refresh_token;

    // Store refresh token in httpOnly cookie (set by server)
    // Access token in memory only (most secure)

    return data;
  }

  async refreshAccessToken() {
    // Prevent multiple concurrent refresh requests
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = (async () => {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh_token: this.refreshToken }),
      });

      if (!response.ok) {
        // Refresh failed, redirect to login
        window.location.href = '/login';
        throw new Error('Refresh failed');
      }

      const data = await response.json();

      this.accessToken = data.data.access_token;
      this.refreshToken = data.data.refresh_token; // Rotated token

      return this.accessToken!;
    })();

    try {
      return await this.refreshPromise;
    } finally {
      this.refreshPromise = null;
    }
  }

  async fetch(url: string, options: RequestInit = {}) {
    // Add access token to request
    const headers = new Headers(options.headers);
    headers.set('Authorization', `Bearer ${this.accessToken}`);

    let response = await fetch(url, { ...options, headers });

    // If 401, try to refresh token and retry
    if (response.status === 401) {
      await this.refreshAccessToken();

      // Retry with new token
      headers.set('Authorization', `Bearer ${this.accessToken}`);
      response = await fetch(url, { ...options, headers });
    }

    return response;
  }
}

// Usage
const authClient = new AuthClient();

await authClient.login('user@example.com', 'password');

// Automatically handles token refresh
const response = await authClient.fetch('/api/profile');
const profile = await response.json();

Refresh Token Rotation

Token rotation enhances security by issuing a new refresh token each time it's used, invalidating the old one. This prevents token reuse attacks.
dart

import 'package:khadem/khadem.dart';

/// Token rotation for enhanced security
class TokenRotationService {
  /// Refresh endpoint with automatic rotation
  static Future<void> refresh(Request req, Response res) async {
    final refreshToken = req.input('refresh_token');

    if (refreshToken == null) {
      return res.status(400).sendJson({'error': 'Refresh token required'});
    }

    try {
      final auth = AuthManager();

      // This generates:
      // 1. New access token
      // 2. NEW refresh token (rotation)
      // 3. Invalidates old refresh token
      final authResponse = await auth.refresh(refreshToken);

      // Log rotation for security audit
      await _logTokenRotation(refreshToken, authResponse.refreshToken);

      res.sendJson({
        'success': true,
        'data': authResponse.toMap(),
      });
    } on AuthException catch (e) {
      // If someone tries to reuse an old refresh token
      await _handleTokenReuse(refreshToken, e);

      res.status(401).sendJson({
        'error': 'Invalid refresh token',
        'message': 'Token may have been compromised',
      });
    }
  }

  /// Detect and handle token reuse (possible theft)
  static Future<void> _handleTokenReuse(
    String token,
    AuthException error,
  ) async {
    // Check if this token was previously used
    final tokenRecord = await TokenBlacklist().query
      .where('token', '=', token)
      .first();

    if (tokenRecord != null) {
      // Token reuse detected! Possible theft
      final userId = tokenRecord.userId;

      // Security incident: Invalidate ALL tokens for this user
      final auth = AuthManager();
      await auth.logoutAll(userId);

      // Log security incident
      await SecurityLog().query.insert({
        'user_id': userId,
        'event': 'token_reuse_detected',
        'ip_address': RequestContext.request.ip,
        'user_agent': RequestContext.request.userAgent,
        'severity': 'high',
        'created_at': DateTime.now().toIso8601String(),
      });

      // Send alert to user
      await _sendSecurityAlert(userId);
    }
  }

  static Future<void> _logTokenRotation(
    String? oldToken,
    String? newToken,
  ) async {
    // Log for audit trail
    print('Token rotated: ${oldToken?.substring(0, 10)}... -> ${newToken?.substring(0, 10)}...');
  }

  static Future<void> _sendSecurityAlert(dynamic userId) async {
    // Send email/notification to user about suspicious activity
    print('Security alert sent to user $userId');
  }
}

🔒 Security Benefits:

  • Detects token theft - reused tokens trigger security alerts
  • Limits damage from compromised tokens
  • Automatic revocation of suspicious sessions
  • Improved audit trail

Verifying and Validating Tokens

Always verify tokens before trusting their contents. Check signature, expiration, and token blacklist status.
dart

import 'package:khadem/khadem.dart';

class TokenVerificationService {
  /// Verify access token
  static Future<bool> verifyToken(String token) async {
    try {
      final auth = AuthManager();

      // This checks:
      // 1. Token signature is valid
      // 2. Token hasn't expired
      // 3. Token isn't blacklisted
      final isValid = await auth.check(token);

      return isValid;
    } catch (e) {
      return false;
    }
  }

  /// Verify and get user
  static Future<Map<String, dynamic>?> verifyAndGetUser(String token) async {
    try {
      final auth = AuthManager();
      final user = await auth.user(token);

      return user.toAuthArray();
    } on AuthException catch (e) {
      print('Token verification failed: ${e.message}');
      return null;
    }
  }

  /// Manual token verification with detailed checks
  static Future<TokenVerificationResult> verifyDetailed(String token) async {
    final result = TokenVerificationResult();

    try {
      // 1. Check format
      if (!_isValidJwtFormat(token)) {
        result.valid = false;
        result.error = 'Invalid token format';
        return result;
      }

      // 2. Decode payload
      final payload = _decodePayload(token);

      // 3. Check expiration
      final exp = payload['exp'] as int;
      final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;

      if (now >= exp) {
        result.valid = false;
        result.error = 'Token expired';
        result.expiredAt = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
        return result;
      }

      // 4. Check signature
      final auth = AuthManager();
      final user = await auth.user(token);

      result.valid = true;
      result.payload = payload;
      result.user = user.toAuthArray();

      return result;
    } catch (e) {
      result.valid = false;
      result.error = e.toString();
      return result;
    }
  }

  static bool _isValidJwtFormat(String token) {
    final parts = token.split('.');
    return parts.length == 3;
  }

  static Map<String, dynamic> _decodePayload(String token) {
    final parts = token.split('.');
    final payload = base64Url.decode(base64Url.normalize(parts[1]));
    return jsonDecode(utf8.decode(payload));
  }
}

class TokenVerificationResult {
  bool valid = false;
  String? error;
  Map<String, dynamic>? payload;
  Map<String, dynamic>? user;
  DateTime? expiredAt;
}

Token Invalidation and Blacklisting

Implement token blacklisting to invalidate tokens before their natural expiration. Essential for logout and security incidents.
dart

import 'package:khadem/khadem.dart';

/// Token blacklist for invalidation
class TokenBlacklistService {
  /// Blacklist a token (logout)
  static Future<void> blacklistToken(String token) async {
    // Decode token to get expiration
    final payload = _decodePayload(token);
    final exp = payload['exp'] as int;
    final expiresAt = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
    final userId = payload['user_id'];

    // Store in blacklist
    await TokenBlacklist().query.insert({
      'token': token,
      'user_id': userId,
      'expires_at': expiresAt.toIso8601String(),
      'blacklisted_at': DateTime.now().toIso8601String(),
    });

    // Optionally store in Redis for faster lookup
    // await redis.set('blacklist:$token', '1', ex: expiresAt.difference(DateTime.now()).inSeconds);
  }

  /// Check if token is blacklisted
  static Future<bool> isBlacklisted(String token) async {
    // Check Redis first (faster)
    // final redisResult = await redis.get('blacklist:$token');
    // if (redisResult != null) return true;

    // Check database
    final record = await TokenBlacklist().query
      .where('token', '=', token)
      .first();

    return record != null;
  }

  /// Blacklist all tokens for a user
  static Future<void> blacklistAllUserTokens(dynamic userId) async {
    // This would require storing all active tokens
    // Or use a simpler approach: store user_id with timestamp
    await UserTokenInvalidation().query.insert({
      'user_id': userId,
      'invalidated_at': DateTime.now().toIso8601String(),
    });

    // During verification, check if token was issued before this timestamp
  }

  /// Cleanup expired blacklist entries
  static Future<void> cleanupExpiredTokens() async {
    await TokenBlacklist().query
      .where('expires_at', '<', DateTime.now().toIso8601String())
      .delete();
  }

  static Map<String, dynamic> _decodePayload(String token) {
    final parts = token.split('.');
    final payload = base64Url.decode(base64Url.normalize(parts[1]));
    return jsonDecode(utf8.decode(payload));
  }
}

// Database migration for token blacklist
class CreateTokenBlacklistTable extends Migration {
  @override
  Future<void> up() async {
    await createTable('token_blacklist', (table) {
      table.id();
      table.text('token').unique();
      table.bigInteger('user_id').index();
      table.timestamp('expires_at');
      table.timestamp('blacklisted_at');
      table.timestamps();
    });
  }

  @override
  Future<void> down() async {
    await dropTable('token_blacklist');
  }
}
dart

import 'package:khadem/khadem.dart';

class LogoutController {
  /// Logout from current device only
  static Future<void> logout(Request req, Response res) async {
    final token = req.header('authorization')?.replaceFirst('Bearer ', '');

    if (token == null) {
      return res.status(401).sendJson({'error': 'Token required'});
    }

    // Blacklist current token
    final auth = AuthManager();
    await auth.logout(token);

    res.sendJson({
      'message': 'Logged out successfully',
    });
  }

  /// Logout from all devices
  static Future<void> logoutAll(Request req, Response res) async {
    final authenticatable = req.authenticatable;

    if (authenticatable == null) {
      return res.status(401).sendJson({'error': 'Unauthenticated'});
    }

    final userId = authenticatable.getAuthIdentifier();

    // Invalidate all tokens for this user
    final auth = AuthManager();
    await auth.logoutAll(userId);

    res.sendJson({
      'message': 'Logged out from all devices',
    });
  }

  /// Logout from other devices (keep current session)
  static Future<void> logoutOthers(Request req, Response res) async {
    final authenticatable = req.authenticatable;
    final currentToken = req.header('authorization')?.replaceFirst('Bearer ', '');

    if (authenticatable == null || currentToken == null) {
      return res.status(401).sendJson({'error': 'Unauthenticated'});
    }

    final userId = authenticatable.getAuthIdentifier();

    // Invalidate all tokens except current
    final auth = AuthManager();
    await auth.logoutOthers(userId, currentToken);

    res.sendJson({
      'message': 'Logged out from other devices',
    });
  }

  /// Revoke specific device/token
  static Future<void> revokeDevice(Request req, Response res) async {
    final deviceToken = req.input('device_token');
    final authenticatable = req.authenticatable;

    if (authenticatable == null) {
      return res.status(401).sendJson({'error': 'Unauthenticated'});
    }

    if (deviceToken == null) {
      return res.status(400).sendJson({'error': 'Device token required'});
    }

    // Verify user owns this token before revoking
    final userId = authenticatable.getAuthIdentifier();

    // Revoke specific token
    await TokenBlacklistService.blacklistToken(deviceToken);

    res.sendJson({
      'message': 'Device revoked successfully',
    });
  }
}

Secure Token Storage

Choose the right storage method based on your application type. Each method has different security implications.
Storage MethodSecurity LevelUse CaseVulnerabilities
httpOnly CookieHighWeb appsCSRF (mitigated with tokens)
Memory (JS variable)HighSPAsLost on refresh
sessionStorageMediumSPAs (single tab)XSS
localStorageLowNot recommendedXSS, persists across tabs
Secure EnclaveVery HighMobile appsLimited availability
dart

// ===== SERVER-SIDE (Khadem/Dart) =====

import 'package:khadem/khadem.dart';

class SecureTokenController {
  /// Set tokens in secure httpOnly cookies
  static Future<void> loginWithCookies(Request req, Response res) async {
    final data = await req.validate({
      'email': 'required|email',
      'password': 'required|min:6',
    });

    final auth = AuthManager(guard: 'web');
    final authResponse = await auth.attempt(data);

    // Set access token in httpOnly cookie (15 min)
    res.cookieHandler.set(
      'access_token',
      authResponse.accessToken!,
      maxAge: Duration(minutes: 15),
      httpOnly: true,
      secure: true, // HTTPS only
      sameSite: 'strict', // CSRF protection
    );

    // Set refresh token in httpOnly cookie (2 weeks)
    if (authResponse.refreshToken != null) {
      res.cookieHandler.set(
        'refresh_token',
        authResponse.refreshToken!,
        maxAge: Duration(days: 14),
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
      );
    }

    res.sendJson({
      'message': 'Login successful',
      'user': authResponse.user,
    });
  }
}

// ===== CLIENT-SIDE (JavaScript/TypeScript) =====

class SecureTokenClient {
  // RECOMMENDED: Store in memory (most secure)
  private accessToken: string | null = null;

  async login(email: string, password: string) {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      credentials: 'include', // Send cookies
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    const data = await response.json();

    // Store access token in memory
    this.accessToken = data.data.access_token;

    // Refresh token is in httpOnly cookie (can't be accessed by JS)

    return data;
  }

  async apiRequest(url: string, options: RequestInit = {}) {
    const headers = new Headers(options.headers);

    // Add access token to header
    if (this.accessToken) {
      headers.set('Authorization', `Bearer ${this.accessToken}`);
    }

    return fetch(url, {
      ...options,
      headers,
      credentials: 'include', // Send cookies
    });
  }
}

// ACCEPTABLE: sessionStorage for SPAs (lost on tab close)
class SessionStorageClient {
  login(accessToken: string) {
    sessionStorage.setItem('access_token', accessToken);
  }

  getToken() {
    return sessionStorage.getItem('access_token');
  }

  logout() {
    sessionStorage.removeItem('access_token');
  }
}

// NOT RECOMMENDED: localStorage (vulnerable to XSS)
// Don't use this unless absolutely necessary
class LocalStorageClient {
  login(accessToken: string) {
    localStorage.setItem('access_token', accessToken); // ⚠️ XSS risk
  }
}

Custom Token Claims

Add custom claims to tokens to include additional user information, permissions, or application-specific data.
dart

import 'package:khadem/khadem.dart';

/// Add custom claims to JWT tokens
class CustomClaimsController {
  static Future<void> loginWithCustomClaims(Request req, Response res) async {
    final data = await req.validate({
      'email': 'required|email',
      'password': 'required|min:6',
    });

    // Get user
    final user = await User().query
      .where('email', '=', data['email'])
      .first();

    if (user == null) {
      return res.status(401).sendJson({'error': 'Invalid credentials'});
    }

    // Verify password
    if (!HashHelper.verify(data['password'], user.password ?? '')) {
      return res.status(401).sendJson({'error': 'Invalid credentials'});
    }

    // Create token with custom claims
    final tokenPayload = {
      // Standard claims
      'user_id': user.id,
      'email': user.email,

      // Custom claims
      'role': user.role, // User role
      'permissions': user.permissions ?? [], // User permissions
      'account_type': user.accountType, // Free/Premium/Enterprise
      'subscription_expires': user.subscriptionExpires?.toIso8601String(),
      'features': user.enabledFeatures ?? [], // Enabled features

      // Timestamps
      'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      'exp': DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000,
    };

    // Generate token (this would use your JWT library)
    final accessToken = await _generateToken(tokenPayload);

    res.sendJson({
      'success': true,
      'data': {
        'access_token': accessToken,
        'token_type': 'Bearer',
        'expires_in': 3600,
        'user': user.toJson(),
      }
    });
  }

  /// Verify custom claims in middleware
  static Future<void> checkSubscription(Request req, Response res, NextFunction next) async {
    final authenticatable = req.authenticatable;

    if (authenticatable == null) {
      return res.status(401).sendJson({'error': 'Unauthenticated'});
    }

    final user = authenticatable.toAuthArray();

    // Check subscription expiration from token claims
    final subscriptionExpires = user['subscription_expires'];

    if (subscriptionExpires != null) {
      final expiresAt = DateTime.parse(subscriptionExpires);

      if (DateTime.now().isAfter(expiresAt)) {
        return res.status(403).sendJson({
          'error': 'Subscription expired',
          'expires_at': subscriptionExpires,
        });
      }
    }

    await next();
  }

  /// Check if user has specific feature
  static Future<void> requireFeature(String feature) async {
    return Middleware((req, res, next) async {
      final user = req.authenticatable?.toAuthArray();

      if (user == null) {
        return res.status(401).sendJson({'error': 'Unauthenticated'});
      }

      final features = (user['features'] as List?)?.cast<String>() ?? [];

      if (!features.contains(feature)) {
        return res.status(403).sendJson({
          'error': 'Feature not available',
          'required_feature': feature,
        });
      }

      await next();
    });
  }

  static Future<String> _generateToken(Map<String, dynamic> payload) async {
    // Use your JWT library to generate token
    throw UnimplementedError();
  }
}

// Usage
void defineRoutes() {
  // Require premium subscription
  Route.get('/api/premium/content', PremiumController.content)
    .middleware([
      AuthMiddleware.bearer(),
      CustomClaimsController.checkSubscription,
    ]);

  // Require specific feature
  Route.get('/api/analytics', AnalyticsController.index)
    .middleware([
      AuthMiddleware.bearer(),
      await CustomClaimsController.requireFeature('analytics'),
    ]);
}

Managing Tokens Across Multiple Devices

Handle user sessions across multiple devices with device tracking and selective logout.
dart

import 'package:khadem/khadem.dart';

/// Track user sessions across multiple devices
class DeviceSession {
  int? id;
  int? userId;
  String? deviceName;
  String? deviceType; // mobile, web, tablet
  String? ipAddress;
  String? userAgent;
  String? refreshToken;
  DateTime? lastActivity;
  DateTime? createdAt;

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'device_name': deviceName,
      'device_type': deviceType,
      'ip_address': ipAddress,
      'last_activity': lastActivity?.toIso8601String(),
      'created_at': createdAt?.toIso8601String(),
    };
  }
}

class DeviceSessionController {
  /// Get all active sessions for current user
  static Future<void> listSessions(Request req, Response res) async {
    final authenticatable = req.authenticatable;

    if (authenticatable == null) {
      return res.status(401).sendJson({'error': 'Unauthenticated'});
    }

    final userId = authenticatable.getAuthIdentifier();

    final sessions = await DeviceSession().query
      .where('user_id', '=', userId)
      .orderBy('last_activity', 'desc')
      .get();

    res.sendJson({
      'sessions': sessions.map((s) => s.toJson()).toList(),
    });
  }

  /// Revoke specific session
  static Future<void> revokeSession(Request req, Response res) async {
    final sessionId = req.param('id');
    final authenticatable = req.authenticatable;

    if (authenticatable == null) {
      return res.status(401).sendJson({'error': 'Unauthenticated'});
    }

    final userId = authenticatable.getAuthIdentifier();

    // Find session
    final session = await DeviceSession().query
      .where('id', '=', sessionId)
      .where('user_id', '=', userId)
      .first();

    if (session == null) {
      return res.status(404).sendJson({'error': 'Session not found'});
    }

    // Blacklist the refresh token
    if (session.refreshToken != null) {
      await TokenBlacklistService.blacklistToken(session.refreshToken!);
    }

    // Delete session
    await session.delete();

    res.sendJson({
      'message': 'Session revoked',
    });
  }

  /// Store new device session on login
  static Future<void> createSession(
    int userId,
    String refreshToken,
    Request req,
  ) async {
    // Parse user agent for device info
    final userAgent = req.userAgent ?? 'Unknown';
    final deviceInfo = _parseUserAgent(userAgent);

    await DeviceSession().query.insert({
      'user_id': userId,
      'device_name': deviceInfo['name'],
      'device_type': deviceInfo['type'],
      'ip_address': req.ip,
      'user_agent': userAgent,
      'refresh_token': refreshToken,
      'last_activity': DateTime.now().toIso8601String(),
      'created_at': DateTime.now().toIso8601String(),
    });
  }

  /// Update session activity
  static Future<void> updateActivity(String refreshToken) async {
    await DeviceSession().query
      .where('refresh_token', '=', refreshToken)
      .update({'last_activity': DateTime.now().toIso8601String()});
  }

  static Map<String, String> _parseUserAgent(String userAgent) {
    // Simple parsing (use a library for production)
    if (userAgent.contains('Mobile')) {
      return {'name': 'Mobile Device', 'type': 'mobile'};
    } else if (userAgent.contains('Tablet')) {
      return {'name': 'Tablet', 'type': 'tablet'};
    } else {
      return {'name': 'Desktop', 'type': 'web'};
    }
  }
}

// Example response from /api/sessions:
// {
//   "sessions": [
//     {
//       "id": 1,
//       "device_name": "Mobile Device",
//       "device_type": "mobile",
//       "ip_address": "192.168.1.100",
//       "last_activity": "2024-01-15T10:30:00Z",
//       "created_at": "2024-01-14T08:00:00Z"
//     },
//     {
//       "id": 2,
//       "device_name": "Desktop",
//       "device_type": "web",
//       "ip_address": "192.168.1.101",
//       "last_activity": "2024-01-15T09:15:00Z",
//       "created_at": "2024-01-10T12:00:00Z"
//     }
//   ]
// }

Token Configuration

Configure token lifetimes, algorithms, and security settings in your auth configuration.
dart

// config/auth.dart
const authConfig = {
  'jwt': {
    // Secret key (use strong random string)
    'secret': env('JWT_SECRET', 'your-256-bit-secret'),

    // Algorithm (HS256, HS384, HS512, RS256, etc.)
    'algo': 'HS256',

    // Access token lifetime (seconds)
    'ttl': env('JWT_TTL', 3600), // 1 hour

    // Refresh token lifetime (seconds)
    'refresh_ttl': env('JWT_REFRESH_TTL', 1209600), // 2 weeks

    // Token issuer
    'issuer': 'khadem-app',

    // Token audience
    'audience': 'khadem-api',

    // Enable token rotation
    'rotate_refresh_tokens': true,

    // Blacklist check
    'blacklist_enabled': true,

    // Leeway for clock skew (seconds)
    'leeway': 60,
  },

  // Token storage
  'token_storage': {
    'driver': 'database', // database, redis, memory
    'table': 'token_blacklist',
    'cleanup_interval': 3600, // Clean expired tokens every hour
  },

  // Device tracking
  'track_devices': true,
  'max_devices': 5, // Maximum concurrent devices per user
};

// Environment variables (.env)
// JWT_SECRET=your-super-secret-256-bit-key
// JWT_TTL=3600
// JWT_REFRESH_TTL=1209600

Token Security Best Practices

Follow these security guidelines to protect your tokens and users.

✅ Do:

  • Use HTTPS in production
  • Set short access token lifetimes
  • Implement token rotation
  • Validate tokens on every request
  • Use strong secret keys (256+ bits)
  • Store refresh tokens in httpOnly cookies
  • Implement token blacklisting
  • Log security events

❌ Don't:

  • Store tokens in localStorage
  • Include sensitive data in payload
  • Use weak algorithms (None, HS256 with weak keys)
  • Allow infinite token lifetimes
  • Ignore token expiration
  • Send tokens in URL parameters
  • Hard-code secret keys
  • Skip signature verification

On this page