Securing the APISecurity · NestJS

Everything from the course on one service, in the order the request meets it: helmet's headers and CORS at the door, the throttler, then who is asking, then what they may do, with passwords hashed and a health check that is never counted.

What you will learn

Read the theory for Securing the API

All Security lessons

All NestJS courses

loading types…

What you'll learn

  • Place every layer from the course in the request pipeline and say what each one answers, and before which other
  • Wire helmet, CORS, the throttler, the authentication and roles guards, and hashed passwords into one service from memory of the lessons
  • Read a response and tell which layer produced it: the headers, a 429, a 401, a 403
  • List what a real deployment still needs that the browser cannot show: secrets from configuration, TLS, a proxy's addresses, shared rate-limit storage

Securing the API

Nine lessons added nine things to the cats API, one at a time, each on its own copy. A real service has all of them at once, and the question that decides whether they work together is order: which layer meets the request first, and which answer wins when two of them would refuse. This lesson is the assembly. There is nothing new to learn about any one layer; there is the whole to see.

The pipeline, with every layer in it

A request to the finished service meets, in this order:

  1. helmet (lesson 6), middleware: it sets the security headers on the response object before anything else runs, so every answer below carries them, a 404, a 429, a 401.
  2. CORS (lesson 7), middleware: it reads Origin, writes the Access-Control headers for an allowed one, and answers a preflight OPTIONS by itself, so a preflight never reaches a guard. Also on every answer below.
  3. ThrottlerGuard (lesson 9), a global guard: it counts the request against the route and the caller and answers 429 past the limit. It runs before authentication on purpose: a password-guessing script must be stopped before its guesses are checked.
  4. AuthGuard (lesson 1): a @Public() route passes; otherwise a valid bearer token or 401.
  5. RolesGuard (lesson 3): a route with @Roles() needs one of them on request.user, or 403. It runs after AuthGuard because it reads what AuthGuard left.
  6. The handler, and under it the service that hashes passwords (lesson 2) and encrypts what must be read back.

Read a response and you can name the layer: headers alone say helmet ran; Access-Control-Allow-Origin says CORS allowed the origin; a 429 stopped at the throttler; a 401 at authentication; a 403 at authorization; anything else reached the code. Two rules of order matter most. Global guards run in the order their APP_GUARD providers appear, module by module, so the throttler in AppModule precedes the two in AuthModule, and within AuthModule authentication precedes roles. And middleware precedes guards, always, which is why headers appear on responses the guards refused.

What is not here, and why: CSRF protection (lesson 8) is for credentials the browser attaches on its own. This service takes a bearer token in a header, so a foreign page cannot make a browser send one, and the middleware would only get in the way. Policies (lesson 4) and Passport (lesson 5) are alternatives to two layers that are here; a service picks one of each.

What the browser cannot show

Five things a real deployment adds, none of which changes the code above:

  • Secrets from configuration. constants.ts holds the JWT secret and the encryption password in source, with the docs' own warning attached. Techniques lesson 1 showed ConfigModule and .env; in production they come from the environment or a secret store, and rotating one is a deploy, not a commit.
  • TLS. Every token, cookie and password in this course travelled over plain HTTP inside the browser. helmet's Strict-Transport-Security header is only honoured once the service is reached over HTTPS, which is the proxy's or the platform's job.
  • The proxy's addresses. Behind a load balancer every request arrives from the same IP, and the throttler counts them all as one caller. trust proxy makes req.ip read X-Forwarded-For, and only from a proxy you control.
  • Shared rate-limit storage. The throttler's counters are in one process's memory. Two instances each grant the full limit; the docs' Redis storage makes the count one.
  • Token lifetime. Sixty seconds was for the lessons. Real access tokens live minutes, refresh tokens days, and a refresh route that issues new access tokens against a stored, revocable refresh token is the usual next step.

Your task

Assemble the service. Each TODO names the lesson it comes from.

  1. main.ts: helmet with the CDN image source, then CORS for the shelter's sites.
  2. app.module.ts: the throttler at twenty requests a minute, counted by a global guard; app.controller.ts: the health check never counted.
  3. auth/auth.controller.ts: two sign-in attempts a minute.
  4. auth/auth.service.ts: registration stores a bcrypt hash.
  5. auth/auth.module.ts: the roles guard, bound globally after the authentication guard; cats/cats.controller.ts: creating and deleting cats need the admin role.

Run after each step and watch the endpoint list: each layer turns a different set of rows green.

When it fails

  • The third sign-in is 200: the throttler is not bound, or the login route has no @Throttle() and shares the twenty-a-minute ceiling.
  • A 429 has no nosniff header, or a preflight is 401: something is registered in the wrong order; middleware belongs before listen, and a guard cannot answer a preflight that CORS should have.
  • Registration is a 500 saying the store takes hashes only: step 4.
  • Ada can add cats: RolesGuard is not bound, or @Roles(Role.Admin) is missing; nobody can, and the answer is 500: the roles guard is bound before the authentication guard and reads an undefined user.
  • The site gets no Access-Control-Allow-Origin: CORS is not enabled, or the origin list does not match.

Remember

  • Middleware (helmet, CORS) before guards; among guards, throttler, then authentication, then authorization.
  • The status code names the layer: 429, 401, 403; the headers say the middleware ran.
  • CSRF protection is for cookie credentials; a bearer-token API does not need it.
  • Production adds secrets from configuration, TLS, a trusted proxy, shared counters and longer-lived tokens with refresh, not different code.
Stuck? Show a hint

main.ts: app.use(helmet({ contentSecurityPolicy: { directives: { imgSrc: ["'self'", 'data:', 'cdn.shelter.example'] } } })); app.enableCors({ origin: ['https://shelter.example', /\.shelter\.example$/], methods: ['GET', 'POST', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 600 }). app.module.ts: ThrottlerModule.forRoot([{ ttl: 60000, limit: 20 }]) and APP_GUARD ThrottlerGuard. app.controller.ts: @SkipThrottle(). auth.controller.ts: @Throttle({ default: { limit: 2, ttl: 60000 } }) on login. auth.service.ts: bcrypt.hash(password, 10). auth.module.ts: a second APP_GUARD, RolesGuard. cats.controller.ts: @Roles(Role.Admin) on create and remove.