Authentication and Sessions for MVC Apps with NestJS

johnbiundo

John Biundo

Posted on July 22, 2019

Authentication and Sessions for MVC Apps with NestJS

John is a member of the NestJS core team, primarily contributing to the documentation.

Note: You can find all of the source code from this article on github here.

Intro

There's a lot of good information on building API servers in NestJS (AKA Nest). That use case is certainly a very strong fit for Nest.

Perhaps surprisingly, Nest is also very good at building traditional web applications - what Nest refers to as MVC apps in the documentation. By this I mean apps that you might otherwise consider building in ExpressJS + Handlebars, Laravel, or Ruby on Rails, with template-driven server-side rendering of pages. Users considering Nest for this use case might find the resources in this area to be a little lighter than the API server use case.

This is probably partially due to the fact that most of the information you need to build say, an ExpressJS + Handlebars app, is already well covered and easy to find. And most of that information applies directly to building such an app in Nest. But leaving it at that still leaves a few questions for the Nest newcomer to sort out on their own. This article attempts to provide a little more color on one aspect of the challenge: how to manage session-based authentication with Nest. Why is this different with Nest?

Fully answering that question probably needs one or more complete articles on its own. The short answer is: if you really want to take advantage of Nest, it helps to try to learn to do things "the Nest way". Nest makes no apologies for being opinionated. It has a certain way of approaching application architecture. If you're on-board with this approach, it helps to go all-in and try to build things the Nest way. This gives you amazing productivity advantages, helps you build DRY maintainable code, and lets you spend most of your time focusing on business logic, not boilerplate.

The Nest way will be familiar to many coming from a Java or .NET background, as many of the concepts - such as OO and dependency injection - are similar. But it should be equally accessible to anyone who has a background in TypeScript, Angular, or any modern FE or BE framework/enviroment. As with any such framework, there's plenty of room for individual choice and style, but following the general patterns of the framework can make you feel like you have the wind at your back instead of swimming against the tide.

Requirements

Phew! With that out of the way, let's get a little more specific. When it comes to the problem at hand, there are only a few Nest-specific techniques that you need to know to get a quick jump out of the starting blocks:

  1. How to set up a template engine like Handlebars to work with Nest
  2. How to integrate template rendering with Nest controllers
  3. How to integrate express-session with Nest
  4. How to integrate an authentication system (we'll specifically use Passport) with Nest

Items 3 and 4 are the main focus of this article. To get there, we'll wade slightly into items 1 and 2 (providing code samples and references to Nest docs that can help you go deeper), but we won't spend a lot of time there. Let me know in the comments if these are areas you'd like to see more about in future articles.

Let's take a quick look at the app we're going to build.
App

Our requirements are simple. Users will authenticate with a username and password. Once authenticated, the server will use Express sessions so that the user remains "logged in" until they choose to log out. We'll set up a protected route that is accessible only to an authenticated user.

Installation

If you haven't installed Nest before, the pre-requisites are simple. You just need a modern Node.js environment (>=8.9 will do) and npm or yarn. Then install Nest:



npm i -g @nestjs/cli


Enter fullscreen mode Exit fullscreen mode

We start by installing the required packages, and building our basic routes.

Passport provides a library called passport-local that implements a username/password authentication strategy, which suits our needs for this use case. Since we are rendering some basic HTML pages, we'll also install the versatile and popular express-handlebars package to make that a little easier. To support sessions and to provide a convenient way to give user feedback during login, we'll also utilize the express-session and connect-flash packages.

Note For any Passport strategy you choose (there are many available here), you'll always need the @nestjs/passport and passport packages. Then, you'll need to install the strategy-specific package (e.g., passport-jwt or passport-local) that scaffolds the particular authentication strategy you are building.

With these basic requirements in mind, we can now start by scaffolding a new Nest application, and installing the dependencies:



nest new mvc-sessions
cd mvc-sessions
npm install --save @nestjs/passport passport passport-local express-handlebars express-session connect-flash 
npm install --save-dev @types/express @types/express-session @types/connect-flash @types/express-handlebars


Enter fullscreen mode Exit fullscreen mode

Web interface

Let's start by building the templates we'll use for the UI of our authentication subsystem. Following a standard MVC type project structure, create the following folder structure (i.e., the public folder and its sub-folders):



mvc-sessions
└───public
│   └───views
│       └───layouts


Enter fullscreen mode Exit fullscreen mode

Now create the following handlebars templates, and configure Nest to use express-handlebars as the view engine. Refer here for more on the handlebars template language , and here for more background on Nest-specific techniques for server-side rendered (MVC style) web apps.

Main layout

Create main.hbs in the layouts folder, and add the following code. This is the outermost container for our views. Note the {{{ body }}} line, which is where each individual view is inserted. This structure allows us to set up global styles. In this case, we're taking advantage of Google's widely used material design lite component library to style our minimal UI. All of those dependencies are taken care of in the <head> section of our layout.



<!-- public/views/layouts/main.hbs -->
<!DOCTYPE html>
<html>
  <head>
    <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
    <style>
      .mdl-layout__content {
        padding: 24px;
        flex: none;
      }
      .mdl-textfield__error {
        visibility: visible;
        padding: 5px;
      }
      .mdl-card {
        padding-bottom: 10px;
        min-width: 500px;
      }
    </style>
  </head>
  <body>
    {{{ body }}}
  </body>
</html>


Enter fullscreen mode Exit fullscreen mode

Home page

Create home.hbs in the views folder, and add the following code. This is the page users land on after authenticating.



<!-- public/views/home.hbs -->
<div class="mdl-layout mdl-js-layout mdl-color--grey-100">
  <main class="mdl-layout__content">
    <div class="mdl-card mdl-shadow--6dp">
      <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
        <h2 class="mdl-card__title-text">Welcome {{ user.username }}!</h2>
      </div>
      <div class="mdl-card__supporting-text">
        <div class="mdl-card__actions mdl-card--border">
          <a class="mdl-button" href='/profile'>GetProfile</a>
        </div>
      </div>
    </div>
  </main>
</div>


Enter fullscreen mode Exit fullscreen mode

Login page

Create login.hbs in the views folder, and add the following code. This is the login form.



<!-- public/views/login.hbs -->
<div class="mdl-layout mdl-js-layout mdl-color--grey-100">
  <main class="mdl-layout__content">
    <div class="mdl-card mdl-shadow--6dp">
      <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
        <h2 class="mdl-card__title-text">Nest Cats</h2>
      </div>
      <div class="mdl-card__supporting-text">
        <form action="/login" method="post">
          <div class="mdl-textfield mdl-js-textfield">
            <input class="mdl-textfield__input" type="text" name="username" id="username" />
            <label class="mdl-textfield__label" for="username">Username</label>
          </div>
          <div class="mdl-textfield mdl-js-textfield">
            <input class="mdl-textfield__input" type="password" name="password" id="password" />
            <label class="mdl-textfield__label" for="password">Password</label>
          </div>
          <div class="mdl-card__actions mdl-card--border">
            <button class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect">Log In</button>
            <span class="mdl-textfield__error">{{ message }}</span>
          </div>
        </form>
      </div>
    </div>
  </main>
</div>


Enter fullscreen mode Exit fullscreen mode

Profile page

Create profile.hbs in the views folder and add the following code. This page displays details about the logged in user. It's rendered on our protected route.



<!-- public/views/profile.hbs -->
<div class="mdl-layout mdl-js-layout mdl-color--grey-100">
  <main class="mdl-layout__content">
    <div class="mdl-card mdl-shadow--6dp">
      <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
        <h2 class="mdl-card__title-text">About {{ user.username }}</h2>
      </div>
      <div>
        <figure><img src="http://lorempixel.com/400/200/cats/{{ user.pet.picId }}">
          <figcaption>{{ user.username }}'s friend {{ user.pet.name }}</figcaption>
        </figure>
        <div class="mdl-card__actions mdl-card--border">
          <a class="mdl-button" href='/logout'>Log Out</a>
        </div>
      </div>
    </div>
  </main>
</div>


Enter fullscreen mode Exit fullscreen mode

Set up view engine

Now we tell Nest to use express-handlebars as the view engine. We do this in the main.ts file, which by convention is where your Nest app will be bootstrapped from. While we won't go into a lot of detail here, the main concepts are:

  1. Pass the <NestExpressApplication> type assertion in the NestFactory.create() method call to gain access to native Express methods.
  2. Having done that, you can now simply access any of Express's native app methods. For the most part, the configuration of the view engine, and any other traditional Express middleware, proceeds as normal as with the view engine setup shown below.

Modify the main.ts file so that it looks like this:



// src/main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
import * as exphbs from 'express-handlebars';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  const viewsPath = join(__dirname, '../public/views');
  app.engine('.hbs', exphbs({ extname: '.hbs', defaultLayout: 'main' }));
  app.set('views', viewsPath);
  app.set('view engine', '.hbs');

  await app.listen(3000);
}
bootstrap();


Enter fullscreen mode Exit fullscreen mode

Authentication routes

The final step in this section is setting up our routes. This is one area where the MVC architecture starts to shine through. It can be a little unfamiliar if you're used to bare Express routes, so let's wade in a little. Feel free to take a few minutes to read the Nest documentation on controllers for lots more details on how Nest Controllers work.

We'll modify app.controller.ts so that it looks like the large code block below.

Before we do that, let's take a moment to look at the GET / route handler in the large code block below (the line starting with @Get('/'). The work is done by the @Render() decorator. Since all we're doing in this first iteration of the code is rendering our templates, the code is trivial. @Render() takes a single argument, which is the name of the template to render. Think of this as the Nest equivalent of Express code like:



app.get('/', function (req, res) {
    res.render('login');
});


Enter fullscreen mode Exit fullscreen mode

Methods decorated with @Render() can also return a value that will supply template variables. For now, we'll just do an empty return (implicitly returning undefined). Later, we'll use this feature to pass template variables.

Go ahead and update src/app.controller.ts with this code:



// src/app.controller.ts
import { Controller, Get, Post, Res, Render } from '@nestjs/common';
import { Response } from 'express';

@Controller()
export class AppController {
  @Get('/')
  @Render('login')
  index() {
    return;
  }

  @Post('/login')
  login(@Res() res: Response): void {
    res.redirect('/home');
  }

  @Get('/home')
  @Render('home')
  getHome() {
    return;
  }

  @Get('/profile')
  @Render('profile')
  getProfile() {
    return;
  }

  @Get('/logout')
  logout(@Res() res: Response): void {
    res.redirect('/');
  }
}


Enter fullscreen mode Exit fullscreen mode

At this point, you should be able to run the app:



$ npm run start


Enter fullscreen mode Exit fullscreen mode

Now, browse to http://localhost:3000 and click through the basic UI. At this point, of course, you can click through the pages without logging in.

Implementing Passport strategies

We're now ready to implement the authentication feature. It's helpful to have a good understanding of how Nest integrates with Passport. The Nest docs covers this in some depth. It's worth taking a quick detour to read that section.

Note: I authored that section of the NestJS docs, so you'll see some similarities/overlaps with the code and documentation below

The key takeaways are:

  1. Nest provides the @nestjs/passport module which wraps Passport in a Nest style package that makes it easy to treat Passport as a provider.
  2. You implement Passport strategies by extending the PassportStrategy class, where you implement the strategy-specific initialization and callback.

As mentioned, we'll utilize the passport-local strategy for this use case. We'll get to that implementation in a moment. Start by generating an AuthModule and in it, an AuthService:



nest g module auth
nest g service auth


Enter fullscreen mode Exit fullscreen mode

We'll also implement a UsersService to manage our User store, so we'll generate that module and service now:



nest g module users
nest g service users


Enter fullscreen mode Exit fullscreen mode

Replace the default contents of these generated files as shown below. For our sample app, the UsersService simply maintains a hard-coded in-memory list of users, and a method to retrieve one by username. In a real app, this is where you'd build your user model and persistence layer, using your library of choice (e.g., TypeORM, Sequelize, Mongoose, etc.).



// src/users/users.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  private readonly users: any[];

  constructor() {
    this.users = [
      {
        userId: 1,
        username: 'john',
        password: 'changeme',
        pet: { name: 'alfred', picId: 1 },
      },
      {
        userId: 2,
        username: 'chris',
        password: 'secret',
        pet: { name: 'gopher', picId: 2 },
      },
      {
        userId: 3,
        username: 'maria',
        password: 'guess',
        pet: { name: 'jenny', picId: 3 },
      },
    ];
  }

  async findOne(username: string): Promise<any> {
    return this.users.find(user => user.username === username);
  }
}


Enter fullscreen mode Exit fullscreen mode

In the UsersModule, the only change is to add the UsersService to the exports array of the @Module decorator so that it is visible outside this module (we'll soon use it in our AuthService).

You can read more here about how Nest uses modules to organize code and to understand more about the exports array and other parameters of the @Module() decorator.

Make sure src/users/users.module.ts looks like this:



// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}


Enter fullscreen mode Exit fullscreen mode

Our AuthService has the job of retrieving a user and verifying the password. Replace the default contents of the src/auth/auth.service.ts file with the code below:



// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username, pass): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}


Enter fullscreen mode Exit fullscreen mode

Warning Of course in a real application, you wouldn't store a password in plain text. You'd instead use a library like bcrypt, with a salted one-way hash algorithm. With that approach, you'd only store hashed passwords, and then compare the stored password to a hashed version of the incoming password, thus never storing or exposing user passwords in plain text. To keep our sample app simple, we violate that absolute mandate and use plain text. Don't do this in your real app!

We'll call into our validateUser() method from our passport-local strategy subclass in a moment. The Passport library expects us to return a full user if the validation succeeds, or a null if it fails (failure is defined as either the user is not found, or the password does not match). In our code, we use a convenient ES6 spread operator to strip the password property from the user object before returning it. Upon successful validation, Passport then takes care of a few details for us, which we'll explore later on in the Sessions section.

And finally, we update our AuthModule to import the UsersModule.



// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
})
export class AuthModule {}


Enter fullscreen mode Exit fullscreen mode

Our app will function now, but remains incomplete until we finish a few more steps. You can restart the app and navigate to http://localhost:3000 and still move around without logging in (after all, we haven't implemented our passport-local strategy yet. We'll get there momentarily).

Implementing Passport local

Now we can implement our passport-local strategy. Create a file called local.strategy.ts in the auth folder, and add the following code:



// src/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string) {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}


Enter fullscreen mode Exit fullscreen mode

We've followed the recipe described in the NestJS Authentication Chapter. In this use case with passport-local, there are no configuration options, so our constructor simply calls super(), without an options object.

We've also implemented the validate() method. For the local-strategy, Passport expects a validate() method with a signature like



validate(username: string, password:string): any


Enter fullscreen mode Exit fullscreen mode

Most of the work is done in our AuthService (and in turn, in our UserService), so this method is quite straightforward. The validate() method for any Passport strategy will follow a similar pattern. If a user is found and valid, it's returned so request handling can continue, and Passport can do some further housekeeping. If it's not found, we throw an exception and let Nest's exceptions layer handle it.

With the strategy in place, we have a few more tasks to complete:

  1. Create guards used to decorate routes so that the configured Passport strategy is invoked
  2. Add @UseGuards() decorators as needed
  3. Implement sessions so that users stay logged in across requests
  4. Configure Nest to use Passport and session-related features
  5. Add a little polish to the user experience

Let's get started. For the following sections, we'll want to adhere to a best practice project structure, so start by creating a few more folders. Under src, create a common folder. Inside common, create filters and guards folders. Our project structure now looks like this:



mvc-sessions
└───src
│   └───auth
│   └───common
│       └───filters
│       └───guards
│   └───users
└───public


Enter fullscreen mode Exit fullscreen mode

Implement guards

The NestJS Guards chapter describes the primary function of guards: to determine whether a request will be handled by the route handler or not. That remains true, and we'll use that feature soon. However, in the context of using the @nestjs/passport module, we will also introduce a slight new wrinkle that may at first be confusing, so let's discuss that now. Once again, the NestJS Authentication chapter has a section which describes this scenario which would be good to read now. The key takeaways are:

  1. Passport supports authentication in two modes. First, you need to perform the authentication step (i.e., logging in)
  2. Subsequently, you need to verify a user's credentials. In the case of passport-local, this means ensuring that the user's session is valid.

Both of these steps are implemented via Nest guards.

Initial Authentication

Looking at our UI, it's easy to see that we'll handle this initial authentication step via a POST request on our /login route. So how do we invoke the "login phase" of the passport-local strategy in that route? As suggested, the answer is to use a Guard. Similar to the way we extended the PassportStrategy class in the last section, we'll start with a default AuthGuard provided in the @nestjs/passport package, and extend it as needed. We'll name our new Guard LoginGuard. We'll then decorate our POST /login route with this LoginGuard to invoke the login phase of our passport-local strategy.

Create a file called login.guard.ts in the guards folder and replace its default contents as follows:



// src/common/guards/login.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LoginGuard extends AuthGuard('local') {
  async canActivate(context: ExecutionContext) {
    const result = (await super.canActivate(context)) as boolean;
    const request = context.switchToHttp().getRequest();
    await super.logIn(request);
    return result;
  }
}


Enter fullscreen mode Exit fullscreen mode

There's a lot going on in these few lines of code, so let's walk through it.

  • Our passport-local strategy has a default name of 'local'. We reference that name in the extends clause of the LoginGuard we are defining in order to tie our custom guard to the code supplied by the passport-local package. This is needed to disambiguate which class we are extending in case we end up using multiple Passport strategies in our app (each of which may contribute a strategy-specific AuthGuard).
  • As with all guards, the primary method we define/override is canActivate(), which is what we do here. You can read more about guards and customizing the canActivate() method here.
  • The critical part happens in the body of canActivate() , where we set up an Express session. Here's what's happening:
    • We call canActivate() on the super class, as we normally would in extending a class method. Our super class provides the framework for invoking our passport-local strategy. Recall from the Guards chapter that canActivate() returns a boolean indicating whether or not the target route will be called. When we get here, Passport will have run the previously configured strategy (from the super class) and will return a boolean to indicate whether or not the user has successfully authenticated. Here, we stash the result so we can do a little more processing before finally returning from our method.
    • The key step for starting a session is to now invoke the logIn() method on our super class, passing in the current request. This actually calls a special method that Passport automatically added to our Express Request object during the previous step. See here and here for more on Passport sessions and these special methods.
    • The Express session has now been set up, and we can return our canActivate() result, allowing only authenticated users to continue.

Sessions

Now that we've introduced sessions, there's one additional detail we need to take care of. Sessions are a way of associating a unique user with some server-side state information about that user. Let's delve briefly into how Passport rides on top of Express sessions to provide some context.

Passport adds properties to the session object to keep track of information about the user and their authentication state. The user details are populated by a serializeUser() call to the Passport library. This function is called automatically by Nest with the user object created in the validate() method (which we implemented in src/auth/local.strategy.ts a few minutes ago). This approach may feel a little complex at first, but it supports a completely flexible model for how you can manage interactions with your User store. In our case, we are simply passing the user object through untouched. In advanced scenarios, you may find yourself calling out to your database or caching layer to augment the user object with more information (e.g., roles/permissions). Unless you're doing something advanced like that, you can usually just use the following boilerplate for the serialization process.

Create the session.serializer.ts file in the auth folder, and add the following code:



// src/auth/session.serializer.ts
import { PassportSerializer } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SessionSerializer extends PassportSerializer {
  serializeUser(user: any, done: (err: Error, user: any) => void): any {
    done(null, user);
  }
  deserializeUser(payload: any, done: (err: Error, payload: string) => void): any {
    done(null, payload);
  }
}


Enter fullscreen mode Exit fullscreen mode

We need to configure our AuthModule to use the Passport features we just defined. Of course AuthService and LocalStrategy make sense as providers (read more about providers here if you need to). Note that the SessionSerializer we just created is also a pluggable provider, and needs to be included in the providers array. Don't worry if this isn't 100% clear at the moment. Just think of providers as a generic way to inject customizable services into your application structure (including 3rd party modules you've included).

Update auth.module.ts to look like this:



// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { SessionSerializer } from './session.serializer';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy, SessionSerializer],
})
export class AuthModule {}


Enter fullscreen mode Exit fullscreen mode

Now let's create our AuthenticatedGuard. This is a traditional guard, as covered in the NestJS Guards chapter. Its role is simply to protect certain routes. Create the file authenticated.guard.ts in the guards folder, and add the following code:



// src/common/guards/authenticated.guard.ts
import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common';

@Injectable()
export class AuthenticatedGuard implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    return request.isAuthenticated();
  }
}


Enter fullscreen mode Exit fullscreen mode

The only thing to point out here is that in order to determine whether a user is authenticated or not, we use the convenient isAuthenticated() method that Passport has attached to the request object for us. Passport will return true only if the user is authenticated (i.e., has a valid session).

Configure Nest to bootstrap features

We can now tell Nest to use the Passport features we've configured. Update main.ts to look like this:



// src/main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';

import * as session from 'express-session';
import flash = require('connect-flash');
import * as exphbs from 'express-handlebars';
import * as passport from 'passport';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  const viewsPath = join(__dirname, '../public/views');
  app.engine('.hbs', exphbs({ extname: '.hbs', defaultLayout: 'main' }));
  app.set('views', viewsPath);
  app.set('view engine', '.hbs');

  app.use(
    session({
      secret: 'nest cats',
      resave: false,
      saveUninitialized: false,
    }),
  );

  app.use(passport.initialize());
  app.use(passport.session());
  app.use(flash());

  await app.listen(3000);
}
bootstrap();


Enter fullscreen mode Exit fullscreen mode

Here, we've added the session and Passport support to our Nest app.

Warning As always, be sure to keep secrets out of your source code (don't put your session secret in the code, as we did here; use environment variables or a config module (such as NestJS Config Manager instead).

Note carefully that the order is important (register the session middleware first, then initialize Passport, then configure Passport to use sessions). We'll see the use of the flash feature in a few minutes.

Add route guards

Now we're ready to start applying these guards to routes. Update app.controller.ts to look like this:



// src/app.controller.ts
import { Controller, Get, Post, Request, Res, Render, UseGuards } from '@nestjs/common';
import { Response } from 'express';

import { LoginGuard } from './common/guards/login.guard';
import { AuthenticatedGuard } from './common/guards/authenticated.guard';

@Controller()
export class AppController {
  @Get('/')
  @Render('login')
  index() {
    return;
  }

  @UseGuards(LoginGuard)
  @Post('/login')
  login(@Res() res: Response) {
    res.redirect('/home');
  }

  @UseGuards(AuthenticatedGuard)
  @Get('/home')
  @Render('home')
  getHome(@Request() req) {
    return { user: req.user };
  }

  @UseGuards(AuthenticatedGuard)
  @Get('/profile')
  @Render('profile')
  getProfile(@Request() req) {
    return { user: req.user };
  }

  @Get('/logout')
  logout(@Request() req, @Res() res: Response) {
    req.logout();
    res.redirect('/');
  }
}


Enter fullscreen mode Exit fullscreen mode

Above, we've imported our two new guards and applied them appropriately. We use the LoginGuard on our POST /login route to initiate the authentication sequence in the passport-local strategy. We use AuthenticatedGuard on our protected routes to ensure they aren't accessible to unauthenticated users.

We're also taking advantage of the Passport feature that automatically stores our User object on the Request object as req.user. With this handy feature, we can now return values on our routes decorated with @Render() to pass a variable into our handlebars templates to customize their content.
For example return { user: req.user } to display information from the User object in our home template.

Finally, we have added the call to req.logout() in our logout route. This relies on the Passport logout() function, which, like the logIn() method we discussed earlier in the Sessions section, has been added to the Express Request object by Passport automatically upon successful authentication. When we invoke logout(), Passport tears down our session for us.

You should now be able to test the authentication logic by attempting to navigate to a protected route. Restart the app and point your browser at http://localhost:3000/profile. You should get a 403 Forbidden error. Return to the root page at http://localhost:3000, and log in, and you should be able to browse around (though the app is still missing a couple of features). Refer to src/users/users.service.ts for the hard-coded usernames and passwords that are accepted.

Adding polish

Let's address that ugly 403 Forbidden error page. If you navigate around the app, trying things like submitting an empty login request, a bad password, and logging out, you'll see that it's not a very good UX. Let's take care of a couple of things:

  1. Let's send the user back to the login page whenever they fail to authenticate, and when they log out of the app
  2. Let's provide a little feedback when a user types in an incorrect password

The best way to handle the first requirement is to implement a Filter. Create the file auth-exceptions.filter.ts in the filters folder, and add the following code:



// src/common/filters/auth-exceptions.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  UnauthorizedException,
  ForbiddenException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class AuthExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    if (
      exception instanceof UnauthorizedException ||
      exception instanceof ForbiddenException
    ) {
      request.flash('loginError', 'Please try again!');
      response.redirect('/');
    } else {
      response.redirect('/error');
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

The only new element here from what's covered in the NestJS Filters chapter is the use of connect-flash. If a route returns either an UnauthorizedException or a ForbiddenException, we redirect to the root route with response.redirect('/'). We also use connect-flash to store a message in Passport's session. This mechanism allows us to temporarily persist a message upon redirect. Passport and connect-flash automatically take care of the details of storing, retrieving, and cleaning up those messages.

The final touch is to display the flash message in our handlebars template. Update app.controller.ts to look like this. In this update, we're adding the AuthExceptionFilter and adding the flash parameters to our index (/) route.



// src/app.controller.ts
import { Controller, Get, Post, Request, Res, Render, UseGuards, UseFilters } from '@nestjs/common';
import { Response } from 'express';

import { LoginGuard } from './common/guards/login.guard';
import { AuthenticatedGuard } from './common/guards/authenticated.guard';
import { AuthExceptionFilter } from './common/filters/auth-exceptions.filter';

@Controller()
@UseFilters(AuthExceptionFilter)

export class AppController {
  @Get('/')
  @Render('login')
  index(@Request() req): { message: string } {
    return { message: req.flash('loginError') };
  }

  @UseGuards(LoginGuard)
  @Post('/login')
  login(@Res() res: Response) {
    res.redirect('/home');
  }

  @UseGuards(AuthenticatedGuard)
  @Get('/home')
  @Render('home')
  getHome(@Request() req) {
    return { user: req.user };
  }

  @UseGuards(AuthenticatedGuard)
  @Get('/profile')
  @Render('profile')
  getProfile(@Request() req) {
    return { user: req.user };
  }

  @Get('/logout')
  logout(@Request() req, @Res() res: Response) {
    req.logout();
    res.redirect('/');
  }
}


Enter fullscreen mode Exit fullscreen mode

We now have a fully functional authentication system for our server side Web application. Go ahead and fire it up and try logging in and out, accessing protected routes while logged out, and typing bad/missing username/password fields, and notice the more friendly error handling.

We're done! Kudo's for hanging in through a long tutorial. I hope this helps you understand some techniques for implementing sessions and authentication with NestJS.

Resources

You can find all of the source code from this article on github here.

Feel free to ask questions, make comments or suggestions, or just say hello in the comments below. And join us at Discord for more happy discussions about NestJS. I post there as Y Prospect.

Acknowledgements

Thanks to Jay McDoniel, Livio Brunner and Kamil Myśliwiec for their help reviewing this article.

💖 💪 🙅 🚩
johnbiundo
John Biundo

Posted on July 22, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related