node.jsnestjspassport.jspassport-localnestjs-passport

Using failureRedirect option of passport-local with Nest.js


I need help with processing after authentication using Nest.js here do I pass the failureRedirect option for passport-local when using Nest.js for authentication?

Without Nest.js

app.post('/login', passport.authenticate('local', {
    //Passing options here.
    successRedirect: '/',
    failureRedirect: '/login'
}));

My code is. (with Nest.js)

local.strategy.ts

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

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(private authService: AuthService) {
        super({
            //I tried passing the option here. but failed.
        })
    }

    async validate(username: string, password: string): Promise<string | null> {
        const user = this.authService.validate(username, password);
        if (!user) {
            throw new UnauthorizedException();
        }
        return user;
    }
}

local.guard.ts

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

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

auth.controller.ts

import { Controller, Get, Post, Render, UseGuards } from "@nestjs/common";
import { LocalAuthGuard } from "./local.guard";

@Controller()
export class AuthController {
    @Get("/login")
    @Render("login")
    getLogin() {}
    
    //Redirect to '/login' when authentication failed.
    @UseGuards(LocalAuthGuard)
    @Post("/login")
    postLogin() {}
}

auth.module.ts

import { Module } from "@nestjs/common";
import { PassportModule } from "@nestjs/passport";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { LocalStrategy } from "./local.strategy";
import { LocalAuthGuard } from "./local.guard";

@Module({
   controllers: [AuthController],
   imports: [PassportModule],
   providers: [AuthService, LocalStrategy, LocalAuthGuard]
})
export class AuthModule {}

I tried adding code to AuthController#postLogin to redirect on login failure, but the code seems to run only on successful login. I would like to redirect to the login page again in case of login failure with the failureRedirect option of passport-local.


Solution

  • I found a workaround since using the passport options sadly didn't work:

    @Injectable()
    export class LocalAuthGuard extends AuthGuard('local') {
      getAuthenticateOptions(context: ExecutionContext): IAuthModuleOptions {
        return {
          successReturnToOrRedirect: '/',
          failureRedirect: '/login',
        };
      }
    }
    

    Instead I created a Nestjs Filter to catch an exception containing a redirect URL.

    redirecting.exception.ts

    export class RedirectingException {
      constructor(public url: string) {}
    }
    

    redirecting-exception.filter.ts

    import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
    import { Response } from 'express';
    import { RedirectingException } from './redirecting.exception';
    
    @Catch(RedirectingException)
    export class RedirectingExceptionFilter implements ExceptionFilter {
      catch(exception: RedirectingException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        response.redirect(exception.url);
      }
    }
    

    In my validate method I'm throwing the RedirectingException with the correct error msg, e.g.

    throw new RedirectingException('/login?error="User not found"');
    

    And the controller handles the rest of the redirecting and passes the error to the view, so it can be displayed:

    @Get('/login')
    @Render('login.pug')
    @Public()
    async login(@Query() query) {
      return { error: query.error };
    }
    
    @Post('/login')
    @Public()
    @UseGuards(LocalAuthGuard)
    @Redirect('/')
    async doLogin() {}
    

    I'd rather use the passport functionality including the failureFlash, but I couldn't get it to work.