Nest JS Building Auth Service with JWT Tokens Part-3

Nest JS Building Auth Service with JWT Tokens Part-3

banner nestjs

Blog Name Link
Part-1 Nest JS Building REST APIs using Mongo DB Part-1 https://tkssharma.com/building-rest-apis-with-nestjs-and-mongodb-part-1
Part-2 Nest JS APIs with mongoose Mongo DB Database Part-2 https://tkssharma.com/building-rest-apis-with-nestjs-and-mongodb-part-2

In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's viewpoint.

Github Link https://github.com/tkssharma/blogs/tree/master/nestjs-rest-apis-docs

What is NestJS?

As described in the Nestjs website, Nestjs is a progressive Node.js framework for building efficient, reliable and scalable server-side applications.

Nestjs combines the best programming practice and the cutting-edge techniques from the NodeJS communities.

  • A lot of NestJS concepts are heavily inspired by the effort of the popular frameworks in the world, esp. Angular .
  • Nestjs hides the complexities of web programming in NodeJS, it provides a common abstraction of the web request handling, you are free to choose Expressjs or Fastify as the background engine.
  • Nestjs provides a lot of third party project integrations, from database operations, such as Mongoose, TypeORM, etc. to Message Brokers, such as Redis, RabbitMQ, etc.

If you are new to Nestjs like me but has some experience of Angular , TypeDI or Spring WebMVC, bootstraping a Nestjs project is really a piece of cake.

Make sure you have installed the latest Nodejs.

npm i -g @nestjs/cli

When it is finished, there is a nest command available in the Path. The usage of nest is similar with ng (Angular CLI), type nest --help in the terminal to list help for all commands.

❯ nest --help
Usage: nest <command> [options]

Options:
  -v, --version                                   Output the current version.
  -h, --help                                      Output usage information.

Commands:
  new|n [options] [name]                          Generate Nest application.
  build [options] [app]                           Build Nest application.
  start [options] [app]                           Run Nest application.
  info|i                                          Display Nest project details.
  update|u [options]                              Update Nest dependencies.
  add [options] <library>                         Adds support for an external library to your project.
  generate|g [options] <schematic> [name] [path]  Generate a Nest element.
    Available schematics:
      ┌───────────────┬─────────────┐
      │ name          │ alias       │
      │ application   │ application │
      │ class         │ cl          │
      │ configuration │ config      │
      │ controller    │ co          │
      │ decorator     │ d           │
      │ filter        │ f           │
      │ gateway       │ ga          │
      │ guard         │ gu          │
      │ interceptor   │ in          │
      │ interface     │ interface   │
      │ middleware    │ mi          │
      │ module        │ mo          │
      │ pipe          │ pi          │
      │ provider      │ pr          │
      │ resolver      │ r           │
      │ service       │ s           │
      │ library       │ lib         │
      │ sub-app       │ app         │
      └───────────────┴─────────────┘

Now generate a Nestjs project via:

nest new nestjs-sample

Open it in your favorite IDEs, such as Intellij WebStorm or VSCode.

Exploring the project files

Expand the project root, you will see the following like tree nodes.

.
├── LICENSE
├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

The default structure of this project is very similar with the one generated by Angular CLI.

  • src/main.ts is the entry file of this application.
  • src/app* is the top level component in a nest application.
    • There is an app.module.ts is a Nestjs Module which is similar with Angular NgModule, and used to organize codes in the logic view.
    • The app.service.ts is an @Injectable component, similar with the service in Angular or Spring's Service, it is used for handling business logic. A service is annotated with @Injectable.
    • The app.controller.ts is the controller of MVC, to handle incoming request, and responds the handled result back to client. The annotatoin @Controller() is similar with Spring MVC's @Controller.
    • The app.controller.spec.ts is test file for app.controller.ts. Nestjs uses Jest as testing framework.
  • test folder is for storing e2e test files.

Protect your APIs with JWT Token

In the last post, we connected to a Mongo server and used a real database to replace the dummy data storage. In this post, we will explore how to protect your APIs when exposing to a client application.

When we come to the security of a web application, technically it will include:

  • Authentication - The application will ask you to provide your principal and then it will identify who are you.
  • **Authorization ** - Based on your claims, check if you have permissions to perform some operations.

Passportjs is one of the most popular authentication frameworks on the Expressjs platform. Nestjs has great integration with passportjs with its @nestjs/passportjs module. We will follow the Authentication chapter of the official guide to add local and jwt strategies to the application we have done the previous posts.

Prerequisites

Install passportjs related dependencies.

$ npm install --save @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-local @types/passport-jwt 

Before starting the authentication work, let's generate some skeleton codes.

Firstly generate a AuthModule and AuthService .

nest g mo auth
nest g s auth

The authentication should work with users in the application.

Similarly, create a standalone UserModule to handle user queries.

nest g mo user
nest g s user

Ok, let's begin to enrich the AuthModule.

Implementing Authentication

First of all, let's create some resources for the user model, a Document and Schema file.

Create new file under /user folder.

import { SchemaFactory, Schema, Prop } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class User extends Document {
  @Prop({ require: true })
  readonly username: string;

  @Prop({ require: true })
  readonly email: string;

  @Prop({ require: true })
  readonly password: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

The User class is to wrap a document in Mongoose, and UserSchema is to describe User document.

Register UserSchema in UserModule, then you can use Model<User> to perform some operations on User document.

@Module({
  imports: [MongooseModule.forFeature([{ name: 'users', schema: UserSchema }])],
  providers: [//...
  ],
  exports: [//...
  ],
})
export class UserModule {}

The users here is used as the token to identify different Model when injecting a Model. When registering a UserSchema in mongoose, the name attribute in the above MongooseModule.forFeature is also the collection name of User documents.

Add a findByUsername method in UserService.

@Injectable()
export class UserService {
  constructor(@InjectModel('users') private userModel: Model<User>) {}

  findByUsername(username: string): Observable<User | undefined> {
    return from(this.userModel.findOne({ username }).exec());
  }
}

In the @Module declaration of the UserModule, register UserService in providers, and do not forget to add it into exports, thus other modules can use this service when importing UserModule.

//...other imports
import { UserService } from './user.service';

@Module({
  providers: [UserService],
  exports:[UserService]//exposing users to other modules...
})
export class UserModule {}

Create a test case to test the findByUsername method.

describe('UserService', () => {
  let service: UserService;
  let model: Model<User>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getModelToken('users'),
          useValue: {
            findOne: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    model = module.get<Model<User>>(getModelToken('users'));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('findByUsername should return user', async () => {
    jest
      .spyOn(model, 'findOne')
      .mockImplementation((conditions: any, projection: any, options: any) => {
        return {
          exec: jest.fn().mockResolvedValue({
            username: 'hantsy',
            email: 'hantsy@example.com',
          } as User),
        } as any;
      });

    const foundUser = await service.findByUsername('hantsy').toPromise();
    expect(foundUser).toEqual({
      username: 'hantsy',
      email: 'hantsy@example.com',
    });
    expect(model.findOne).lastCalledWith({username: 'hantsy'});
    expect(model.findOne).toBeCalledTimes(1);
  });
});

UserService depends on a Model<User>, use a provider to mock it by jest mocking feature. Using jest.spyOn method, you can stub the details of a methods, and watch of the calling of this method.

Let's move to AuthModule.

With @nestjs/passpart, it is simple to set up your passport strategy by extending PassportStrategy, we will create two passport strategies here.

  • LocalStrategy to handle authentication by username and password fields from request.
  • JwtStrategy to handle authentication by given JWT token header.

Simplify , generate two files by nest command line.

nest g class auth/local.strategy.ts --flat
nest g class auth/jwt.strategy.ts --flat

Firstly, let's implement the LocalStrategy.

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      usernameField: 'username',
      passwordField: 'password',
    });
  }

  validate(username: string, password: string): Observable<any> {
    return this.authService
      .validateUser(username, password)
      .pipe(throwIfEmpty(() => new UnauthorizedException()));
  }
}

In the constructor, use super to provide the essential options of the strategy you are using. For the local strategy, it requires username and password fields.

And the validate method is used to validate the authentication info against given info, here it is the username and password provided from request.

More details about the configuration options and validation of local strategy, check passport-local project.

In AuthService, add a method validateUser.

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
  ) {}

  validateUser(username: string, pass: string): Observable<any> {
    return this.userService.findByUsername(username).pipe(
      map(user => {
        if (user && user.password === pass) {
          const { password, ...result } = user;
          return result;
        }
        return null;
      }),
    );
  }
}

In the real application, we could use a crypto util to hash and compare the input password. We will discuss it in the further post.

It invokes findByUsername in UserService from UserModule. Imports UserModule in the declaration of AuthModule.

@Module({
  imports: [
    UserModule,
 	...]
    ...   
})
export class AuthModule {}

Let's create a method in AppController to implement the authentication by given username and password fields.

@Controller()
export class AppController {
  constructor(private authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  login(@Req() req: Request): Observable<any> {
    return this.authService.login(req.user);
  }
}

It simply calls another method login in AuthService.

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
  ) {}
  //...
  login(user: Partial<User>): Observable<any> {
    const payload = {
      sub: user.username,
      email: user.email,
      roles: user.roles,
    };
    return from(this.jwtService.signAsync(payload)).pipe(
      map(access_token => {
        access_token;
      }),
    );
  }
}

The login method is responsible for generating a JWT based access token based on the authenticated principal.

The URI path auth/login use a LocalAuthGuard to protect it.

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

Let's summarize how local strategy works.

  1. When a user hits auth/login with username and password, LocalAuthGuard will be applied.
  2. LocalAuthGuard will trigger LocalStrategy , and invokes its validate method, and store the result back to request.user.
  3. Back the controller, read user principal from request, generate a JWT token and send it back to the client.

After logging in, the access token can be extracted and put into the HTTP header in the new request to access the protected resources.

Let's have a look at how JWT strategy works.

Firstly implement the JwtStrategy.

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  validate(payload: any) :any{
    return { email: payload.email, sub: payload.username };
  }
}

In the constructor, there are several options configured.

The jwtFromRequest specifies the approach to extract token, it can be from HTTP cookie or request header Authorization .

If ignoreExpiration is false, when decoding the JWT token, it will check expiration date.

The secretOrKey is used to sign the JWT token or decode token.

In the validate method, the payload is the content of decoded JWT claims. You can add custom validation based on the claims.

More about the configuration options and verify method, check passport-jwt project.

In the declaration of AuthModule , imports JwtModule, it accept a register method to add initial options for signing the JWT token.

@Module({
  imports: [
     // ...
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [//..., 
      LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

Similarly create a JwtAuthGuard, and register it in the providers in AuthModule.

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt'){}

Create a method to read profile of the current user.

@Controller()
export class AppController {
  constructor(private authService: AuthService) {}
  
  //...  
  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Req() req: Request): any {
    return req.user;
  }
}

Let's review the workflow of the JWT strategy.

  1. Given a JWT token XXX, access /profile with header Authorization:Bearer XXX.
  2. JwtAuthGuard will trigger JwtStrategy, and calls validate method, and store the result back to request.user.
  3. In the getProfile method, send the request.user to client.

If you want to set a default strategy, change PassportModule in the declaration of AuthModule to the following.

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    //...
})
export class AuthModule {}

There are several application lifecycle hooks provided in Nestjs at runtime. In your codes you can observe these lifecycle events and perform some specific tasks for your application.

For example, create a data initializer for Post to insert sample data.

@Injectable()
export class UserDataInitializerService
  implements OnModuleInit, OnModuleDestroy {
  constructor(@InjectModel('users') private userModel: Model<User>) {}
  onModuleInit(): void {
    console.log('(UserModule) is initialized...');
    this.userModel
      .create({
        username: 'hantsy',
        password: 'password',
        email: 'hantsy@example.com',
      })
      .then(data => console.log(data));
  }
  onModuleDestroy(): void {
    console.log('(UserModule) is being destroyed...');
    this.userModel
      .deleteMany({})
      .then(del => console.log(`deleted ${del.deletedCount} rows`));
  }
}

More info about the lifecycle hooks, check the Lifecycle events chapter of the official docs.

Run the application

Open your terminal, run the application by executing the following command.

npm run start

Login using the username/password pair.

>curl http://localhost:3000/auth/login -d "{\"username\":\"hantsy\", \"password\":\"password\"}" -H "Content-Type:application/json"
>{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZjJkMGU0ODZhOTZiZTEyMDBmZWZjZWMiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTk2Nzg4NDg5LCJleHAiOjE1OTY3OTIwODl9.4oYpKTikoTfeeaUBoEFr9d1LPcN1pYqHjWXRuZXOfek"}

Try to access the /profile endpoint using this access_token.

>curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZjJkMGU0ODZhOTZiZTEyMDBmZWZjZWMiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTk2Nzg4NDg5LCJleHAiOjE1OTY3OTIwODl9.4oYpKTikoTfeeaUBoEFr9d1LPcN1pYqHjWXRuZXOfek"
{"username":"hantsy","email":"hantsy@example.com","id":"5f2d0e486a96be1200fefcec","roles":["USER"]}

Grab the source codes from my github, switch to branch feat/auth.

Comments