How To Build A Twitter Clone With NestJS, Prisma And React ( Part 2 )
- Part 1: ( Setup & first steps )
- Part 2: Authentication ( this article )
- Part 3 Coming Soon
Authentication
There are a lot of different authentication strategies to protect our API endpoints.
Generally, I strongly suggest to delegate such a crucial feature to a dedicated service such as Firebase Authentication, AWS Cognito or Auth0. However, today we're going to build a basic and incomplete authentication system to understand how Nest approaches the problem.
Let me say that again: this is not a complete solution, it's far from being secure and production-ready since it lacks a lot of essential features for a good authentication system. We just want to explore the possibilities Nest gives us to implement authentication in our server and how it can integrate existing solutions.
The authentication system we are going to build is based on JSON Web Tokens ( JWT ). Those are essentially a standard and secure way to transmit information over the network, encrypted and signed by your server to be verified on every request.
The authentication flow is basically this:
- A user will ask for a JWT sending a request to the
auth/login
endpoint with his username and password in the request body. - If that information is correct, the server will generate, encrypt and send back a signed JWT, which will carry the username and will have an expiration time.
- On every subsequent request, the user will send the received JWT in the
Authorization
header, which will be verified by the server. If the token is valid and the expiration time has not passed, the server will proceed to handle the request, and it will know which user made it thanks to the username stored in the JWT.
Sending the access token for every request very much exposes it to man-in-the-middle attacks, that's why this authentication system usually requires a very short token expiration time and a mechanism to refresh the token.
Since this is beyond the scope of this tutorial, we will set an expiration time of one hour, after which the user will need to ask for another token sending his username and password to the auth/login
endpoint again.
To learn more about JWT you can read this well-crafted introduction.
Guards
Nest provides a very versatile element to handle endpoints protection: guards.
A guard is just an Injectable
class which implements the CanActivate
interface. It can be applied to any endpoint or a whole controller class.
Guards do not enforce a particular authentication strategy, they are just used to tell Nest to run some code before the request gets passed to the handler method.
To implement our first guard let's first generate the auth
module.
nest generate module auth
nest generate service auth
We can now generate the guard in the same module.
nest generate guard auth/simple
Let's take a look at the generated file.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class SimpleGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
As you can see the only thing we need here is the canActivate
method.
When this guard is applied to an endpoint or a controller, Nest calls the canActivate
method before every request, and, based on its boolean
return value, it either passes the request to the controller or returns a 403 Forbidden
response. Of course, we can throw any other exception and it will be caught and send back to the client.
The most powerful feature of this method is that it can access the request object, thanks to its context
argument.
Let's update this guard to check the presence of an MY_AUTH_TOKEN
string in the Authorization
header.
// ...
export class SimpleGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const req: Request = context.switchToHttp().getRequest();
const token = req.headers['authorization'];
if (!token) {
throw new UnauthorizedException('token_not_found');
}
if (token !== 'MY_AUTH_TOKEN') {
throw new UnauthorizedException('invalid_token');
}
return true;
}
}
To apply this guard to an endpoint or a controller we can use the UseGuards
decorator. Let's do that with the getHello
method in the AppController
.
// src/app.controller.ts
import {
// ...
UseGuards,
} from '@nestjs/common';
import { SimpleGuard } from './auth/simple.guard';
// ...
@Controller()
export class AppController {
// ...
@UseGuards(SimpleGuard)
@Get('hello')
getHello(): string {
return this.appService.getHello();
}
}
Let's test this out.
http localhost:3000/hello
HTTP/1.1 401 Unauthorized
{
"error": "Unauthorized",
"message": "token_not_found",
"statusCode": 401
}
http localhost:3000/hello Authorization:"INVALID_TOKEN"
HTTP/1.1 401 Unauthorized
{
"error": "Unauthorized",
"message": "invalid_token",
"statusCode": 401
}
http localhost:3000/hello Authorization:"MY_AUTH_TOKEN"
HTTP/1.1 200 OK
Hello World!
We now know what a guard is and how to use it.
However, to implement our authentication system we are not going to write a guard, and that's because someone already wrote one for us.
Passport
Nest provides us an additional module to integrate with passport, the most popular and mature NodeJS authentication library.
Passport acts as a toolset capable of handling a lot of different authentication strategies. The key to make it work in a Nest application is, once again, to encapsulate the one we need in an injectable service. Once we do that, we can use a built-in guard exported by the @nestjs/passport
library to let passport do its work for every incoming request.
Let's install everything we need.
npm install @nestjs/passport passport @nestjs/jwt passport-jwt
npm install @types/passport-jwt --save-dev
As you can see, we also installed @nestjs/jwt
, which is a utility package to manipulate JWTs, thanks to the jsonwebtoken library which it encapsulates.
We will now need some JWT configuration constants which we can store in the auth/jwt.constants.ts
file.
export const jwtConstants = {
secret: 'secretKey',
};
The secret
field is going to be used by passport to sign and verify every generated JWT. We usually want to provide a more robust and complicated secret.
Next, we are going to import the PassportModule
and JwtModule
provided by the @nestjs/passport
and @nestjs/jwt
packages in our AuthModule
's imports
.
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { jwtConstants } from './jwt.constants';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService],
})
export class AuthModule {}
The JwtModule.register
is a sort of factory to allow us to provide some configuration to the JwtModule
. This technique is pretty frequent in the NestJS world, and we refer to it as dynamic modules.
To be able to access the database in the AuthService
we now need to import our PrismaService
in the AuthModule.providers
field.
// ...
import { PrismaService } from '../prisma.service';
// ...
@Module({
// ...
providers: [AuthService, PrismaService],
// ...
Next, we will create an auth.dto.ts
file with a LoginDto
class and an AuthResponse
, and in our AuthService
class we will implement the login
method.
This method will then:
- Check if a user with the provided username really exists.
- Validate the password using the bcrypt library, comparing it with the hash in our database.
- Generate and return a signed JWT along with the user object.
// auth.dto.ts
import { IsString, Length } from 'class-validator';
import { User } from '@prisma/client';
export class LoginDto {
@IsString()
@Length(3, 30)
username: string;
@IsString()
@Length(6, 30)
password: string;
}
export class AuthResponse {
token: string;
user: User;
}
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma.service';
import { LoginDto } from './auth.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private db: PrismaService, private jwt: JwtService) {}
async login(data: LoginDto): Promise<AuthResponse> {
const { username, password } = data;
const user = await this.db.user.findOne({
where: { username },
});
if (!user) {
throw new NotFoundException();
}
const passwordValid = await bcrypt.compare(password, user.password);
if (!passwordValid) {
throw new UnauthorizedException('invalid_password');
}
delete user.password;
return {
token: this.jwt.sign({ username }),
user,
};
}
}
Everything here is pretty clear. Notice how we asked Nest to inject the JwtService
from the @nestjs/jwt
package to be used inside our class.
This is only possible because the JwtService
is an exported provider in the JwtModule
we imported in the AuthModule
. We'll see how this mechanism works with a local module later on.
We can now generate our auth controller and implement the auth/login
endpoint.
nest generate controller auth
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto, AuthResponse } from './auth.dto';
@Controller('auth')
export class AuthController {
constructor(private service: AuthService) {}
@Post('login')
login(@Body() data: LoginDto): Promise<AuthResponse> {
return this.service.login(data);
}
}
Let's test this out:
http POST localhost:3000/auth/login username="jack" password="invalid"
HTTP/1.1 401 Unauthorized
{
"error": "Unauthorized",
"message": "invalid password",
"statusCode": 401
}
http POST localhost:3000/auth/login username="jack" password="123456"
HTTP/1.1 201 Created
{
"token": "<a very long token>",
"user": {
"username": "jack",
"displayName": "Jack"
}
}
It definitely seems to work.
We now need to implement a strategy, extending the default one exported by passport-jwt
, which will make passport able to verify the JWT on every request.
Let's create the auth/jwt.strategy.ts
file.
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './jwt.constants';
import { PrismaService } from '../prisma.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private db: PrismaService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: { username: string }) {
const user = await this.db.user.findOne({
where: { username: payload.username },
});
return user;
}
}
Let's analyze what we're doing here:
- We're creating an injectable class extending the passport strategy exported from
passport-jwt
and wrapped by thePassportStragey
utility function exported by@nestjs/passport
. - We're passing some configuration data to the strategy constructor, and injecting the
PrismaService
at the same time. - The
validate
method will only be called by passport when a valid JWT has been found in theAuthorization
header. The return value of this method will be attached to therequest
object by passport, and will be accessible in every controller handler asrequest.user
. Therefore we just need to fetch the user from the database and return it.
We can now add this new strategy class to the providers
list of the AuthModule
.
// auth.module.ts
// ..
import { JwtStrategy } from './jwt.strategy';
@Module({
// ...
providers: [AuthService, PrismaService, JwtStrategy],
// ...
We are now ready to apply our JWT authentication system to our endpoints through a guard.
The @nestjs/passport
module exports a built-in AuthGuard
to be used in our UseGuards
decorator. Let's do that with our UsersController
.
// users.controller.ts
import {
// ...
UseGuards
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@UseGuards(AuthGuard('jwt'))
@Controller('users')
export class UsersController {
// ...
Passing the jwt
string parameter, Nest will look for a provider class anywhere among our application's dependencies which extends the Strategy
exported by the passport-jwt
strategy, and it will find our JwtStrategy
class.
Every endpoint in this controller is now protected. Let's test this out.
http localhost:3000/users/jack
HTTP/1.1 401 Unauthorized
{
"message": "Unauthorized",
"statusCode": 401
}
As we can see, without an authentication token in the Authorization
header we always receive a 401 error. Let's get one with our auth/login
endpoint.
http POST localhost:3000/auth/login username="jack" password="123456"
HTTP/1.1 201 Created
{
"token": "<auth token>",
"user": {...}
}
Just copy the received token and export it in an environment variable like this:
export TOKEN="<your token here>"
You can now use it for every request like this:
http localhost:3000/users/jack Authorization:"Bearer $TOKEN"
HTTP/1.1 200 OK
{
"displayName": "Jack",
"password": "123456",
"username": "jack"
}
Let's now see how we can access the authenticated user in a handler method.
Custom decorators
As we already know, the JwtStrategy
takes care of attaching the result of the validate
function in the request object, which is the user we fetched from the database.
The request object is the same you may know if you ever used the express framework, which Nest is based on and which we got already installed by the Nest CLI.
To access it in a controller method we can use the Req
decorator.
Let's implement a new protected endpoint auth/me
to demonstrate that.
// auth.controller.ts
import {
// ...
Get,
UseGuards,
Req,
} from '@nestjs/common';
import { User } from '@prisma/client';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
// ...
@UseGuards(AuthGuard('jwt'))
@Get('me')
me(@Req() req: Request): User {
const user = req.user as User;
delete user.password;
return user;
}
// ...
And let's test it.
http localhost:3000/auth/me Authorization:"Bearer $TOKEN"
HTTP/1.1 200 OK
{
"displayName": "Jack",
"username": "jack",
}
As we can see there is something pretty disturbing in this implementation.
Every time we need to access the user
object we have to cast it to the right User
type and eventually remove the password
field, which will become annoying as soon as our application grows.
This is a perfect use case for a custom decorator.
Let's create a new file src/common/decorators/auth-user.decorator.ts
.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@prisma/client';
const AuthUser = createParamDecorator((_, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as User;
delete user.password;
return user;
});
export default AuthUser;
While for a simple class or function decorator we could simply use the Typescript syntax, Nest provides us a createParamDecorator
utility specifically for arguments of controllers' handlers.
We provide a function as the only argument, whose second argument is the server ExecutionContext
, from which we can get the request
object.
Now we can replace the Req
decorator with our new AuthUser
decorator in the me
handler.
// auth.controller.ts
// ...
import AuthUser from '../common/decorators/auth-user.decorator';
// ...
@UseGuards(AuthGuard('jwt'))
@Get('me')
me(@AuthUser() user: User): User {
return user;
}
// ...
Custom decorators are a very powerful feature of Nest. More on that in the dedicated page of the Nest documentation.
User registration
The last thing we need to handle is user registration.
Right now is barely implemented in the UsersController
, but we want to properly implement it in the AuthController
as a new auth/register
endpoint.
After the new user has been created we should generate and send back a JWT to let him authenticate on subsequent requests, without the need to call the auth/login
endpoint.
Let's add a new RegisterDto
class to the auth.dto.ts
file, identical to the CreateUserDto
( you can actually copy that ).
// auth.dto.ts
// ...
export class RegisterDto {
@IsString()
@Length(3, 30)
username: string;
@IsString()
@Length(6, 30)
password: string;
@IsString()
@Length(1, 50)
displayName: string;
}
We can now implement our register
method in the AuthService
, and to do that we want to take advantage of the create
method we have in the UsersService
.
This means the UsersModule
has to expose that feature exporting the UsersService
to be used by other modules.
To do that we just need to add an exports
field to the Module
decorator of the UsersModule
, and put the UsersService
inside.
// ...
import { UsersService } from './users.service';
@Module({
// ...
exports: [UsersService],
})
export class UsersModule {}
This way, any other module can import the UsersModule
to take advantage of any of the exported classes.
Let's do that with the AuthModule
.
// ...
import { UsersModule } from '../users/users.module';
@Module({
imports: [
UsersModule,
// ...
],
// ...
})
export class AuthModule {}
Now, thanks to the power of Nest, we can easily inject the UsersService
into the AuthService
and implement our register
method.
import { LoginDto, RegisterDto, AuthResponse } from './auth.dto';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
// ...
private users: UsersService
) {}
// ...
async register(data: RegisterDto): Promise<AuthResponse> {
const user = await this.users.create(data);
return {
token: this.jwt.sign({ username: user.username }),
user,
};
}
}
Let's now wire our new method to the corresponding auth/register
endpoint.
// ...
import { LoginDto, RegisterDto, AuthResponse } from './auth.dto';
@Controller('auth')
export class AuthController {
// ...
@Post('register')
register(@Body() data: RegisterDto): Promise<AuthResponse> {
return this.service.register(data);
}
// ...
}
Finally, we just need to clean everything up removing the create
method from the UsersController
.
Let's test the new auth/register
endpoint.
http POST localhost:3000/auth/register username="mary" displayName="Mary" password="secret"
HTTP/1.1 201 Created
{
"token": "<generated code>",
"user": {
"username": "mary",
"displayName": "Mary"
}
}
export TOKEN="<our new token>"
http localhost:3000/auth/me Authorization:"Bearer $TOKEN"
HTTP/1.1 200 OK
{
"displayName": "Mary",
"username": "mary"
}
We are now ready to implement our main application feature: tweets.