diff --git a/.env.example b/.env.example index 6cbadc3..9bcd43e 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,8 @@ # JWT_SECRET_KEY="your-super-secret-and-long-jwt-key" # OPTIONAL: The duration for which a JWT is valid, in hours. -# Defaults to 1 hour if not specified. -# JWT_EXPIRY_HOURS="1" +# Defaults to 720 hour (1 month) if not specified. +# JWT_EXPIRY_HOURS="720" # REQUIRED FOR PRODUCTION: The specific origin URL of your web client. # This allows the client (e.g., the HT Dashboard) to make requests to the API. @@ -34,7 +34,7 @@ # Use "https://api.eu.sendgrid.com" for EU-based accounts. # SENDGRID_API_URL="https://api.sendgrid.com" -# ADMIN OVERRIDE: Sets the single administrator account for the application. +# REQUIRED: Sets the single administrator account for the application. # On server startup, the system ensures that the user with this email is the # one and only administrator. # - If no admin exists, one will be created with this email. @@ -43,3 +43,16 @@ # - If an admin with this email already exists, nothing changes. # This provides a secure way to set or recover the admin account. # OVERRIDE_ADMIN_EMAIL="admin@example.com" + + +# OPTIONAL: Limit for the /auth/request-code endpoint (requests per window). +# RATE_LIMIT_REQUEST_CODE_LIMIT=3 + +# OPTIONAL: Window for the /auth/request-code endpoint, in hours. +# RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS=24 + +# OPTIONAL: Limit for the generic /data API endpoints (requests per window). +# RATE_LIMIT_DATA_API_LIMIT=1000 + +# OPTIONAL: Window for the /data API endpoints, in minutes. +# RATE_LIMIT_DATA_API_WINDOW_MINUTES=60 diff --git a/README.md b/README.md index cecf467..c3f8b08 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ This API server comes packed with all the features you need to launch a professi * Securely control access to API features and data management capabilities based on user roles. > **Your Advantage:** A powerful, built-in security model that protects your data and ensures users only access what they're supposed to. 🛡️ +#### 🛡️ **Built-in API Rate Limiting** +* Protects critical endpoints like email verification and data access from abuse and denial-of-service attacks. +* Features configurable, user-aware limits that distinguish between guests and authenticated users. +* Includes a bypass for trusted roles (admin, publisher) to ensure dashboard functionality is never impeded. +> **Your Advantage:** Your API is protected from day one against common abuse vectors, ensuring stability and preventing costly overages on services like email providers. ✅ + #### ⚙️ **Centralized App & User Settings** * Effortlessly sync user-specific settings like theme, language, and font styles across devices. * Manage personalized content preferences, including saved headlines and followed topics/sources. diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index e14d15f..75dca6b 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -13,8 +13,10 @@ import 'package:flutter_news_app_api_server_full_source_code/src/services/dashbo import 'package:flutter_news_app_api_server_full_source_code/src/services/database_seeding_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/default_user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/jwt_auth_token_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/verification_code_storage_service.dart'; @@ -65,6 +67,7 @@ class AppDependencies { late final DashboardSummaryService dashboardSummaryService; late final PermissionService permissionService; late final UserPreferenceLimitService userPreferenceLimitService; + late final RateLimitService rateLimitService; /// Initializes all application dependencies. /// @@ -222,6 +225,10 @@ class AppDependencies { permissionService: permissionService, log: Logger('DefaultUserPreferenceLimitService'), ); + rateLimitService = MongoDbRateLimitService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbRateLimitService'), + ); _isInitialized = true; _log.info('Application dependencies initialized successfully.'); @@ -238,6 +245,7 @@ class AppDependencies { if (!_isInitialized) return; await _mongoDbConnectionManager.close(); tokenBlacklistService.dispose(); + rateLimitService.dispose(); _isInitialized = false; _log.info('Application dependencies disposed.'); } diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index d65fc70..81147df 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -109,7 +109,7 @@ abstract final class EnvironmentConfig { /// The value is read from the `JWT_EXPIRY_HOURS` environment variable. /// Defaults to 1 hour if not set or if parsing fails. static Duration get jwtExpiryDuration { - final hours = int.tryParse(_env['JWT_EXPIRY_HOURS'] ?? '1'); + final hours = int.tryParse(_env['JWT_EXPIRY_HOURS'] ?? '720'); // 1 month return Duration(hours: hours ?? 1); } @@ -139,4 +139,37 @@ abstract final class EnvironmentConfig { /// This is used to set or replace the single administrator account on startup. /// Returns `null` if the `OVERRIDE_ADMIN_EMAIL` is not set. static String? get overrideAdminEmail => _env['OVERRIDE_ADMIN_EMAIL']; + + /// Retrieves the request limit for the request-code endpoint. + /// + /// Defaults to 3 if not set or if parsing fails. + static int get rateLimitRequestCodeLimit { + return int.tryParse(_env['RATE_LIMIT_REQUEST_CODE_LIMIT'] ?? '3') ?? 3; + } + + /// Retrieves the time window for the request-code endpoint rate limit. + /// + /// Defaults to 24 hours if not set or if parsing fails. + static Duration get rateLimitRequestCodeWindow { + final hours = + int.tryParse(_env['RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS'] ?? '24') ?? + 24; + return Duration(hours: hours); + } + + /// Retrieves the request limit for the data API endpoints. + /// + /// Defaults to 1000 if not set or if parsing fails. + static int get rateLimitDataApiLimit { + return int.tryParse(_env['RATE_LIMIT_DATA_API_LIMIT'] ?? '1000') ?? 1000; + } + + /// Retrieves the time window for the data API rate limit. + /// + /// Defaults to 60 minutes if not set or if parsing fails. + static Duration get rateLimitDataApiWindow { + final minutes = + int.tryParse(_env['RATE_LIMIT_DATA_API_WINDOW_MINUTES'] ?? '60') ?? 60; + return Duration(minutes: minutes); + } } diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index 6cc5ed7..57c2732 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -66,6 +66,12 @@ Middleware errorHandler() { /// Maps HttpException subtypes to appropriate HTTP status codes. int _mapExceptionToStatusCode(HttpException exception) { + // Special case for rate limiting + if (exception is ForbiddenException && + exception.message.contains('too many requests')) { + return 429; // Too Many Requests + } + return switch (exception) { InvalidInputException() => HttpStatus.badRequest, // 400 AuthenticationException() => HttpStatus.unauthorized, // 401 diff --git a/lib/src/middlewares/rate_limiter_middleware.dart b/lib/src/middlewares/rate_limiter_middleware.dart new file mode 100644 index 0000000..4e0dac0 --- /dev/null +++ b/lib/src/middlewares/rate_limiter_middleware.dart @@ -0,0 +1,65 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; + +/// Extracts the client's IP address from the request. +/// +/// It prioritizes the 'X-Forwarded-For' header, which is standard for +/// identifying the originating IP of a client connecting through a proxy +/// or load balancer. If that header is not present, it falls back to the +/// direct connection's IP address. +String? _getIpAddress(RequestContext context) { + final headers = context.request.headers; + // The 'X-Forwarded-For' header can contain a comma-separated list of IPs. + // The first one is typically the original client IP. + final xff = headers['X-Forwarded-For']?.split(',').first.trim(); + if (xff != null && xff.isNotEmpty) { + return xff; + } + // Fallback to the direct connection IP if XFF is not available. + return context.request.connectionInfo.remoteAddress.address; +} + +/// Middleware to enforce rate limiting on a route. +/// +/// This middleware uses the [RateLimitService] to track and limit the number +/// of requests from a unique source (identified by a key) within a specific +/// time window. +/// +/// - [limit]: The maximum number of requests allowed. +/// - [window]: The time duration in which the requests are counted. +/// - [keyExtractor]: A function that extracts a unique key from the request +/// context. This could be an IP address, an email from the body, etc. +Middleware rateLimiter({ + required int limit, + required Duration window, + required Future Function(RequestContext) keyExtractor, +}) { + return (handler) { + return (context) async { + final rateLimitService = context.read(); + final key = await keyExtractor(context); + + // If a key cannot be extracted, we bypass the rate limiter. + // This is a safeguard; for IP-based limiting, a key should always exist. + if (key == null || key.isEmpty) { + return handler(context); + } + + // The checkRequest method will throw a ForbiddenException if the + // limit is exceeded. This will be caught by the global error handler. + await rateLimitService.checkRequest( + key: key, + limit: limit, + window: window, + ); + + // If the check passes, proceed to the next handler. + return handler(context); + }; + }; +} + +/// A specific implementation of the keyExtractor for IP-based rate limiting. +Future ipKeyExtractor(RequestContext context) async { + return _getIpAddress(context); +} diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 54ece71..c278f54 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -61,4 +61,7 @@ abstract class Permissions { // User Preference Permissions static const String userPreferenceBypassLimits = 'user_preference.bypass_limits'; + + // General System Permissions + static const String rateLimitingBypass = 'rate_limiting.bypass'; } diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index e3cc0b9..99b275f 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -34,10 +34,12 @@ final Set _dashboardPublisherPermissions = { Permissions.headlineUpdate, Permissions.headlineDelete, Permissions.dashboardLogin, + Permissions.rateLimitingBypass, }; final Set _dashboardAdminPermissions = { ..._dashboardPublisherPermissions, + Permissions.rateLimitingBypass, Permissions.topicCreate, Permissions.topicUpdate, Permissions.topicDelete, diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 7559967..d7a3e51 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart'; import 'package:logging/logging.dart'; @@ -300,6 +301,25 @@ class DatabaseSeedingService { ], }); + // Index for the rate limit attempts collection + await _db.runCommand({ + 'createIndexes': kRateLimitAttemptsCollection, + 'indexes': [ + { + // This is a TTL index. MongoDB will automatically delete request + // attempt documents 24 hours after they are created. + 'key': {'createdAt': 1}, + 'name': 'createdAt_ttl_index', + 'expireAfterSeconds': 86400, // 24 hours + }, + { + // Index on the key field for faster lookups. + 'key': {'key': 1}, + 'name': 'key_index', + }, + ], + }); + _log.info('Database indexes are set up correctly.'); } on Exception catch (e, s) { _log.severe('Failed to create database indexes.', e, s); diff --git a/lib/src/services/mongodb_rate_limit_service.dart b/lib/src/services/mongodb_rate_limit_service.dart new file mode 100644 index 0000000..fb6f1ff --- /dev/null +++ b/lib/src/services/mongodb_rate_limit_service.dart @@ -0,0 +1,87 @@ +import 'package:core/core.dart'; +import 'package:data_mongodb/data_mongodb.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; +import 'package:logging/logging.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +/// The name of the MongoDB collection for storing rate limit attempts. +const String kRateLimitAttemptsCollection = 'rate_limit_attempts'; + +/// {@template mongodb_rate_limit_service} +/// A MongoDB-backed implementation of [RateLimitService]. +/// +/// This service tracks request attempts in a dedicated MongoDB collection. +/// It relies on a TTL (Time-To-Live) index on the `createdAt` field to +/// ensure that old request records are automatically purged by the database, +/// which is highly efficient. +/// {@endtemplate} +class MongoDbRateLimitService implements RateLimitService { + /// {@macro mongodb_rate_limit_service} + MongoDbRateLimitService({ + required MongoDbConnectionManager connectionManager, + required Logger log, + }) : _connectionManager = connectionManager, + _log = log; + + final MongoDbConnectionManager _connectionManager; + final Logger _log; + + DbCollection get _collection => + _connectionManager.db.collection(kRateLimitAttemptsCollection); + + @override + Future checkRequest({ + required String key, + required int limit, + required Duration window, + }) async { + try { + final now = DateTime.now(); + final windowStart = now.subtract(window); + + // 1. Count recent requests for the given key within the time window. + final recentRequestsCount = await _collection.count( + where.eq('key', key).and(where.gte('createdAt', windowStart)), + ); + + _log.finer( + 'Rate limit check for key "$key": Found $recentRequestsCount ' + 'requests in the last ${window.inMinutes} minutes (limit is $limit).', + ); + + // 2. If the limit is reached or exceeded, throw an exception. + if (recentRequestsCount >= limit) { + _log.warning( + 'Rate limit exceeded for key "$key". ' + '($recentRequestsCount >= $limit)', + ); + throw const ForbiddenException( + 'You have made too many requests. Please try again later.', + ); + } + + // 3. If the limit is not reached, record the new request. + await _collection.insertOne({ + '_id': ObjectId(), + 'key': key, + 'createdAt': now, + }); + _log.finer('Recorded new request for key "$key".'); + } on HttpException { + // Re-throw exceptions that we've thrown intentionally. + rethrow; + } catch (e, s) { + _log.severe('Error during rate limit check for key "$key"', e, s); + throw const OperationFailedException( + 'An unexpected error occurred while checking request rate limits.', + ); + } + } + + @override + void dispose() { + // This is a no-op because the underlying database connection is managed + // by the injected MongoDbConnectionManager, which has its own lifecycle. + _log.finer('dispose() called, no action needed.'); + } +} diff --git a/lib/src/services/rate_limit_service.dart b/lib/src/services/rate_limit_service.dart new file mode 100644 index 0000000..8a80f3b --- /dev/null +++ b/lib/src/services/rate_limit_service.dart @@ -0,0 +1,37 @@ +import 'package:core/core.dart'; + +/// {@template rate_limit_service} +/// Defines the interface for a service that provides rate-limiting capabilities. +/// +/// This service is used to check and record requests against a specific key +/// (e.g., an IP address or email) to prevent abuse of sensitive or expensive +/// endpoints. +/// {@endtemplate} +abstract class RateLimitService { + /// {@macro rate_limit_service} + const RateLimitService(); + + /// Checks if a request associated with the given [key] is allowed. + /// + /// This method performs the following logic: + /// 1. Counts the number of recent requests for the [key] within the [window]. + /// 2. If the count is greater than or equal to the [limit], it throws a + /// [ForbiddenException] indicating the rate limit has been exceeded. + /// 3. If the count is below the limit, it records the current request + /// (e.g., by storing a timestamp) and allows the request to proceed. + /// + /// - [key]: A unique identifier for the request source (e.g., IP address). + /// - [limit]: The maximum number of requests allowed within the window. + /// - [window]: The time duration to consider for counting requests. + /// + /// Throws [ForbiddenException] if the rate limit is exceeded. + /// Throws [OperationFailedException] for unexpected errors during the check. + Future checkRequest({ + required String key, + required int limit, + required Duration window, + }); + + /// Disposes of any resources used by the service (e.g., timers). + void dispose(); +} diff --git a/routes/_middleware.dart b/routes/_middleware.dart index 419731c..98f813a 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -10,6 +10,7 @@ import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_ import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_token_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/verification_code_storage_service.dart'; @@ -131,6 +132,7 @@ Handler middleware(Handler handler) { (_) => deps.userPreferenceLimitService, ), ) + .use(provider((_) => deps.rateLimitService)) .call(context); }; }); diff --git a/routes/api/v1/auth/request-code/_middleware.dart b/routes/api/v1/auth/request-code/_middleware.dart new file mode 100644 index 0000000..160d33f --- /dev/null +++ b/routes/api/v1/auth/request-code/_middleware.dart @@ -0,0 +1,15 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/rate_limiter_middleware.dart'; + +/// This middleware applies a rate limit specifically to the +/// `/api/v1/auth/request-code` endpoint. +Handler middleware(Handler handler) { + return handler.use( + rateLimiter( + limit: EnvironmentConfig.rateLimitRequestCodeLimit, + window: EnvironmentConfig.rateLimitRequestCodeWindow, + keyExtractor: ipKeyExtractor, + ), + ); +} diff --git a/routes/api/v1/auth/request-code.dart b/routes/api/v1/auth/request-code/index.dart similarity index 100% rename from routes/api/v1/auth/request-code.dart rename to routes/api/v1/auth/request-code/index.dart diff --git a/routes/api/v1/data/_middleware.dart b/routes/api/v1/data/_middleware.dart index 2eaa41d..c5b67f9 100644 --- a/routes/api/v1/data/_middleware.dart +++ b/routes/api/v1/data/_middleware.dart @@ -1,26 +1,40 @@ import 'package:core/core.dart'; import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/authentication_middleware.dart'; -import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/authorization_middleware.dart'; // Import authorization middleware +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/authorization_middleware.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/rate_limiter_middleware.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permissions.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart'; -/// Middleware specific to the generic `/api/v1/data` route path. -/// -/// This middleware chain performs the following in order: -/// 1. **Authentication Check (`requireAuthentication`):** Ensures that the user -/// is authenticated. If not, it aborts the request with a 401. -/// 2. **Model Validation & Context Provision (`_modelValidationAndProviderMiddleware`):** -/// - Validates the `model` query parameter. -/// - Looks up the `ModelConfig` from the `ModelRegistryMap`. -/// - Provides the `ModelConfig` and `modelName` into the request context -/// for downstream middleware and route handlers. -/// 3. **Authorization Check (`authorizationMiddleware`):** Enforces role-based -/// and model-specific permissions based on the `ModelConfig` metadata. -/// If the user lacks permission, it throws a [ForbiddenException]. -/// -/// This setup ensures that data routes are protected, have the necessary -/// model-specific configuration available, and access is authorized before -/// reaching the final route handler. +// Helper middleware for applying rate limiting to the data routes. +Middleware _dataRateLimiterMiddleware() { + return (handler) { + return (context) { + final user = context.read(); + final permissionService = context.read(); + + // Users with the bypass permission are not rate-limited. + if (permissionService.hasPermission( + user, + Permissions.rateLimitingBypass, + )) { + return handler(context); + } + + // For all other users, apply the configured rate limit. + // The key is the user's ID, ensuring the limit is per-user. + final rateLimitHandler = rateLimiter( + limit: EnvironmentConfig.rateLimitDataApiLimit, + window: EnvironmentConfig.rateLimitDataApiWindow, + keyExtractor: (context) async => context.read().id, + )(handler); + + return rateLimitHandler(context); + }; + }; +} // Helper middleware for model validation and context provision. Middleware _modelValidationAndProviderMiddleware() { @@ -79,15 +93,19 @@ Handler middleware(Handler handler) { // resulting in a 401 response via the global `errorHandler`). // - If `User` is present, the request proceeds to the next middleware. // - // 2. `_modelValidationAndProviderMiddleware()`: + // 2. `_dataRateLimiterMiddleware()`: // - This runs if `requireAuthentication()` passes. + // - It checks if the user has a bypass permission. If not, it applies + // the configured rate limit based on the user's ID. + // - If the limit is exceeded, it throws a `ForbiddenException`. + // + // 3. `_modelValidationAndProviderMiddleware()`: + // - This runs if rate limiting passes. // - It validates the `?model=` query parameter and provides the // `ModelConfig` and `modelName` into the context. - // - If model validation fails, it throws a BadRequestException, caught - // by the global errorHandler. - // - If successful, it calls the next handler in the chain (authorizationMiddleware). + // - If model validation fails, it throws a `BadRequestException`. // - // 3. `authorizationMiddleware()`: + // 4. `authorizationMiddleware()`: // - This runs if `_modelValidationAndProviderMiddleware()` passes. // - It reads the `User`, `modelName`, and `ModelConfig` from the context. // - It checks if the user has permission to perform the requested HTTP @@ -97,14 +115,15 @@ Handler middleware(Handler handler) { // - If successful, it calls the next handler in the chain (the actual // route handler). // - // 4. Actual Route Handler (from `index.dart` or `[id].dart`): + // 5. Actual Route Handler (from `index.dart` or `[id].dart`): // - This runs last, only if all preceding middlewares pass. It will have // access to a non-null `User`, `ModelConfig`, and `modelName` from the context. // - It performs the data operation and any necessary handler-level // ownership checks (if flagged by `ModelActionPermission.requiresOwnershipCheck`). // return handler - .use(authorizationMiddleware()) // Applied third (inner) - .use(_modelValidationAndProviderMiddleware()) // Applied second + .use(authorizationMiddleware()) // Applied fourth (inner-most) + .use(_modelValidationAndProviderMiddleware()) // Applied third + .use(_dataRateLimiterMiddleware()) // Applied second .use(requireAuthentication()); // Applied first (outermost) }