We started using Nest.js as the main framework for our Node + Typescript service development, mainly because it has built-in configurations and ready to use project structure baked in with Typescript integration. Most importantly it contains to build and linting scripts which are quite hard to get right from scratch and it’s just working out of the box there.
At https://treescale.com we have been always focused on having flexible ways of service development so that each service will be independent atomic for better scalability and ease of maintenance. That’s why from the beginning we just implemented JWT token-based auth so that we can embed the most commonly used information about individual HTTP requests and other services can just extract that using our JWT Secret keys. That sounds like a pretty standard way of doing things but still wanted to point out the way we did it and how simple the structure became when we moved everything into Cookies rather than saving in LocalStorage so that our UI now doesn’t send it over Authorization
header. Everything works just by performing all requests to API over browser cookie, which is more secure by the way compared to Local Storage.
Setting up Passport JWT for Nest.js
Nest.js consists of, so-called, atomic modules which are connected to the main application module with the Injectable Class principle. Well, that’s just a fancy way of saying there is a main module that calls all other modules to initialize them and share the same context and set middleware for Express.js which is the actual HTTP handler there. That is quite important to understand that at the end Express.js is the one who handles and processes the request, Nest.js is just a wrapper, which means all of the Express.js libraries are usable with Nest.js, including Passport.js and passport-jwt
library, which is the basis for us here.
As a core entry point for authentication we have a module called auth
which looks like this
// Auth Module
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
import { AuthService } from './auth.service';
@Module({
imports: [
...
PassportModule.register({
session: false,
}),
JwtModule.register({
secret: AppConfig().jwtSecret,
signOptions: { expiresIn: '7d' },
}),
...
],
providers: [
...
AuthService,
JwtStrategy
...
],
controllers: [AuthController],
})
export class AuthModule {}
This is an initialization structure where we have the main PassportModule coming from @nestjs/passportlibrary
that makes all of the middleware attachments and core passport.js logic, similar to what we’ve been doing with Express.js with the passport.use
function, it’s just became more TypeScript like here.
JwtStrategy
on the other hand is something we added as a custom injectable Strategy for Nest.js which does the actual parsing of JWT and registers authenticated users to request context, similar to all other passport.js strategies, but as a Nest.js module to keep the concept of having injectable modules in place.
// JwtStrategy for handling Passport JWT
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
]),
ignoreExpiration: false,
secretOrKey: <providing secrets from configService here>,
});
}
async validate(payload: any) {
// validating payload here
if (<user is authenticated>) {
return <user data here>
}
// return 401 Unauthorized error
return null;
}
}
This is how the basic JWT Strategy looks like for Nest.js, but it is for having JWT token coming from Authorization: Bearer
header, which is OK for most of the applications, especially if you have multiple clients with Mobile apps, BUT for us, it is way more flexible to have a Cookie based authorization as well, which means we are just passing token via cookie without expecting to have Authorization header. which brings us to having custom JWT extractor function like this one
// JwtStrategy for handling Passport JWT
...
...
import { Request as RequestType } from 'express';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
JwtStrategy.extractJWT,
ExtractJwt.fromAuthHeaderAsBearerToken(),
]),
ignoreExpiration: false,
secretOrKey: <providing secrets from configService here>,
});
}
private static extractJWT(req: RequestType): string | null {
if (
req.cookies &&
'token' in req.cookies &&
req.cookies.user_token.length > 0
) {
return req.cookies.token;
}
return null;
}
...
...
}
You can see that we have an array of JwtStrategy.extractJWT
and ExtractJwt.fromAuthHeaderAsBearerToken()
JWT token extractors which means that this will also support having an Authorization header because ExtractJwt.fromExtractors
works in a way that it goes over all handlers and extracts JWT token from the first successful executed handler.
The rest of the logic is the same as a regular JWT Passport strategy, we just check user permissions and token validity inside JwtStrategy.validate
function and return User | null
in case if it is successful or if we want to throw 401 Unauthorized
error. Note that validate function is being called from the Passport module internally, each Nest.js Passport strategy implementation has to have that validate
function, similar to the done
callback that we used to have for Express.js.
Multi-domain and Cookie based JWT Token
Cookie-based authentication systems sometimes could be challenging if you have a multi-domain structure as we have. For example, our main web application is hosted under https://app.treescale.com, API is here https://api.treescale.com and initially, it was unclear how to make cookie setting/getting process flexible for later usage as well especially if we know that some of the browsers don’t support wildcard domain-based access. Quickly we just figured out that we just have to fix some strict rules like
- UI React application shouldn’t read Cookies that are delegated to API, it just has to send an API request and get required information like user authentication status or user data
- For having logout API should fully control its cookie, UI have to just send an API request to remove cookie for signing out the user from browser
That helped to keep things in place, so that now only our API is in charge of reading and writing cookies related to authentication, UI just does communication with API, otherwise, it could become a real mess. Usually, when you have bidirectional read-write access it is very easy to end up with outdated data or even corrupted data, that’s because the cases where things could go wrong growing up significantly.
Signing Out User from API side not from UI
Fixing the rule of keeping the only API in charge for managing authentication cookies makes it easy to understand that even if signing out UI shouldn’t touch Cookie by removing it, which of course could be done obviously, but because API always expects to have a coming cookie, it also has to verify that it is the time to remove authentication cookie.
The endpoint for signing out user is extremely simple like this
@Get('signout')
async logout(@Res({ passthrough: true }) res: ResponseType) {
// Some internal checks
...
...
res.cookie('token', '', { expires: new Date() });
...
}
It removes the token
cookie and considers the User assigned out. It is quite simple, but it also helps to track when the user signs out, or when the cookie just gets expired. As you can imagine it has huge power for having analytics attached to it, like having average user session time or similar.
Conclusion
Nest.js helped us a lot to complete things a lot easier and faster because of this intuitive structure and readable codebase. The way of having atomic modules we’ve been able to support many authentication types without complicating with Passport.js callbacks as we had before Nest.js.
JWT + Cookie works a lot better for us because our UI just sends a simple Axios request without considering to add a custom header or decide how to inject user credentials, the browser just picks up with sending cookies to a relevant https://treescale.com domain name and it is ready to go if the user authenticated. Otherwise, it will just reject things with 401 Unauthorized
errors, which will be easily handled by UI.
Structure for your project will be different of-course if you have a mobile app for example because you don’t have a Cookie there, so that you still have to send JWT Token via standard Authorization header, but even with that, this JwtStrategy Nest.js Injectable class will handle things smoothly.