Use JwtService inside RolesGuard to decode JWT token and get user roles without using passport

Viewed 1672

I really don't know what should I do to resolve my problem. I tried implement AuthGuard for protected routes in controller. I want to check user role in roles.guard.ts and if he has one of required the controller will be open for him. My structure looks like:

- src
  - auth
    auth.controller.ts
    auth.service.ts
    auth.module.ts
  - roles
    roles.decorator.ts
    roles.guard.ts
  app.module.ts
  main.ts

In auth.service.ts I'm using JwtService to generate token and verify token and it works as well:

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService
  ) {
  }

  findUser(id: number): string {
    if (id === 0) throw new Error("User not available");

    return "martin";
  }

  async generateToken(email: string, role: string[]): Promise<string> {
    const payload = { email: email, role: role };

    return this.jwtService.sign(payload, { expiresIn: '24h', secret: process.env['JWT_SECRET'] });
  }

  async validateToken(token: string): Promise<boolean> {
    const isValidToken = await this.jwtService.verify(token, { secret: process.env['JWT_SECRET'] });

    return !!isValidToken;
  }
}

In auth.controller.ts I used @Roles decorator to define required user role:

import { Body, Controller, Get, Param, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Roles } from '../roles/roles.decorator';
import { RolesGuard } from '../roles/roles.guard';

@UseGuards(RolesGuard)
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {
  }

  @Get(':id')
  @Roles('admin')
  async findUser(@Body() id: number) {
    return this.authService.findUser(id);
  }

  @Get('generate')
  async generateToken() {
    return this.authService.generateToken('localhost', ['admin', 'user', 'seller'] );
  }
}

And my auth.module.ts is nothing special, the same as in the docs:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [JwtModule.register({
    secret: process.env['JWT_SECRET']
  })],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {
}

And in the roles.guard.ts which is activated to check role from @Roles I want to use JwtService to decode my JWT token stored in cookies, so I wrote this code:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private jwtService: JwtService
  ) {
  }

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) return true;

    const request = context.switchToHttp().getRequest();
    const user = request.headers;

    if (!user.auth_token) return false;

    const matchRoles = () => this.jwtService.verify(user.auth_token, { secret: process.env['JWT_SECRET'] });

    console.log(matchRoles());
  }
}

But Nest return error in while compile code:

Nest can't resolve dependencies of the RolesGuard (Reflector,?). Please make sure that the argument JwtService at index [1] is available in the RolesGuard context.

Potential solutions:
- If JwtService is a provider, is it part of the current Rol
esGuard?
- If JwtService is exported from a separate @Module, is that
 module imported within RolesGuard?
  @Module({
    imports: [ /* the Module containing JwtService */ ]
  })

At the end my app.module.ts:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { Connection } from 'typeorm';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './roles/roles.guard';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['.env.development', '.env.production'],
    }),
    TypeOrmModule.forRoot({
      entities: [],
      synchronize: true
    }),
    AuthModule,
    RolesGuard
  ],
  controllers: [],
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard
    }
  ],
})
export class AppModule {
  constructor(private connection: Connection) {
  }
}

And I don't know what to do? Another module only for roles.guard.ts or what? I really don't want to use passport and implement his strategy, when I should can use (in theory) JwtService properties. Or maybe I should move roles.* files to auth directory?

1 Answers

RolesGuard should not be in the imports array. The only thing that should ever go in the imports array are Modules. All Nest enhancers (Filters, Guards, Interceptors, and Pipes) exist outside of the providers array as well unless you are globally binding them with their respective APP_* constants.

Related