JWT strategy
Stateless authentication.
We can now proceed to the JWT part in order to conclude authentication. If you are still not familiar with the JWT (JSON Web Token), I recommend this read.
Let's go back to the AuthService
and inject there the JwtService
. It allows for signing the token. Then, add the login()
method, responsible for returning the JWT from the user data.
The identification field in the payload is called
sub
to keep consistency with JWT standards.
However, with this current solution, we'll soon face the same situation of the user
extracted from the request
. That is, the lack of typing. Let's then already solve this. Create the file auth -> interfaces -> jwt-payload.interface.
And apply the type of this interface to the payload
.
Now, we should create environment variables for the JWT: SECRET and TTL (time-to-live). We'll perform that entire process related to creating, typing and using environment variables:
In the .env file, create the variables
JWT_SECRET
andJWT_TTL
It is recommended to use a long and unique secret, like a password from this site.
The TTL may be in ms format, like 7d
(seven days).
Update the .env.example file accordingly
Require the existence of these variables in the
ENV_VALIDATION_SCHEMA
Create a namespace in the file auth -> config -> jwt.config
In the
AuthModule
, add to theimports
theJwtModule
, configuring it with thejwtConfig
What is being done here, is to set the secret, which is needed to sign the token. We're also setting the token's expiration time. And of course, using the JwtService
to create this signature.
In the AuthController
, we can then return the JWT in the login()
route.
Let's just alter this route to return the JWT as a cookie, as it is considered a good practice for better safety. The options passed to the cookie
below increase the security of this process:
secure
: Is sent only over HTTPShttpOnly
: Cannot be accessed by JavaScript code, preventing XSS attackssameSite
: Is sent only for same domain, preventing CSRF attacks (not to be used if Frontend and Backend are in different domains)
The Response
type is from express
.
The passthrough
option allows Nest to keep control of the response
in its pipeline.
In a real-world system, you may need to configure some CORS options to be able to successfully use cookies with those options.
Excellent, we're receiving a JWT when sending correct credentials. It is what will be used to verify whether or not we have permission to access a route. In the JwtModule
configuration, we use the secret
to generate the signature of the token
that is returned. In the next step, we'll once again need the secret
, but this time to verify if the token
is valid when accessing protected routes. Let's then add one more item to the imports
of the AuthModule
.
Now, create the file strategies -> jwt.strategy with following contents. We're performing the same process we did earlier of injecting a configuration namespace, but now instead of doing this inside a dynamic module, we're doing it in a provider.
Strategy
should be imported from passport-jwt
.
Here, we're stating that, when a user tries to access a route that requires authentication, their JWT will be extracted as a Bearer Token, which is a recommended form to send it. And the secret
to be used in the token validation is the same we've already defined. The validate()
method has basically the same return as the one in the LocalStrategy
, because Passport's strategies always append the return of this method to the request
as the user
field.
Now, we should just remember to add the JwtStrategy
to the providers
of the AuthModule
.
However, there is a situation that may appear as a security vulnerability. Imagine that a user account has been deleted. If the token
has not expired yet, it will still be possible to access routes with it. So, let's go back to the AuthService
and create the method validateJwt()
, which checks the existence of the user
to whom a token
is associated.
After that, back in the JwtStrategy
, call validateJwt()
inside the validate()
method. Now, a token
is valid only if its respective user
still exists in the database.
In the next step, we'll use the AuthGuard
of type jwt
. So, let's already create another guard to represent it due to the reason already discussed of avoiding magic strings.
Then, in the AuthController
, let's create the route getProfile()
, which returns the user
that has the same id
of the user
appended to the request
. We shall also protect it with our newly-created guard.
And now create the namesake method in the AuthService
.
Great! Authentication is now functional. However, before proceeding to authorization, there's still one pending issue we should give attention to. Not all routes should require authentication, so we must learn how to have public routes.
Commit - Jwt strategy
Last updated