NestJS/GraphQL/Passport - getting unauthorised error from guard

Viewed 1577

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.

2 Answers

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 but incase of graphql execution context is different so you have to manually set your args in req.body and for that getRequest method is used.

As execution context of graphql and rest api 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;
}

I've been able to get a successful login with a guard like this:

@Injectable()
export class LocalGqlAuthGuard extends AuthGuard('local') {
  constructor() {
    super();
  }
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    const req = ctx.getContext().req;
    req.body = ctx.getArgs();
    return req;
  }
  async canActivate(context: ExecutionContext) {
    await super.canActivate(context);
    const ctx = GqlExecutionContext.create(context);
    const req = ctx.getContext().req;
    await super.logIn(req);
    return true;
  }
}
Related