graphqlnestjspassport-localnestjs-passport

NestJS/GraphQL/Passport - getting unauthorised error from guard


I'm trying to follow along with this tutorial and I'm struggling to convert the implementation to GraphQL.

local.strategy.ts

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authenticationService: AuthenticationService) {
    super();
  }

  async validate(email: string, password: string): Promise<any> {
    const user = await this.authenticationService.getAuthenticatedUser(
      email,
      password,
    );

    if (!user) throw new UnauthorizedException();

    return user;
  }
}

local.guard.ts

@Injectable()
export class LogInWithCredentialsGuard extends AuthGuard('local') {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const { req } = ctx.getContext();
    req.body = ctx.getArgs();

    await super.canActivate(new ExecutionContextHost([req]));
    await super.logIn(req);
    return true;
  }
}

authentication.type.ts

@InputType()
export class AuthenticationInput {
  @Field()
  email: string;

  @Field()
  password: string;
}

authentication.resolver.ts

@UseGuards(LogInWithCredentialsGuard)
@Mutation(() => User, { nullable: true })
logIn(
  @Args('variables')
  _authenticationInput: AuthenticationInput,
  @Context() req: any,
) {
  return req.user;
}

mutation

mutation {
  logIn(variables: {
    email: "email@email.com",
    password: "123123"
  } ) {
    id
    email
  }
}

Even the above credentials are correct, I'm receiving an unauthorized error.


Solution

  • The problem is in your LogInWithCredentialsGuard.

    You shouldn't override canAcitavte method, all you have to do is update the request with proper GraphQL args because in case of API request, Passport automatically gets your credentials from req.body. With GraphQL, execution context is different, so you have to manually set your args in req.body. For that, getRequest method is used.

    As the execution context of GraphQL and REST APIs is not same, you have to make sure your guard works in both cases whether it's controller or mutation.

    here is a working code snippet

    @Injectable()
    export class LogInWithCredentialsGuard extends AuthGuard('local') {
      // Override this method so it can be used in graphql
      getRequest(context: ExecutionContext) {
        const ctx = GqlExecutionContext.create(context);
        const gqlReq = ctx.getContext().req;
        if (gqlReq) {
          const { variables } = ctx.getArgs();
          gqlReq.body = variables;
          return gqlReq;
        }
        return context.switchToHttp().getRequest();
      }
    }
    

    and your mutation will be like

    @UseGuards(LogInWithCredentialsGuard)
    @Mutation(() => User, { nullable: true })
    logIn(
      @Args('variables')
      _authenticationInput: AuthenticationInput,
      @Context() context: any, // <----------- it's not request
    ) {
      return context.req.user;
    }