NestJS - Redirect user from guard when check fails

Viewed 7929

I've added some simple, working login functionality to a NestJS app I'm building. I now want to block access to certain routes for logged out users, so I've added a simple AuthGuard, which looks like this;

@Injectable()
export class AuthGuard implements CanActivate {
    public canActivate(
        context: ExecutionContext,
    ): boolean {
        const request = context.switchToHttp().getRequest();
        const response = context.switchToHttp().getResponse();
        if (typeof request.session.authenticated !== "undefined" && request.session.authenticated === true) {
            return true;
        }
        return false;
    }
}

This works in the way it blocks the user from accessing routes the guard is applied to, but in doing so it just shows this JSON response;

{
    "statusCode": 403,
    "error": "Forbidden",
    "message": "Forbidden resource"
}

What I want is the user to be redirected to the login page when the guard fails. I've tried replacing the return false with response.redirect("/auth/login"); and although it works, in the console I get messages

(node:11526) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Can't set headers after they are sent.

(node:11526) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

So although it works, it doesn't seem to be the best solution.

The route the guard is called on is as follows btw:

@Get()
@UseGuards(AuthGuard)
@Render("index")
public index() {
    return {
        user: {},
    };
}

Is it at all possible to properly redirect a user after the guard fails?

2 Answers

You can achieve what you want using both guard and filter. Through Unauthorized Exception from guard and handle the exception from filter to redirect. Here is a such filter:

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Response } from 'express';
import { UnauthorizedException } from '@nestjs/common';

@Catch(UnauthorizedException)
export class ViewAuthFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();

    response.status(status).redirect('/admins');
  }
}

below is how to use it:

@Get('/dashboard')
@UseGuards(JwtGuard)
@UseFilters(ViewAuthFilter)
@Render('dashboard/index')
dashboard() {
    return {};
}

It seems very strange that an endpoint that is supposed to return a JSON, returns a redirection to HTML content.

Though, if you want to manage how your exceptions are handled with user-friendly custom content, I suggest you take a look at exception filters:

https://docs.nestjs.com/exception-filters

Directly pasted from the documentation of nestjs, here is an explicit example:

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}
Related