Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting User Data by using Guards (Roles, JWT)

The documentation is kinda thin here so I ran into a problem. I try to use Guards to secure Controller or it's Actions, so I gonna ask for the role of authenticated requests (by JWT). In my auth.guard.ts I ask for "request.user" but it's empty, so I can't check the users role. I don't know how to define "request.user". Here is my auth module and it's imports.

auth.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RolesGuard } from './auth.guard';

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

  @Get('token')
  async createToken(): Promise<any> {
    return await this.authService.signIn();
  }

  @Get('data')
  @UseGuards(RolesGuard)
  findAll() {
    return { message: 'authed!' };
  }
}

roles.guard.ts

Here user.request is empty, because I never define it. The documentation doesn't show how or where.

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

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user; // it's undefined
    const hasRole = () =>
      user.roles.some(role => !!roles.find(item => item === role));
    return user && user.roles && hasRole();
  }
}

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { HttpStrategy } from './http.strategy';
import { UserModule } from './../user/user.module';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secretOrPrivateKey: 'secretKey',
      signOptions: {
        expiresIn: 3600,
      },
    }),
    UserModule,
  ],
  providers: [AuthService, HttpStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

auth.service.ts

import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';

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

  async signIn(): Promise<object> {
    // In the real-world app you shouldn't expose this method publicly
    // instead, return a token once you verify user credentials
    const user: any = { email: '[email protected]' };
    const token: string = this.jwtService.sign(user);
    return { token };
  }

  async validateUser(payload: any): Promise<any> {
    // Validate if token passed along with HTTP request
    // is associated with any registered account in the database
    return await this.userService.findOneByEmail(payload.email);
  }
}

jwt.strategy.ts

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'secretKey',
    });
  }

  async validate(payload: any) {
    const user = await this.authService.validateUser(payload);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Documentation: https://docs.nestjs.com/guards

Thanks for any help.

like image 856
bonblow Avatar asked Nov 22 '18 07:11

bonblow


People also ask

How do you handle role/permissions updates with JWT?

How do you handle role/permissions updates with JWT? Usually, JWT implementations in a REST Api backend save the user roles and/or permissions inside the JWT token claims. In this cases, clients make use of this claims to restrict user's interaction with some of the app features.

What happens if jwtauthguard is added before rolesguard?

} Note how both the auth and roles guard are activated in the same scope and that JwtAuthGuard is added before RolesGuard. If I were to change the sequence of the guards then the RolesGuard would not be able to retrieve the user of the request.

Can you use JWT roles in controllers?

So bottom line: Ensure the “roles” claim of your JWT contains an array of roles assigned to the user, and you can use in your controllers. It all works seamlessly.

Is the roles guard applied for get/route?

Your decorator and guards look fine, but from the snippet of your users.controller.ts file it is not clear whether the roles guard is actually applied for the GET / route. I do, however, have an NestJS app with a quite similar setup based on the guards documentation.


4 Answers

Additionally to your RolesGuard you need to use an AuthGuard.

Standard

You can use the standard AuthGuard implementation which attaches the user object to the request. It throws a 401 error, when the user is unauthenticated.

@UseGuards(AuthGuard('jwt'))

Extension

If you need to write your own guard because you need different behavior, extend the original AuthGuard and override the methods you need to change (handleRequest in the example):

@Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {

  handleRequest(err, user, info: Error) {
    // don't throw 401 error when unauthenticated
    return user;
  }

}

Why do this?

If you look at the source code of the AuthGuard you can see that it attaches the user to the request as a callback to the passport method. If you don't want to use/extend the AuthGuard, you will have to implement/copy the relevant parts.

const user = await passportFn(
  type || this.options.defaultStrategy,
  options,
  // This is the callback passed to passport. handleRequest returns the user.
  (err, info, user) => this.handleRequest(err, info, user)
);
// Then the user object is attached to the request
// under the default property 'user' which you can change by configuration.
request[options.property || defaultOptions.property] = user;
like image 111
Kim Kern Avatar answered Oct 17 '22 06:10

Kim Kern


You can attach multiple guards together (@UseGuards(AuthGuard('jwt'), RolesGuard)) to pass the context between them. Then you will have access 'req.user' object inside 'RolesGuard'.

like image 39
balakrishna Avatar answered Oct 17 '22 07:10

balakrishna


After I got the selected answer working (thank you), I found this option as well that you can add to the constructor that essentially does the same thing.

http://www.passportjs.org/docs/authorize/

Association in Verify Callback

One downside to the approach described above is that it requires two instances of the same strategy and supporting routes.

To avoid this, set the strategy's passReqToCallback option to true. With this option enabled, req will be passed as the first argument to the verify callback.

    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
    
        constructor(private authService: AuthService) {
            super({
                passReqToCallback: true
            })
        }

        // rest of the strategy (validate)
    }
like image 2
Mike Simpson Avatar answered Oct 17 '22 06:10

Mike Simpson


Does it work if you use req.authInfo?

As long as you don't provide a custom callback to passport.authenticate method, the user data should be attached to the request object like this.

req.authInfo should be the object you returned in your validate method

like image 1
Lewsmith Avatar answered Oct 17 '22 05:10

Lewsmith