Welcome to Chapter 11 of our Node.js backend journey! In this chapter, we’re diving deep into critical security enhancements that are non-negotiable for any production-ready application: Rate Limiting, Cross-Origin Resource Sharing (CORS), and Role-Based Access Control (RBAC). These mechanisms are essential for protecting your API from abuse, enabling secure interactions with frontend applications, and ensuring users only access resources they are authorized to see.
By the end of this chapter, you will have fortified your Fastify application with robust defenses. We’ll implement measures to prevent brute-force attacks and denial-of-service attempts through rate limiting, configure secure communication channels for client-side applications using CORS, and establish a granular permission system with RBAC to control access based on user roles. This incremental approach will ensure each security layer is properly integrated and tested, bringing us closer to a truly production-grade backend.
Prerequisites
Before starting this chapter, ensure you have completed the previous chapters, especially those covering:
- Chapter 9: Authentication & Authorization: Where we set up user registration, login, JWT token handling, and basic authorization.
- Chapter 10: Session & Token Handling: Where we refined JWT handling and introduced refresh tokens.
We will build upon the existing authentication context, assuming request.user contains authenticated user information, including their assigned roles.
Planning & Design
Implementing security features requires careful consideration of where they fit within the request lifecycle and how they interact with existing components.
Component Architecture
The security features we’re implementing will act as middleware or plugins within our Fastify application’s request pipeline.
- CORS will typically be applied early to handle pre-flight requests and set appropriate headers.
- Rate Limiting will also be applied early, often before authentication, to prevent unauthenticated abuse.
- RBAC will be applied at the route level, after authentication, to check user roles against required permissions for specific endpoints.
File Structure
We’ll integrate these features as Fastify plugins or decorators, maintaining a clean and modular project structure.
src/plugins/cors.ts: For CORS configuration.src/plugins/rate-limit.ts: For rate limiting configuration.src/plugins/rbac.ts: For our custom RBAC implementation.src/routes/...: Existing routes will be modified to incorporate RBAC.
Step-by-Step Implementation
Let’s begin by enhancing our application’s security, one layer at a time.
1. Implementing Rate Limiting
Rate limiting is crucial for preventing various forms of abuse, such as brute-force attacks, spamming, and denial-of-service (DoS) attacks. We’ll use the @fastify/rate-limit plugin.
a) Setup/Configuration
First, install the plugin:
npm install @fastify/rate-limit
npm install --save-dev @types/fastify__rate-limit # If using TypeScript
Next, let’s create a plugin for rate limiting.
File: src/plugins/rate-limit.ts
import fp from 'fastify-plugin';
import rateLimit from '@fastify/rate-limit';
import { FastifyInstance } from 'fastify';
import { AppConfig } from '../config';
/**
* @fastify/rate-limit plugin for Fastify.
* Configures rate limiting for the application.
*/
export default fp(async (fastify: FastifyInstance) => {
const { RATE_LIMIT_MAX_REQUESTS, RATE_LIMIT_TIME_WINDOW_MS } = AppConfig;
fastify.register(rateLimit, {
max: RATE_LIMIT_MAX_REQUESTS, // Max requests per time window
timeWindow: RATE_LIMIT_TIME_WINDOW_MS, // Time window in milliseconds (e.g., 60000 for 1 minute)
hook: 'preHandler', // Where to apply the hook (before route handlers)
// Optional: Add a key generator if you want to limit based on something other than IP
// keyGenerator: (request) => request.headers['x-forwarded-for'] || request.ip,
errorResponseBuilder: (request, context) => {
fastify.log.warn(`Rate limit exceeded for IP: ${request.ip} on route: ${request.url}`);
return {
statusCode: 429,
error: 'Too Many Requests',
message: `You have exceeded the request limit of ${context.max} requests in ${context.timeWindow}ms. Please try again after some time.`,
retryAfter: context.after, // Time in seconds to wait before retrying
};
},
// For production, consider using a store like Redis for distributed rate limiting
// store: new RedisStore({ client: new Redis(...) })
});
fastify.log.info(`Rate Limiting enabled: Max ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_TIME_WINDOW_MS / 1000} seconds.`);
});
We need to add these configuration variables to our src/config.ts and .env files.
File: src/config.ts (add to AppConfig interface and AppConfig object)
// ... existing imports and interfaces
interface IAppConfig {
// ... existing fields
RATE_LIMIT_MAX_REQUESTS: number;
RATE_LIMIT_TIME_WINDOW_MS: number;
}
export const AppConfig: IAppConfig = {
// ... existing fields
PORT: parseInt(process.env.PORT || '3000', 10),
NODE_ENV: process.env.NODE_ENV || 'development',
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
JWT_SECRET: process.env.JWT_SECRET || 'supersecretjwtkey',
JWT_ACCESS_TOKEN_EXPIRATION: process.env.JWT_ACCESS_TOKEN_EXPIRATION || '15m',
JWT_REFRESH_TOKEN_EXPIRATION: process.env.JWT_REFRESH_TOKEN_EXPIRATION || '7d',
RATE_LIMIT_MAX_REQUESTS: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10),
RATE_LIMIT_TIME_WINDOW_MS: parseInt(process.env.RATE_LIMIT_TIME_WINDOW_MS || '60000', 10), // 1 minute
// ... other fields
};
File: .env (add these lines)
# ... existing environment variables
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_TIME_WINDOW_MS=60000 # 1 minute
Finally, register the plugin in our main application file.
File: src/app.ts (add import and registration)
// ... existing imports
import rateLimitPlugin from './plugins/rate-limit'; // Add this import
// ... inside the buildApp function
export function buildApp() {
const fastify = Fastify({
logger: PinoLogger,
disableRequestLogging: true, // Disable default request logging if using pino-http
});
// Register plugins
fastify.register(SwaggerPlugin);
fastify.register(SensiblePlugin);
fastify.register(HelmetPlugin);
fastify.register(rateLimitPlugin); // Register rate limit plugin here
// ... other plugins
b) Core Implementation Explanation
- We’re using
fp(fastify-plugin) to make sure our plugin is properly encapsulated and registered. max: This defines the maximum number of requests allowed within thetimeWindow.timeWindow: This defines the duration in milliseconds for which themaxrequests are counted. Here, 60000ms means 1 minute.hook: 'preHandler': This ensures the rate limiting check happens before any route handler is executed.errorResponseBuilder: This customizes the error response sent to the client when the rate limit is exceeded. It also logs the event, which is vital for monitoring.keyGenerator: (Commented out) By default,@fastify/rate-limitusesrequest.ip. If your application is behind a proxy, you might need to userequest.headers['x-forwarded-for']to get the real client IP. Ensure your proxy is configured to set this header correctly and securely.- For production, especially with multiple instances, you’d use a distributed store like Redis (
@fastify/rate-limit-redis) to ensure consistent rate limiting across all instances.
c) Testing This Component
- Start your Fastify application:
npm run dev - Use a tool like
curlor Postman to make rapid requests to any endpoint, e.g.,GET /api/v1/health. - Make more than
RATE_LIMIT_MAX_REQUESTSrequests withinRATE_LIMIT_TIME_WINDOW_MS. - You should receive a
429 Too Many Requestsstatus code with the custom error message configured. - Check your server logs for the
Rate limit exceededwarning.
Example curl loop (on Linux/macOS):
for i in $(seq 1 101); do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/v1/health; done
(Adjust 101 to be RATE_LIMIT_MAX_REQUESTS + 1)
The first RATE_LIMIT_MAX_REQUESTS requests should return 200, and subsequent requests within the window should return 429.
2. Implementing CORS (Cross-Origin Resource Sharing)
CORS is a security feature implemented by web browsers that restricts web pages from making requests to a different domain than the one that served the web page. This prevents malicious scripts from making unauthorized requests to your API. We’ll use @fastify/cors.
a) Setup/Configuration
Install the plugin:
npm install @fastify/cors
npm install --save-dev @types/fastify__cors # If using TypeScript
Now, create a plugin for CORS.
File: src/plugins/cors.ts
import fp from 'fastify-plugin';
import cors from '@fastify/cors';
import { FastifyInstance } from 'fastify';
import { AppConfig } from '../config';
/**
* @fastify/cors plugin for Fastify.
* Configures Cross-Origin Resource Sharing (CORS) for the application.
*/
export default fp(async (fastify: FastifyInstance) => {
const { ALLOWED_ORIGINS, NODE_ENV } = AppConfig;
// Split comma-separated origins, trim whitespace, and filter out empty strings
const origins = ALLOWED_ORIGINS.split(',').map(o => o.trim()).filter(o => o);
fastify.register(cors, {
origin: (origin, callback) => {
// Allow requests with no origin (like mobile apps or curl requests)
if (!origin) {
callback(null, true);
return;
}
// In development, allow all origins for easier testing
if (NODE_ENV === 'development') {
callback(null, true);
return;
}
// In production, check against the list of allowed origins
if (origins.includes(origin)) {
callback(null, true);
} else {
fastify.log.warn(`CORS: Origin ${origin} not allowed.`);
callback(new Error('Not allowed by CORS'), false);
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], // Allowed HTTP methods
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization', 'Refresh-Token'], // Headers allowed in requests
credentials: true, // Allow cookies, authorization headers, etc. to be sent
});
fastify.log.info(`CORS enabled. Allowed origins: ${NODE_ENV === 'development' ? 'ALL (development mode)' : origins.join(', ')}`);
});
We need to add ALLOWED_ORIGINS to our src/config.ts and .env files.
File: src/config.ts (add to AppConfig interface and AppConfig object)
// ... existing imports and interfaces
interface IAppConfig {
// ... existing fields
ALLOWED_ORIGINS: string;
}
export const AppConfig: IAppConfig = {
// ... existing fields
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || 'http://localhost:4200,http://localhost:3000', // Default for development
// ... other fields
};
File: .env (add these lines)
# ... existing environment variables
ALLOWED_ORIGINS=http://localhost:4200,http://localhost:3000,https://your-production-frontend.com
Finally, register the CORS plugin in our main application file. Ensure it’s registered early, typically before other route handlers or authentication.
File: src/app.ts (add import and registration)
// ... existing imports
import corsPlugin from './plugins/cors'; // Add this import
// ... inside the buildApp function
export function buildApp() {
const fastify = Fastify({
logger: PinoLogger,
disableRequestLogging: true,
});
// Register plugins
fastify.register(SwaggerPlugin);
fastify.register(SensiblePlugin);
fastify.register(HelmetPlugin);
fastify.register(corsPlugin); // Register CORS plugin here, ideally before rate limit
fastify.register(rateLimitPlugin);
// ... other plugins
b) Core Implementation Explanation
origin: This is the most critical part.- In
developmentmode, we allow all origins (callback(null, true)) for ease of development. Never do this in production. - In
production, we parse a comma-separated list of allowed origins fromALLOWED_ORIGINSenvironment variable. Theoriginfunction then checks if the incoming request’s origin is in this list. !origin: Allows requests without anOriginheader (e.g.,curlor server-to-server requests).
- In
methods: Specifies which HTTP methods are allowed for cross-origin requests.allowedHeaders: Lists headers that can be used in the actual request. Important for sendingAuthorizationheaders with JWTs.credentials: true: This is vital if your frontend needs to send cookies (e.g.,HttpOnlyrefresh tokens) orAuthorizationheaders. It signals to the browser that the actual request can include user credentials.
c) Testing This Component
- Start your Fastify application:
npm run dev - Test an allowed origin:
- Use
curlwith anOriginheader from yourALLOWED_ORIGINSlist:curl -H "Origin: http://localhost:4200" http://localhost:3000/api/v1/health -v - You should see
Access-Control-Allow-Origin: http://localhost:4200(or*in dev) in the response headers.
- Use
- Test a disallowed origin (in production mode or with a specific origin list):
- Change
NODE_ENVtoproductionin your.envfor this test, or ensurehttp://some-bad-domain.comis not inALLOWED_ORIGINS. curl -H "Origin: http://some-bad-domain.com" http://localhost:3000/api/v1/health -v- You should see a CORS error in your browser console if using a frontend, or potentially no
Access-Control-Allow-Originheader from the server and a403or similar error from the client’s perspective (thoughcurlwon’t enforce browser CORS). The server logs should showCORS: Origin ... not allowed.
- Change
3. Implementing RBAC (Role-Based Access Control)
RBAC ensures that authenticated users can only access resources and perform actions that are permitted by their assigned roles. We’ll create a Fastify decorator that checks for required roles on specific routes.
a) Setup/Configuration
We’ll assume that our request.user object (set by the authentication middleware from previous chapters) contains a roles array (e.g., ['ADMIN', 'USER']).
Let’s define our roles centrally.
File: src/constants/roles.ts
export enum UserRole {
ADMIN = 'ADMIN',
EDITOR = 'EDITOR',
USER = 'USER',
GUEST = 'GUEST', // For unauthenticated but known users, if applicable
}
Now, create a Fastify plugin/decorator for RBAC.
File: src/plugins/rbac.ts
import fp from 'fastify-plugin';
import { FastifyInstance, FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
import { UserRole } from '../constants/roles'; // Import our roles
// Extend FastifyRequest to include the user property from authentication
declare module 'fastify' {
interface FastifyRequest {
user?: {
id: string;
email: string;
roles: UserRole[]; // Assumes roles are an array of UserRole
};
}
}
/**
* RBAC (Role-Based Access Control) plugin for Fastify.
* Provides a decorator `hasRole` to enforce role-based access on routes.
*/
export default fp(async (fastify: FastifyInstance) => {
/**
* Decorator function to check if the authenticated user has any of the required roles.
* @param requiredRoles An array of roles that are allowed to access the route.
* @returns A preHandler hook function.
*/
fastify.decorate('hasRole', function (requiredRoles: UserRole[]) {
return async (request: FastifyRequest, reply: FastifyReply) => {
// 1. Ensure user is authenticated
if (!request.user) {
fastify.log.warn(`RBAC: Unauthorized access attempt to ${request.url} - no user context.`);
throw fastify.httpErrors.unauthorized('Authentication required to access this resource.');
}
// 2. Check if user has any of the required roles
const userRoles = request.user.roles || [];
const hasPermission = requiredRoles.some(role => userRoles.includes(role));
if (!hasPermission) {
fastify.log.warn(`RBAC: Forbidden access attempt by user ${request.user.id} (roles: ${userRoles.join(', ')}) to ${request.url}. Required roles: ${requiredRoles.join(', ')}`);
throw fastify.httpErrors.forbidden('You do not have the necessary permissions to access this resource.');
}
// If checks pass, continue to the route handler
};
});
fastify.log.info('RBAC plugin loaded. `hasRole` decorator available.');
});
Now, register this RBAC plugin in src/app.ts. It should be registered after authentication, as it depends on request.user.
File: src/app.ts (add import and registration)
// ... existing imports
import rbacPlugin from './plugins/rbac'; // Add this import
// ... inside the buildApp function
export function buildApp() {
const fastify = Fastify({
logger: PinoLogger,
disableRequestLogging: true,
});
// Register plugins (order matters for some)
fastify.register(SwaggerPlugin);
fastify.register(SensiblePlugin);
fastify.register(HelmetPlugin);
fastify.register(corsPlugin);
fastify.register(rateLimitPlugin);
fastify.register(AuthPlugin); // Assuming this sets request.user
fastify.register(rbacPlugin); // Register RBAC AFTER AuthPlugin
// ... other plugins
b) Core Implementation Explanation
declare module 'fastify': This is crucial for TypeScript. It tells TypeScript that theFastifyRequestinterface now has an optionaluserproperty, which our authentication plugin populates.fastify.decorate('hasRole', ...): We’re using Fastify’s decorator pattern to add a new utility function,hasRole, to thefastifyinstance. This function returns apreHandlerhook.hasRole(requiredRoles: UserRole[]): This function takes an array of roles that are allowed to access the current route.- Authentication Check:
if (!request.user)ensures that the user is authenticated before checking roles. If not, it throws anunauthorizederror. - Role Check:
requiredRoles.some(role => userRoles.includes(role))checks if the user’s roles array contains at least one of therequiredRoles. This implies an OR condition (e.g., ifrequiredRolesis[ADMIN, EDITOR], a user withADMINrole orEDITORrole can access). - Forbidden Error: If no matching role is found, it throws a
forbiddenerror, providing clear feedback to the client and logging the attempt.
c) Applying RBAC to Routes
Now, let’s update some of our routes to use this hasRole decorator. For example, let’s imagine an admin endpoint.
First, ensure your AuthPlugin (from Chapter 9/10) correctly populates request.user.roles. For demonstration, let’s assume AuthPlugin is properly parsing JWTs and setting request.user.
File: src/routes/user.routes.ts (Example modification)
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { UserRole } from '../constants/roles'; // Import UserRole
// Extend FastifyInstance to include the hasRole decorator
declare module 'fastify' {
interface FastifyInstance {
hasRole: (requiredRoles: UserRole[]) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
}
}
export async function userRoutes(fastify: FastifyInstance) {
// Example: Get current user profile (requires USER role or higher, e.g., ADMIN)
fastify.get('/profile', {
preHandler: [fastify.authenticate, fastify.hasRole([UserRole.ADMIN, UserRole.USER])],
handler: async (request: FastifyRequest, reply: FastifyReply) => {
// request.user is guaranteed to exist here due to fastify.authenticate
fastify.log.info(`User ${request.user!.id} accessed profile.`);
return reply.send({ user: request.user });
},
schema: {
tags: ['User'],
summary: 'Get current user profile',
security: [{ bearerAuth: [] }],
response: {
200: {
type: 'object',
properties: {
user: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
roles: { type: 'array', items: { type: 'string', enum: Object.values(UserRole) } },
},
},
},
},
401: fastify.getSchema('401'), // Unauthorized
403: fastify.getSchema('403'), // Forbidden
},
},
});
// Example: Admin-only endpoint to list all users
fastify.get('/admin/users', {
preHandler: [fastify.authenticate, fastify.hasRole([UserRole.ADMIN])], // Only ADMIN can access
handler: async (request: FastifyRequest, reply: FastifyReply) => {
// In a real app, you'd fetch users from the database
fastify.log.info(`Admin user ${request.user!.id} listing all users.`);
return reply.send({ message: 'List of all users (admin-only data)' });
},
schema: {
tags: ['Admin'],
summary: 'List all users (Admin only)',
security: [{ bearerAuth: [] }],
response: {
200: {
type: 'object',
properties: {
message: { type: 'string' },
},
},
401: fastify.getSchema('401'),
403: fastify.getSchema('403'),
},
},
});
}
d) Testing This Component
- Ensure your
AuthPluginsets roles correctly. For testing, you might temporarily hardcode roles in your JWT payload or during user creation.- Create a user with
ADMINrole (e.g., during seeding or by manual DB update). - Create a user with
USERrole.
- Create a user with
- Start your Fastify application:
npm run dev - Test
/api/v1/user/profile:- Log in as a
USER. Get the access token. - Make a
GETrequest to/api/v1/user/profilewith theAuthorization: Bearer <token>header. It should return200 OKwith the user profile. - Log in as an
ADMIN. Get the access token. - Make a
GETrequest to/api/v1/user/profilewith theAuthorization: Bearer <token>header. It should also return200 OK.
- Log in as a
- Test
/api/v1/user/admin/users:- Log in as a
USER. Get the access token. - Make a
GETrequest to/api/v1/user/admin/userswith theAuthorization: Bearer <token>header. It should return403 Forbiddenwith the message “You do not have the necessary permissions…”. - Check server logs for the
RBAC: Forbidden access attemptwarning. - Log in as an
ADMIN. Get the access token. - Make a
GETrequest to/api/v1/user/admin/userswith theAuthorization: Bearer <token>header. It should return200 OKwith the admin message.
- Log in as a
Production Considerations
- Rate Limiting:
- Distributed Stores: For multi-instance deployments (e.g., AWS ECS), use a shared store like Redis for rate limiting to ensure consistent limits across all instances.
@fastify/rate-limit-redisis a good option. - Configuration: Tune
maxandtimeWindowbased on expected traffic and abuse patterns. Different endpoints might need different limits. - Monitoring: Monitor
429responses and rate limit logs to detect attacks or misconfigured clients.
- Distributed Stores: For multi-instance deployments (e.g., AWS ECS), use a shared store like Redis for rate limiting to ensure consistent limits across all instances.
- CORS:
- Strict Origins: In production,
ALLOWED_ORIGINSmust be strictly defined to only include your legitimate frontend domains. Avoid*at all costs. - Pre-flight Caching: Browsers cache pre-flight
OPTIONSresponses. SetAccess-Control-Max-Ageheader viafastify-corsoptions to improve performance by reducingOPTIONSrequests.
- Strict Origins: In production,
- RBAC:
- Granularity: For complex applications, consider more granular permissions (e.g.,
canCreateUser,canDeleteProduct) rather than just roles. Roles can then be collections of permissions. - Performance: RBAC checks add a small overhead. Ensure your role storage and retrieval are efficient.
- Auditing: Log all attempts at unauthorized access for security auditing and incident response.
- Data Consistency: Ensure roles are consistently managed across your user management system and database.
- Granularity: For complex applications, consider more granular permissions (e.g.,
Code Review Checkpoint
At this point, your application has significantly enhanced security features.
Files Created/Modified:
src/plugins/rate-limit.ts: Implemented global rate limiting.src/plugins/cors.ts: Configured CORS for secure cross-origin communication.src/plugins/rbac.ts: Developed a custom RBAC decorator for role-based access control.src/constants/roles.ts: DefinedUserRoleenum.src/config.ts: Added environment variables for rate limit and CORS origins..env: Updated with new environment variables.src/app.ts: Registered the new plugins.src/routes/user.routes.ts: Modified to apply RBAC to specific routes.package.json/package-lock.json: Added@fastify/rate-limit,@fastify/corsand their types.
Integration:
- Rate limiting is applied globally to all requests, protecting the API from excessive traffic.
- CORS is configured to allow only specified origins to interact with the API, enhancing browser-based security.
- RBAC decorator (
fastify.hasRole) is available for use in any routepreHandlerchain, ensuring that only users with appropriate roles can access sensitive endpoints. This relies on therequest.userobject being populated by the authentication plugin.
Common Issues & Solutions
Issue: “Too Many Requests” (429) even with few requests.
- Cause: The
RATE_LIMIT_MAX_REQUESTSorRATE_LIMIT_TIME_WINDOW_MSmight be set too low, or you’re testing with akeyGeneratorthat isn’t distinguishing users/IPs correctly (e.g., behind a proxy withoutx-forwarded-for). - Solution:
- Adjust
RATE_LIMIT_MAX_REQUESTSandRATE_LIMIT_TIME_WINDOW_MSin.envto reasonable values. - If behind a proxy, ensure
request.headers['x-forwarded-for']is correctly used inkeyGeneratorand your proxy is configured to pass the real IP. - Check Fastify logs for rate limit warnings; they often contain the IP that triggered it.
- Adjust
- Cause: The
Issue: CORS error in browser console: “No ‘Access-Control-Allow-Origin’ header is present…”
- Cause: Your frontend’s origin is not included in
ALLOWED_ORIGINSin your.envfile (for production builds) or yourcorsPluginlogic is incorrect. - Solution:
- Verify that
ALLOWED_ORIGINSin your.envfile (and consequentlyAppConfig.ALLOWED_ORIGINS) explicitly lists the full URL of your frontend application (e.g.,http://localhost:4200orhttps://your-app.com). - Ensure
credentials: trueis set in the CORS plugin options if your frontend needs to sendAuthorizationheaders or cookies. - Check server logs for
CORS: Origin ... not allowed.warnings. - Ensure the
corsPluginis registered early insrc/app.ts.
- Verify that
- Cause: Your frontend’s origin is not included in
Issue:
403 Forbiddenwhen trying to access an RBAC-protected route, even with an authenticated user.- Cause: The authenticated user’s
request.user.rolesarray does not contain any of therequiredRolesspecified for the route, orrequest.useris not being populated correctly by your authentication plugin. - Solution:
- Verify
request.user: Temporarily logrequest.userin yourauthenticateplugin or directly in the RBACpreHandlerto ensure it contains the expectedrolesarray. - Check
requiredRoles: Double-check theUserRoleenum values and ensure the roles specified infastify.hasRole([UserRole.ADMIN])match the actual roles assigned to your test user. - User Data: Confirm that your user creation or seeding process assigns the correct roles to users in the database.
- Order of Plugins: Ensure
AuthPluginis registered beforerbacPlugininsrc/app.ts.
- Verify
- Cause: The authenticated user’s
Testing & Verification
- Start the Application:
npm run dev - Health Check: Access
GET /api/v1/health.- Verify rate limiting: Make rapid requests. The first few should pass (200), subsequent ones should fail with
429 Too Many Requests. - Verify CORS (using
curlwithOriginheader): An allowed origin should receiveAccess-Control-Allow-Originin headers. A disallowed origin (ifNODE_ENV=production) should not.
- Verify rate limiting: Make rapid requests. The first few should pass (200), subsequent ones should fail with
- User Authentication & RBAC:
- Register/Login as a
USER: Obtain JWT access token. - Access User Profile (
GET /api/v1/user/profile):- Using the
USERtoken: Should return200 OK. - Using an invalid/expired token: Should return
401 Unauthorized.
- Using the
- Access Admin Endpoint (
GET /api/v1/user/admin/users):- Using the
USERtoken: Should return403 Forbidden.
- Using the
- Register/Login as an
ADMIN: Obtain JWT access token. - Access Admin Endpoint (
GET /api/v1/user/admin/users):- Using the
ADMINtoken: Should return200 OK.
- Using the
- Register/Login as a
- Logging: Review your application logs (
npm run devoutput) to ensure warnings for rate limit hits, CORS rejections, and forbidden RBAC access attempts are correctly recorded.
Summary & Next Steps
In this chapter, we significantly strengthened our Node.js Fastify API’s security posture. We successfully implemented:
- Rate Limiting: To protect against abuse and DoS attacks.
- CORS: To enable secure communication with specific frontend applications.
- RBAC: To enforce granular access control based on user roles, ensuring users only access authorized resources.
These features are foundational for building robust and secure web applications and are critical for production environments.
In the next chapter, Chapter 12: File Upload & Static File Serving, we’ll explore how to handle file uploads securely and efficiently, and how to serve static assets directly from our Fastify application. This is a common requirement for many applications, from user profile pictures to document storage.