Skip to content

Security

Security in nestrs splits into two concerns wired by two composable layers. Authentication establishes who the caller is. Authorization declares what they may do, then enforces it on every read, every by-id write, and every response body before it leaves the process. Bind the two guards on the controller and the framework does the rest — there is no per-handler checklist.

Here for the first auth task? Add login and protect a route walks the whole happy path — issue a JWT on POST /login, bind the two guards, curl the round-trip — before any of the reference below.

The authn layer leans on jsonwebtoken, argon2, and the oauth2 crate. The authz layer is the framework’s own CASL-style engine, with row-level filtering bridged into SeaORM.

ConcernLayerWho answers it
Who is calling?AuthenticationA Strategy turns the request into a principal (or returns a 401 / OAuth redirect)
What may they do?AuthorizationAn Ability declares allowed actions; the framework applies it to queries, by-id loads, and response bodies

Each layer ships its own guard. Bind them in order — the authn guard runs first and attaches the principal; the authz guard reads it and installs the ambient Ability.

crates/features/src/users/http/controller.rs
#[controller(path = "/users")]
#[use_guards(AuthGuard, AuthzGuard)]
pub struct UsersController {
#[inject]
svc: Arc<UsersService>,
}

That’s the whole opt-in surface for HTTP. GraphQL and WebSockets get matching bridges that re-establish the ambient ability at the dispatch point — see Per-transport bridges.

  • Every read is filtered. The Repo gateway every service uses joins the ambient ability’s condition_for into the SQL — a member querying Users::find() gets WHERE org_id = $caller_org_id appended.
  • Every by-id write checks access. Bind<S, A> calls Service::access(id); missing rows return 404, denied rows return 403 — see By-id binding.
  • Every response is masked. The Authorize shaper runs after a 2xx handler: fields the caller cannot read are stripped from the body, and retain_wire_keys drops anything outside the wire DTO so an unrestricted grant cannot leak an unexposed column.

The wiring sketch for an app that serves all three transports:

apps/api/src/module.rs
#[module(
imports = [
DatabaseModule::for_root(None),
HttpModule::for_root(None),
GraphqlModule::for_root(None),
AuthnModule,
AuthzHttpModule,
AuthzGraphqlModule,
AuthzWsModule,
UsersHttpModule,
UsersGraphqlModule,
UsersWsModule,
],
)]
pub struct ApiModule;
  • Threat model — what these layers cover and what they don’t (explanation — read before going to production, not needed for your first feature).
  • Guards — the primitive every security layer builds on.
  • Throttler — rate limiting as a guard, composes with AuthGuard.