jwtnestjspassport.jsguard

In NestJS, how do I build a guard that verifies if the user's email is confirmed?


I'm using NestJS 10. I would like to create a guard that protects an endpoint, not only if the user is logged in, but also if they have confirmed their email. I'm using NestJS/passport, and create JWT tokens like so

  async getTokens(userId: string, username: string) {
    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        {
          sub: userId,
          username,
        },
        {
          secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
          expiresIn: ACCESS_TOKEN_DURATION,
        },
      ),
      this.jwtService.signAsync(
        {
          sub: userId,
          username,
        },
        {
          secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
          expiresIn: REFRESH_TOKEN_DURATION,
        },
      ),
    ]);

    return {
      accessToken,
      refreshToken,
    };
  }

I have this AccessTokenStrategy configured in my auth module ...

type JwtPayload = {
  sub: string;
  username: string;
};

@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    console.log("access token strategy ...");
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_ACCESS_SECRET,
    });
  }

  validate(payload: JwtPayload) {
    return payload;
  }
}

But I'm not sure how to guard my email confirm guard. My user entity has a boolean "isEmailConfirmed" attribute but I'm not quite sure how to add/extract that from the payload if I am to build such a guard.


Solution

  • You didn't provide your database engine, so I'm going to assume it is Prisma, although it isn't that important here.

    You could create a guard as follows:

    import { Injectable, type CanActivate, type ExecutionContext } from '@nestjs/common';
    
    import { DbUserService } from '@/modules/database/user.service';
    
    interface GuardRequest {
        readonly userId: string;
    }
    
    @Injectable()
    export class IsEmailConfirmedGuard implements CanActivate {
        constructor(private readonly dbUserService: DbUserService) {}
    
        public async canActivate(context: ExecutionContext): Promise<boolean> {
            const request = context.switchToHttp().getRequest<GuardRequest>();
            const user = request.userId;
    
            const isEmailConfirmed = await this.dbUserService.isEmailConfirmed(
                userId,
            );
    
            return isEmailConfirmed;
        }
    }
    

    Of course isEmailConfirmed function should query your database for the isEmailConfirmed attribute value of the user in accordance to the userId, let's say.

    Then you apply the guard to your endpoint controller:

    import { Body, Controller, HttpCode, HttpStatus, Param, Patch, UseGuards } from '@nestjs/common';
    
    import { IsEmailConfirmedGuard } from '@/guards/is-email-confirmed.guard';
    
    import Routes from './example.routes';
    
    @Controller(Routes.EXAMPLE_CONTROLLER)
    export class ExampleController {
        @UseGuards(IsEmailConfirmedGuard)
        @Patch(Routes.EXAMPLE)
        @HttpCode(HttpStatus.OK)
        public async example(
        ): Promise<void> {
           return;
        }
    }