bloomBUILD•SHIP•BLOOM
.md
Authentication & Sessions

Authentication

bloom_auth_server provides server-side authentication primitives modeled on djangors-auth, designed as the backend partner to BloomAuth<U> on Flutter clients.

Server-Side Authentication Primitives

Authentication state is configured globally through BLOOM_AUTH_SECRET loaded by BloomEnv. The package provides constant-time password verification, sliding-window throttling, and JWT token issuance out of the box:

lib/apps/accounts/views.dart
dart
// lib/apps/accounts/views.dart - Authentication Flows
import 'package:bloom_auth_server/bloom_auth_server.dart';
import 'package:bloom_db/bloom_db.dart';
import 'package:bloom_errors/bloom_errors.dart';
import 'package:bloom_framework/bloom_server.dart';
import 'models.dart';

// Unified rate limiter: 5 attempts / 15m window, 1h lockout after 5 consecutive failures
final authLimiter = AuthRateLimiter(
  maxAttempts: 5,
  window: const Duration(minutes: 15),
  lockoutDuration: const Duration(hours: 1),
);

Future<BloomResponse> handleLogin(BloomRequest req, DbExecutor db) async {
  final body = req.bodyJson as Map<String, dynamic>;
  final email = body['email']?.toString().trim().toLowerCase() ?? '';
  final password = body['password']?.toString() ?? '';

  // 1. Verify rate limiting and lockout state
  try {
    authLimiter.verifyAllowed(email);
  } on AccountLockedException catch (e) {
    throw BloomTooManyRequestsException(
      'Account is temporarily locked due to too many failed attempts.',
      {'retryAfterSeconds': e.retryAfterSeconds},
    );
  } on RateLimitException catch (e) {
    throw BloomTooManyRequestsException(
      'Too many login attempts. Please try again later.',
      {'retryAfterSeconds': e.retryAfterSeconds},
    );
  }

  // 2. Lookup user and verify password
  final user = await User.objects().filter(Q('email', email)).first(db);
  if (user == null || !verifyPassword(password, user.passwordHash)) {
    // Constant-time dummy verification preserves uniform latency
    if (user == null) dummyVerifyPassword(password);
    authLimiter.recordFailure(email);
    throw BloomUnauthorizedException('Invalid email or password');
  }

  // 3. Successful authentication clears failure streaks
  authLimiter.recordSuccess(email);

  // 4. Issue cryptographically signed HMAC-SHA256 session token
  final token = issueSessionToken(
    userId: user.id.toString(),
    email: user.email,
    roles: const ['user'],
    ttl: const Duration(days: 7),
  );

  return BloomResponse.json({'token': token, 'user': user.toJson()});
}

Password Hashing & Session Tokens

OpenBSD BCrypt Hashing

Uses hashPassword(password, cost: 12) with configurable rounds (4,096 iterations).

When a requested account is not found, dummyVerifyPassword(password) executes uniform verification cycles to defeat timing attacks and account enumeration.

Signed Session JWTs

issueSessionToken() generates RFC 7519 JWT bearer tokens signed with HMAC-SHA256.

Tokens include domain-separated token_type: 'session' claims to prevent token substitution attacks.

Rate Limiting & Account Lockout

AuthRateLimiter combines sliding-window rate limiting (InMemoryRateLimiter) and consecutive-failure account lockout (InMemoryLockoutManager) with fail-closed security semantics:

  • verifyAllowed(identifier) — Throws AccountLockedException or RateLimitException if thresholds are exceeded.
  • recordFailure(identifier) — Increments attempt count and triggers account lockout after consecutive failed attempts.
  • recordSuccess(identifier) — Clears failure streaks upon successful authentication.

Route Protection with BloomAuthMiddleware

Attach BloomAuthMiddleware to routes or routers to enforce bearer token validation, decode claims, and attach user information to req.auth and req.authUserId:

lib/apps/accounts/urls.dart
dart
// lib/apps/accounts/urls.dart - Protecting Routes with Middleware
import 'package:bloom_auth_server/bloom_auth_server.dart';
import 'package:bloom_framework/bloom_server.dart';

void registerUrls(BloomApiRouter router, BloomServerContext ctx) {
  // Public routes
  router.post('/api/auth/login', (req) => handleLogin(req, ctx.db));

  // Protected route requiring valid session token
  router.get(
    '/api/accounts/me',
    (req) async {
      final userId = req.authUserId; // Extracted automatically
      return BloomResponse.json({'userId': userId, 'claims': req.auth?.toMap()});
    },
    middlewares: [const BloomAuthMiddleware()],
  );

  // Role-gated route
  router.get(
    '/api/admin/metrics',
    (req) async => BloomResponse.json({'status': 'ok'}),
    middlewares: [BloomAuthMiddleware.requireRole('admin')],
  );
}

Hash-Bound Password Reset

generatePasswordResetToken() creates single-purpose tokens bound cryptographically to the user's current password hash. Whenever the user's password changes, all outstanding reset tokens are invalidated immediately without requiring database revocation tables.