Logodart_express

Sessions#

dart_express provides built-in session management with pluggable storage backends.

Quick Start#

Enable sessions with a secret key:

final app = DartExpress(
  sessionSecret: 'your-secret-key-min-32-characters-long!',
);

app.get('/counter', (req, res) {
  final count = (req.session['count'] as int? ?? 0) + 1;
  req.session['count'] = count;
  res.json({'visits': count});
});

await app.listen(3000);

How Sessions Work#

  1. First Request: Client has no session cookie
  2. Server Creates Session: Generates session ID and stores data
  3. Send Cookie: Server sends signed session cookie to client
  4. Subsequent Requests: Client sends cookie, server loads session

Accessing Sessions#

Reading Data#

app.get('/profile', (req, res) {
  final userId = req.session['userId'];
  final username = req.session['username'];
  
  if (userId == null) {
    return res.status(401).json({'error': 'Not logged in'});
  }
  
  res.json({
    'userId': userId,
    'username': username,
  });
});

Writing Data#

app.post('/login', (req, res) async {
  final body = await req.body;
  
  // Verify credentials...
  
  // Store in session
  req.session['userId'] = '123';
  req.session['username'] = body['username'];
  req.session['loginTime'] = DateTime.now().toIso8601String();
  
  res.json({'success': true});
});

Deleting Data#

app.post('/logout', (req, res) {
  req.session.clear();
  res.json({'message': 'Logged out'});
});

Session Stores#

Memory Store (Default)#

Good for development, not for production:

final app = DartExpress(
  sessionSecret: 'secret',
  sessionStore: MemorySessionStore(), // Default
);

Limitations:

  • Data lost on server restart
  • Doesn't work with multiple server instances
  • Memory usage grows over time

Custom Store#

Implement SessionStore for your backend:

class RedisSessionStore implements SessionStore {
  final RedisClient redis;
  
  RedisSessionStore(this.redis);
  
  @override
  Future<Map<String, dynamic>?> get(String sessionId) async {
    final data = await redis.get('session:$sessionId');
    return data != null ? jsonDecode(data) : null;
  }
  
  @override
  Future<void> set(String sessionId, Map<String, dynamic> data, {Duration? ttl}) async {
    await redis.set(
      'session:$sessionId',
      jsonEncode(data),
      ex: ttl?.inSeconds ?? 86400, // 24 hours default
    );
  }
  
  @override
  Future<void> destroy(String sessionId) async {
    await redis.del('session:$sessionId');
  }
  
  @override
  Future<void> dispose() async {
    await redis.close();
  }
}

// Usage
final app = DartExpress(
  sessionSecret: 'secret',
  sessionStore: RedisSessionStore(redisClient),
);

Session Configuration#

Session Secret#

Required - Used to sign session cookies:

final app = DartExpress(
  sessionSecret: Platform.environment['SESSION_SECRET']!,
);

Requirements:

  • Minimum 32 characters
  • Cryptographically random
  • Never commit to git
  • Rotate periodically

Secure Cookies#

Enable for HTTPS (production):

final app = DartExpress(
  sessionSecret: 'secret',
  secureCookies: true, // HTTPS only
);

Customize the session cookie name:

final app = DartExpress(
  sessionSecret: 'secret',
  sessionCookieName: 'my_app_session',
);

Authentication Example#

Complete login/logout flow:

void main() async {
  final app = DartExpress(
    sessionSecret: Platform.environment['SESSION_SECRET']!,
    secureCookies: true,
  );
  
  // Login endpoint
  app.post('/login', (req, res) async {
    final body = await req.body;
    final email = body['email'];
    final password = body['password'];
    
    // Validate credentials (example)
    if (email == 'user@example.com' && password == 'password') {
      req.session['userId'] = '123';
      req.session['email'] = email;
      req.session['loginAt'] = DateTime.now().toIso8601String();
      
      return res.json({'success': true});
    }
    
    res.status(401).json({'error': 'Invalid credentials'});
  });
  
  // Protected route
  app.get('/dashboard', requireAuth, (req, res) {
    res.json({
      'user': req.session['email'],
      'loggedIn': true,
    });
  });
  
  // Logout
  app.post('/logout', (req, res) {
    req.session.clear();
    res.json({'message': 'Logged out successfully'});
  });
  
  await app.listen(3000);
}

// Auth middleware
Future<void> requireAuth(Request req, Response res, NextFunction next) async {
  if (!req.session.containsKey('userId')) {
    return res.status(401).json({'error': 'Authentication required'});
  }
  await next();
}

Session Lifetime#

Sessions expire after inactivity:

final app = DartExpress(
  sessionSecret: 'secret',
  sessionStore: MemorySessionStore(
    cleanupInterval: Duration(minutes: 5),  // Cleanup frequency
  ),
);

Default TTL: 24 hours

Security Best Practices#

1. Use Strong Secrets#

// ❌ Bad
sessionSecret: '12345'

// ✅ Good
sessionSecret: Platform.environment['SESSION_SECRET']!

Generate with:

dart run -e "import 'dart:math'; import 'dart:convert'; print(base64Encode(List.generate(32, (_) => Random.secure().nextInt(256))))"

2. HTTPS Only in Production#

final app = DartExpress(
  sessionSecret: secret,
  secureCookies: Platform.environment['ENV'] == 'production',
);

3. Regenerate Session on Login#

app.post('/login', (req, res) async {
  // Clear old session
  req.session.clear();
  
  // Create new session
  req.session['userId'] = userId;
  req.session['createdAt'] = DateTime.now().toIso8601String();
});

4. Implement Session Timeout#

Future<void> checkSessionTimeout(Request req, Response res, NextFunction next) async {
  final loginTime = req.session['loginAt'] as String?;
  
  if (loginTime != null) {
    final login = DateTime.parse(loginTime);
    final now = DateTime.now();
    
    if (now.difference(login).inHours > 8) {
      req.session.clear();
      return res.status(401).json({'error': 'Session expired'});
    }
  }
  
  await next();
}

5. Store Minimal Data#

// ❌ Don't store sensitive data
req.session['password'] = password;
req.session['creditCard'] = ccNumber;

// ✅ Store only IDs and references
req.session['userId'] = userId;

Testing Sessions#

import 'package:test/test.dart';
import 'package:http/http.dart' as http;

void main() {
  test('login creates session', () async {
    final response = await http.post(
      Uri.parse('http://localhost:3000/login'),
      body: {'email': 'test@example.com', 'password': 'pass'},
    );
    
    expect(response.statusCode, 200);
    
    // Extract session cookie
    final cookie = response.headers['set-cookie'];
    expect(cookie, isNotNull);
    
    // Use session in next request
    final dashboard = await http.get(
      Uri.parse('http://localhost:3000/dashboard'),
      headers: {'cookie': cookie!},
    );
    
    expect(dashboard.statusCode, 200);
  });
}

Common Patterns#

Role-Based Access#

req.session['role'] = 'admin';

Future<void> requireAdmin(Request req, Response res, NextFunction next) async {
  if (req.session['role'] != 'admin') {
    return res.status(403).json({'error': 'Admin only'});
  }
  await next();
}

Shopping Cart#

app.post('/cart/add', (req, res) async {
  final body = await req.body;
  final cart = req.session['cart'] as List? ?? [];
  
  cart.add(body['itemId']);
  req.session['cart'] = cart;
  
  res.json({'cartSize': cart.length});
});

Remember Me#

app.post('/login', (req, res) async {
  final body = await req.body;
  final rememberMe = body['rememberMe'] == true;
  
  req.session['userId'] = userId;
  
  if (rememberMe) {
    res.cookie('remember_token', token, maxAge: Duration(days: 30));
  }
});