Definitive guide for Nest.js guards and Passport
Hi everybody !
Today, I’m gonna summarize key aspects of implementing guards with Nest.js and Passport. They will be discussed in Express context because… huh well, I haven’t done any Fastify in a couple of years 😅
Please note that if you want to jump straight up to the code and related integration tests, you can have a look at the related sample repo directly :
Prelude
This article is an extension of one I previously wrote here, it assumes you already have a good understanding of Nest.js, Passport, Express and Typescript decorators. If you don’t already, please be sure to have a look at the following resources first.
Is this useful to me ?
If you’re wondering how to :
- use injection in guard / strategy
- implement multiple strategies
- implement multiple guards
Yeah, then this is for you 😃
Let’s dive in ! 💻
First, let’s have a quick refresher on both Guard
and AuthGuard
.
Guard
Lifecycle
In Nest.js, here is the order in which decorators get called :
- Middleware
- Guard
- ValidationPipe
- Controller route handler method
Hence, for example, don’t expect a 400 / Validation failed error with a malformed payload if ever your request does not pass the Guard in the first place.
AuthGuard
Lifecycle
To put it simply, it does :
- extends Guard
- pipe the options to the underlying passport strategy
- call the authorization logic of the underlying passport strategy
- finally call
PassportStrategy
'sverify
method (which, under the hood, callsvalidate
on Nest.js PassportStrategy), allowing you to plug your own additional authorization logic
Multiple strategies
The golden rule to remember : multiple strategies will succeed as long as at least one passes.
Use cases
Typical use case include providing alternate ways to authorize requests.
Example
Let’s implement both a JWT strategy for the Bearer request header and a JWT strategy for the request query (which will retrieve the JWT from a token
query parameter) :
As you can notice, the only thing that differ between these 2 classes is the jwtFromRequest
option that is used with passport-jwt
strategy and the name given to the strategies (here jwt.bearer
and jwt.query
), so you could of course refactor it in a nicer way.
Then, to use them all you have to do is create the corresponding AuthGuard
:
And use it in your controller :
This way, as long as a valid JWT is sent either in the expected header or query parameter, you user will be allowed by the JwtGuard
.
Multiple guards
The golden rule to remember : multiple guards will succeed as long as all of them passes.
Use cases
Typical use cases include reusing different combinations of Guard(s) on different Controller routes.
Example
Let’s say we have 2 routes, both expect a valid JWT to be provided but one requires the user to be an admin while the other one implement some kind of IP rate limiting.
Of course for rate limiting you would typically use some nice library like nestjs-rate-limiter but for the purpose of this example I will use a dummy homemade implementation (please don’t use this in production).
Let’s first assume we have this controller :
I’m gonna reuse the JwtGuard
from the former example, and add on top of it :
The RoleGuard
is nothing more than the one depicted in Nest.js documentation : it checks that the authenticated user has the required role.
The RateGuard
will save request IP and compare it with previous ones : provided the limit
has been reached, it will prevent the request from completing successfully.
Here again, you can find the details of the associated services and decorators implementation in the sample repo.
Context & injection in guard / strategy
I’ve often seen and wondered myself what I could actually use or inject in AuthGuard
/ PassportStrategy
.
Context
- Guard has access to both
ExecutionContext
andReflector
, so it can retrieve metadata from Controller’s route handler - Strategy can only access
Reflector
but notExecutionContext
Injection
- Strategy can be injected with any service (or provider, as Nest.js also call them)
- Guard is not meant to be injected with any service
A workaround to inject service(s) in a guard it is to use Jinni’ solution but it implies to make both your guard and service(s) global, so I would strongly advice against it.
What if I need both ?
There’s a way :
- retrieve the metadata in the Guard
- store them on the request before triggering strategy from
super.canActivate
- then retrieve them from the request in Strategy’s
validate
and perform your logic with injected services
Seems like a weird solution ?
That’s actually what passport already does with req.user 👈
Example
That’s actually what I’ve done for the RateGuard
:
- the
RATE_LIMIT
is defined on the Controller route
this way a different rate limit could be set for different routes - it is then retrieved in the
RateGuard
and set on the request
request.limit = limit - finally it’s retrieved from the request in the
RateStrategy
and compared to check whether to allow the request or not
I hope it will be useful to you and save you some time digging into the documentation / stackoverflow / source code 😃
Stay safe and take care of your relatives !
Please find the sample repo with integration tests here :