In this guide, we will build a Nest.js API that shows how to handle Nest.js JWT authentication.
To fully understand and follow this guide, it is important to have:
Node.js runtime installed on your computer.
Some basic knowledge of working with Nest.js
A working MongoDB setup. This can be using locally installed MongoDB or the cloud MongoDB Atlas
To create any Nest.js you need Nest.js Installed. This will allow us to scaffold a basic Nest.js application instead of building everything from scratch.
npm i -g @nestjs/cli
Once the above command is executed, navigate to a folder where you want the Nest.js app to live. Open the folder using a text editor such as VS code. Finally, create Nest.js using the following command:
nest new nest-jwt-api
This will create a nest-jwt-api
and scaffold a basic Nest.js app.
To create this application, we need some Node.js packages. These are:
@nestjs/mongoose
mongoose
@types/bcrypt
bcrypt
@nestjs/jwt
@nestjs/passport
@types/passport-jwt
passport
passport-strategy
passport-local
@types/passport-local
@types/passport-jwt
To install them change the directory to nest-jwt-api
:
cd nest-jwt-api
Then run the following commands to install them:
npm i passport passport-strategy passport-local passport-jwt @nestjs/jwt @types/passport-jwt bcrypt @types/bcrypt mongoose @nestjs/mongoose
npm i --save-dev @types/passport-local @types/passport-jwt
We will use MongoDB to save the user authentication details. Here we will use Mongoose to simplify the database interactions. At this point ensure you have MongoDB up and running. Then create the configuration file to communicate with MongoDB. To create the configuration, navigate to the src/app.module.ts
:
Import Mongoose
import { MongooseModule } from '@nestjs/mongoose';
Set up the MongooseModule inside the module imports as follows:
1 2 3 4
@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost:27017/auth'), ]
Here, ensure you have the MongoDB URI that points to your database. Parameter auth
represents the database that Mongoose will create to save user details.
For Mongoose to create a user, we need a schema that Nest will use to communicate with MongoDB. Therefore, create a users
directory inside the src
directory. Inside the users
directory, create a user.schema.ts file
. Then set up the user schema as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {
Schema,
SchemaFactory,
Prop
} from "@nestjs/mongoose";
import {
Document
} from 'mongoose';
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop({
required: true
})
username: string;
@Prop({
required: true
})
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Next, create a user entity for setting up the user password and email. To this create an user.entity.ts file inside the users directory as follows:
1
2
3
4
export class User {
username: string;
password: string;
}
Finally, create a data object for the above User
. Go ahead and create the create-user.dto.ts
file inside the users directory as follows:
1
2
3
4
5
import {
User
} from './user.entity';
export class CreateUserDto extends User {}
We have the user schema ready. We need to create a service for this schema. Inside the users
directory create a hash.service.ts
as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {
Injectable
} from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class HashService {
async hashPassword(password: string) {
const saltOrRounds = 10;
return await bcrypt.hash(password, saltOrRounds);
}
async comparePassword(password: string, hash) {
return await bcrypt.compare(password, hash)
}
}
The above code will create a hash password to ensure secure application authentication. Now create a service to execute the user.
To do so create the users.service.ts
file inside the users
directory as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import {
Injectable,
BadRequestException
} from '@nestjs/common';
import {
CreateUserDto
} from './create-user.dto';
import {
InjectModel
} from '@nestjs/mongoose';
import {
Model
} from 'mongoose';
import {
HashService
} from './hash.service';
import {
User,
UserDocument
} from './user.schema';
@Injectable()
export class UserService {
constructor(@InjectModel(User.name) private userModel: Model < UserDocument > , private hashService: HashService) {}
async getUserByUsername(username: string) {
return this.userModel.findOne({
username
})
.exec();
}
async registerUser(createUserDto: CreateUserDto) {
// validate DTO
const createUser = new this.userModel(createUserDto);
// check if user exists
const user = await this.getUserByUsername(createUser.username);
if (user) {
throw new BadRequestException();
}
// Hash Password
createUser.password = await this.hashService.hashPassword(createUser.password);
return createUser.save();
}
}
Finally, create a user.controller.ts
file inside the users
directory. This will set up a controller to access the user service using a Nest.js endpoint as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import {
Controller,
Get,
Post,
Body,
Param,
UseGuards
} from '@nestjs/common';
import {
UserService
} from './user.service';
import {
CreateUserDto
} from './create-user.dto';
import {
AuthGuard
} from '@nestjs/passport';
@Controller('regester')
export class UserController {
constructor(private readonly userService: UserService) {}
@UseGuards(AuthGuard('jwt'))
@Get('username')
getUserByUsername(@Param() param) {
return this.userService.getUserByUsername(param.username);
}
@Post()
registerUser(@Body() createUserDto: CreateUserDto) {
return this.userService.registerUser(createUserDto);
}
}
Passport allows you to create a strategy to authenticate a user using a username and password. The strategy needs a verify callback, which receives these credentials to complete the authentication process.
Here we will create two strategies:
JWT strategy to create JWT using a secret constant.
To set it up, create a strategy
directory inside the src
folder. Inside the created folder, add a constants.ts
and create a constant as follows:
1 2 3
export const jwtConstants = { secret: 'kimkimani', }
Finally, create a jwt.strategy.ts
file and set up the JWT strategy as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import {
jwtConstants
} from './constants';
import {
ExtractJwt,
Strategy
} from 'passport-jwt';
import {
PassportStrategy
} from '@nestjs/passport';
import {
Injectable
} from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return {
userId: payload.sub,
username: payload.username
};
}
}
Local strategy to check and validate the user username and password. We will create this strategy later as it requests a service to validate the user.
The API is taking shape; we can now add the authentication features to validate the users. First, create an auth
directory inside the src
folder. Here:
Create a service to validate the user. Create a file auth.service.ts inside the auth directory as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import {
UserService
} from 'src/users/user.service';
import {
Injectable
} from '@nestjs/common';
import {
JwtService
} from '@nestjs/jwt';
import {
HashService
} from 'src/users/hash.service';
@Injectable()
export class AuthService {
constructor(private userService: UserService,
private hashService: HashService,
private jwtService: JwtService) {}
async validateUser(email: string, pass: string): Promise < any > {
const user = await this.userService.getUserByUsername(email);
if (user && (await this.hashService.comparePassword(pass, user.password))) {
return user;
}
return null;
}
async login(user: any) {
const payload = {
username: user.email,
sub: user.id
};
return {
access_token: this.jwtService.sign(payload),
};
}
}
Now that we have our service, let’s create a local strategy. To set up, create a local.strategy.ts
file inside the strategy
directory as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {
AuthService
} from 'src/auth/auth.service';
import {
Strategy
} from 'passport-local';
import {
PassportStrategy
} from '@nestjs/passport';
import {
Injectable,
UnauthorizedException
} from '@nestjs/common';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise < any > {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException({
message: "You have entered a wrong username or password"
});
}
return user;
}
}
Create a controller to set up a login endpoint. Create a file auth.controller.ts
inside the auth
directory as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {
AuthService
} from './auth.service';
import {
Controller,
Request,
UseGuards,
Post
} from '@nestjs/common';
import {
AuthGuard
} from '@nestjs/passport';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(AuthGuard('local'))
@Post(`/login`)
async login(@Request() req) {
return this.authService.login(req.user);
}
}
For Nest.js to be able to access the services we have created we need to set the necessary modules for both Auth
and Users
.
To create a user module, navigate to the users
directory and create a user.module.ts
as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import {
Module
} from '@nestjs/common';
import {
UserService
} from './user.service';
import {
UserController
} from './user.controller';
import {
MongooseModule
} from '@nestjs/mongoose';
import {
User,
UserSchema
} from 'src/users/user.schema';
import {
JwtModule
} from '@nestjs/jwt';
import {
jwtConstants
} from 'src/strategy/constants';
import {
HashService
} from 'src/users/hash.service';
import {
AuthService
} from 'src/auth/auth.service';
import {
JwtStrategy
} from 'src/strategy/jwt.strategy';
import {
LocalStrategy
} from 'src/strategy/local.strategy';
@Module({
imports: [
MongooseModule.forFeature([{
name: User.name,
schema: UserSchema
}]),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {
expiresIn: '60d'
},
}),
],
controllers: [UserController],
providers: [UserService, HashService, AuthService, JwtStrategy, LocalStrategy],
})
export class UserModule {}
To create a user module, navigate to the auth
directory and create a auth.module.ts
as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import {
Module
} from '@nestjs/common';
import {
AuthService
} from './auth.service';
import {
AuthController
} from './auth.controller';
import {
MongooseModule
} from '@nestjs/mongoose';
import {
User,
UserSchema
} from 'src/users/user.schema';
import {
JwtModule
} from '@nestjs/jwt';
import {
jwtConstants
} from 'src/strategy/constants';
import {
UserService
} from 'src/users/user.service';
import {
HashService
} from 'src/users/hash.service';
import {
LocalStrategy
} from 'src/strategy/local.strategy';
@Module({
imports: [
MongooseModule.forFeature([{
name: User.name,
schema: UserSchema
}]),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {
expiresIn: '60d'
},
}),
],
controllers: [AuthController],
providers: [AuthService, UserService, LocalStrategy, HashService],
})
export class AuthModule {}
To use these modules navigate to the app.module.ts
file and add them as follows:
Import the modules
1 2 3 4 5 6
import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module';
Execute the modules inside the module imports
1 2 3 4 5 6
@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost:27017/auth'), UserModule, AuthModule, ],
Test if the API works as expected. To do this run the following command:
npm run start
This will successfully start the application on port 3000.
To test the user authentication, open postman and run the following API endpoints.
Registering the user:
Send a post request to http://localhost:3000/register
. Ensure you add the username and password to create a new user as follows:
Click send to execute the endpoint. At this point, you should receive a response of the created user.
To confirm if the user has been created, you can navigate to MongoDB, open the auth database and access the users document:
The user password is correctly hashed to ensure its security as shown in the above database collection.
Login the user:
Send a post request to http://localhost:3000/register
. This time add the details you used to register the user:
When you click send, If the password and username are identified in the database, the user will receive an access token, as illustrated below. The user will be able to use the API’s protected routes using the given access token.
That’s all for this guide!!