DEV Community

Fernando
Fernando

Posted on

Learning to build an API in NestJS (Node + Typescript) - Part 03

Back again for the third round of NestJS goodness. 😎

If you didn't read the past two articles... you should! This is a process-oriented series, so every article builds on the last one.

Last time we managed to implement a super basic service, inject it into a controller, route endpoints properly, capture params (query parameters, URI embedded parameters, and request body), and import a module to the main module.

Now we are going to apply this new knowledge by building useful stuff. We start by coding a semi-public authentication scheme. No users, for now, just an unguarded endpoint that always returns a valid token. The rest of the endpoints should be guarded.

The first question that comes to my mind is ¿How do we generate a token?...

Generating a Token

NestJS has a JwtModule we can install (docs). ❤️

npm i @nestjs/jwt
Enter fullscreen mode Exit fullscreen mode

It has a service that can sign a user token. This should be used by our AuthenticationService to execute the sign in action.

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable({})
export class AuthenticationService {
  constructor(private readonly jwtService: JwtService) {
    this.jwtService = jwtService;
  }
  async signIn(user: any): Promise<any> {
    // Here we should map the user entity to a stripped-down object
    const userInfo = { username: 'username', sub: 'sub' };
    return this.jwtService.sign(userInfo);
  }
}
Enter fullscreen mode Exit fullscreen mode

Remember that if we use a service, we need to import the module where it came from. This specific import has a few configuration parameters that need to be provided. These involve the expiration time of the token, the hashing algorithm, the secret key, etc... These parameters come from the underlying library jsonwebtoken.
So at AuthenticationModule, we will import JtwModule with just a fantasy secret:

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthenticationService } from './authentication.service';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [PassportModule, JwtModule.register({ secret: 'secret-key' })],
  providers: [AuthenticationService],
})
export class AuthenticationModule {}
Enter fullscreen mode Exit fullscreen mode

Get the token for future requests

So now we should create a sign in endpoint which will return our token.

// All other imports
// ...
import { AuthenticationService } from 'src/authentication/authentication.service';

@Controller('example')
export default class ExampleController {
  constructor(
    private readonly exampleService: ExampleService,
    private readonly authenticationService: AuthenticationService,
  ) {}

  @Get('sign-in')
  async signIn() {
    return await this.authenticationService.signIn({});
  }

  // All the other endpoints
  //...
}
Enter fullscreen mode Exit fullscreen mode

That is looking good. The next step is to guard the rest of the endpoints by checking if there is a valid bearer token in the authorization header of the request. For that, we need to implement an authentication step in the server pipeline.

Implementing Passport

Looking at the official documentation, there is an authentication section. It introduces the passport library.

Passport gives us the middleware responsible for parsing the request and validating that the jwt token is valid.

We need to install the core Node library (passport), the underlying code to support a jwt strategy (passport-jwt), the NestJS wrapper (@nestjs/passport), and some types for development easiness (@types/passport-jwt).

npm install passport passport-jwt @nestjs/passport
npm install -D @types/passport-jwt
Enter fullscreen mode Exit fullscreen mode

So how does Passport know if the jwt token is valid?

Configuring Passport JWT Strategy

In the same way, we configured NestJS's JwtModule for building our jwt tokens, Passport will need to be configured with the same parameters to be able to parse correctly the token.

To go alongside the convention, this configuration goes in a jwt.strategy.ts file inside the authentication folder. It defines a class that is responsible for extending the base passport strategy class with the configuration needed. This includes implementing a validate function which will consume a user validation callback (we need to code this callback). This class will be available through AuthenticationModule as a provider for the rest of the application to use. This implies that the strategy class should be decorated with @Injectable(). The file should look something like this:

// import the specific strategy from Passport
import { Strategy } from 'passport-jwt';
// import NestJS passport wrapper
import { PassportStrategy } from '@nestjs/passport';
// import the Injectable decorator to allow this class to be imported into the AuthenticationModule
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super();
  }
}
Enter fullscreen mode Exit fullscreen mode

For this to work properly we need to add some parameters to the base constructor. We will provide the two most basic ones:

  • jwtFromRequest: Defines where to find the jwt token (it could be another place than the authorization header...). We will stick with the classic Bearer schema. For that, the value should be ExtractJwt.fromAuthHeaderAsBearerToken(). This seems to map to this and this parts of the passport documentation about extracting the jwt token from the request.
  • secretOrKey: The key that will sign the tokens provided by our app. For demo purposes, the value will be "secret-key". This seems to map to this part of the passport documentation

The file ends up looking like this:

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private authenticationService: AuthenticationService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'secret-key',
    });
  }

  async validate(userInfo: any): Promise<any> {
    // This method needs to be implemented right now
    const user = await this.authenticationService.validateUser(userInfo);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we are missing the user validation callback.

User validation

Passport requires a callback from us which will be used to validate a user. That validation callback receives the supposed user information as interpreted by the passport middleware. So with that information, we have to execute the validation process.

For this jwt strategy scenario, as we defined how our tokens are built, Passport already knows if the token is valid before the user validation. So what gets passed to our callback is the decoded jwt token. This is implied in this passage of the passport documentation.

Here we can add some logic if we wanted to. The only constraint is that if the validation es correct, we should return an object with useful user information (this info will be available at a user property of the request object). If not, the return value should be null. In our case, the validation will always succeed because we won't have users. Nonetheless, that validation callback needs to be implemented and should be placed in the AuthenticationService.

import { Injectable } from '@nestjs/common';

@Injectable({})
export class AuthenticationService {
  validateUser(userInfo: any): boolean | null {
    // Passport expects this return value to be a user object when validation is successful.
    // That info will be available at a `user` property in the `request` object
    return true;
  }

  // SignIn method
  // ...
}
Enter fullscreen mode Exit fullscreen mode

For the module to work, we need to import the recently installed PassportModule. Also, the Authentication Service should be exposed as an export. So the file will look like this:

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthenticationService } from './authentication.service';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [PassportModule, JwtModule.register({ secret: 'secret-key' })],
  providers: [AuthenticationService],
  exports: [AuthenticationService],
})
export class AuthenticationModule {}

Enter fullscreen mode Exit fullscreen mode

Guard endpoints

What remains is to guard our endpoints. Last step to finish this one up. 💪

NestJS Passport wrapper provides us with an AuthGuard decorator. You can provide the strategy name for the guard, but it is better to extend the class to a specific one per strategy basis. That way the code is cleaner and easier to maintain. This is evident when you use more than one strategy. For now, we just have de jwt one, but when adding users and logging in, that endpoint should be guarded by the local strategy which is based on username and password.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtGuard extends AuthGuard('jwt') {}
Enter fullscreen mode Exit fullscreen mode

Then import the guard in the controller and use it!

@Controller('example')
export default class ExampleController {
  constructor(
    private readonly exampleService: ExampleService,
    private readonly authenticationService: AuthenticationService,
  ) {}

  @Get()
  @UseGuards(JwtGuard)
  index() {
    return this.exampleService.getAll();
  }

  // All other endpoints
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Try your endpoints with curl:

curl http://localhost:3000/example -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwic3ViIjoic3ViIiwiaWF0IjoxNjY3MDc3NzYxfQ.LX2jeE1bavLOk9x-YvBZdEsfOo2ow4fMJ9DT4CYhXiE

curl http://localhost:3000/example
Enter fullscreen mode Exit fullscreen mode

curl result

Wrap up

This concludes the third article of this series. Today we covered how to create a JWT token and validate it using the Passport library and guarding our endpoints with custom guards extending from the NestJS Passport wrapper.

Next, we should start including users to make this authentication more realistic. For that, we need to use a database. So that will be the next step. Set up a database, choose a library to interact with it, successfully self-sign up to the app, and then be able to log in (log out will be left for another time...).

That's all for now! thanks for reading. I hope these articles are useful to you. Have fun and keep learning!

Top comments (0)