Authentication and Sessions for MVC Apps with NestJS
John Biundo
Posted on July 22, 2019
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:
- How to set up a template engine like Handlebars to work with Nest
- How to integrate template rendering with Nest controllers
- How to integrate express-session with Nest
- 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.
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
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
andpassport
packages. Then, you'll need to install the strategy-specific package (e.g.,passport-jwt
orpassport-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
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
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>
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>
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>
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>
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:
- Pass the
<NestExpressApplication>
type assertion in theNestFactory.create()
method call to gain access to native Express methods. - 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();
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');
});
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('/');
}
}
At this point, you should be able to run the app:
$ npm run start
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:
- Nest provides the
@nestjs/passport
module which wraps Passport in a Nest style package that makes it easy to treat Passport as a provider. - 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
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
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);
}
}
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 {}
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;
}
}
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 {}
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;
}
}
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
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:
- Create guards used to decorate routes so that the configured Passport strategy is invoked
- Add
@UseGuards()
decorators as needed - Implement sessions so that users stay logged in across requests
- Configure Nest to use Passport and session-related features
- 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
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:
- Passport supports authentication in two modes. First, you need to perform the authentication step (i.e., logging in)
- 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;
}
}
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 theLoginGuard
we are defining in order to tie our custom guard to the code supplied by thepassport-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-specificAuthGuard
). - 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 thecanActivate()
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 thatcanActivate()
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 ExpressRequest
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.
- We call
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);
}
}
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 {}
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();
}
}
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();
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('/');
}
}
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:
- Let's send the user back to the login page whenever they fail to authenticate, and when they log out of the app
- 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');
}
}
}
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('/');
}
}
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.
Posted on July 22, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.