I write the GqlAuthGuard module, but it always throw an UnauthorizedException when JWT expired or illegal. It will lead to an error in stacktrace.
I want to throw AuthenticationError(from apollo-server-express).
How can I do it?
import { Injectable, ExecutionContext } from '#nestjs/common'
import { AuthGuard } from '#nestjs/passport'
import { GqlExecutionContext } from '#nestjs/graphql'
#Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
public getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
GraphQL Authorization Error
I had to modify a bit the solution proposed by Jay McDoniel:
import { Injectable, ExecutionContext } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
import { GqlExecutionContext } from '#nestjs/graphql';
import { AuthenticationError } from 'apollo-server-express';
#Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
return (await super.canActivate(context)) as boolean;
} catch (e) {
throw new AuthenticationError('You are not logged-in.');
}
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
You can override the canActivate method from the super class (AuthGuard('jwt')), and from there you can find if the super.canActivate(req) returns a true or false by saving the result to a variable. If false, you can throw your own error there instead of the UnauthorizedException that Nest throws.
import { Injectable, ExecutionContext } from '#nestjs/common'
import { AuthGuard } from '#nestjs/passport'
import { GqlExecutionContext } from '#nestjs/graphql'
#Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
public async canActivate(context: ExecutionContext): Promise<boolean> {
const result = (await super.canActivate(context)) as boolean;
if (!result) {
throw new AuthenticationError('Your Message Here');
}
return result;
}
public getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
Related
I have the following global guard:
authorization.guard.ts
import { ExecutionContext, Injectable } from "#nestjs/common"
import { Reflector } from "#nestjs/core"
import { AuthGuard } from "#nestjs/passport"
#Injectable()
export class AuthorizationGuard extends AuthGuard(["azure-ad"]) {
public constructor(private readonly reflector: Reflector) {
super()
}
async canActivate(context: ExecutionContext) {
const isPublic = this.reflector.get<boolean>(
"isPublic",
context.getHandler(),
)
if (isPublic) {
return true
}
const req = context.switchToHttp().getRequest()
if(req.headers.isbypass){
//help needed
}
const result = (await super.canActivate(context)) as boolean
await super.logIn(req)
return result
}
}
and the following auth module and strategy:
import { Module } from "#nestjs/common";
import { PassportModule } from "#nestjs/passport";
import { UsersModule } from "modules/users/users.module";
import { AzureADStrategy } from "./azureAD.strategy";
import { SessionSerializer } from "./session.serializer";
#Module({
imports: [PassportModule, UsersModule],
providers: [AzureADStrategy, SessionSerializer],
})
export class AuthModule {}
import {
BearerStrategy,
IBearerStrategyOption,
ITokenPayload,
VerifyCallback,
} from "passport-azure-ad";
import {
Inject,
Injectable,
OnModuleInit,
UnauthorizedException,
} from "#nestjs/common";
import passport = require("passport");
import { UsersService } from "modules/users/users.service";
import env from "../../config";
const tenantId = env.TENANT_ID;
const clientID = env.CLIENT_ID || "";
const azureCredentials: IBearerStrategyOption = {
//
};
#Injectable()
export class AzureADStrategy extends BearerStrategy implements OnModuleInit {
onModuleInit() {
passport.use("azure-ad", this);
}
constructor(
#Inject(UsersService) private usersService: UsersService
) {
super(
azureCredentials,
async (token: ITokenPayload, done: VerifyCallback) => {
if (Date.now() / 1000 > token.exp) {
return done(new UnauthorizedException("access token is expired"));
}
const tokenUsername = token?.preferred_username?.slice(0, 9);
const tokenAppId = !tokenUsername && token?.azp;
if (!tokenUsername && !tokenAppId) {
return done(new UnauthorizedException("Missing User"));
}
let user;
if (tokenUsername) {
try {
user = await this.usersService.getUser(
tokenUsername
);
if (!user) {
return done(
new UnauthorizedException("User is not a test user")
);
}
} catch (err) {
return done(err);
}
user.tz = tokenUsername;
} else {
user.appId = tokenAppId;
}
return done(null, user, token);
}
);
}
}
The guard is defined globally using:
const reflector = app.get(Reflector);
app.useGlobalGuards(
new AuthorizationGuard(reflector),
);
And the auth module is imported in app.module.ts:
#Module({
imports: [
AuthModule,
...
]
Now, for the question.
I would like to have a way to "backdoor" the global authorization by checking if req.headers.isbypass exists in the request's headers, and if it does use userService in authorizationGuard, so i can inject the user from the DB to req.user myself and continue the request.
How do I achieve that?
I would change the app.useGlobalGuards() to be a global guard provider, adding
{
provider: APP_GUARD,
useClass: AuthorizationGuard,
}
Into the providers of your AppModule so that Nest handles all of the DI for you. From there, it's just adding the UsersService to the constructor like you already have for the Reflector
#Injectable()
export class AuthorizationGuard extends AuthGuard(["azure-ad"]) {
public constructor(
private readonly reflector: Reflector,
private readonly usersService: UsersServce
) {
super()
}
async canActivate(context: ExecutionContext) {
const isPublic = this.reflector.get<boolean>(
"isPublic",
context.getHandler(),
)
if (isPublic) {
return true
}
const req = context.switchToHttp().getRequest()
if(req.headers.isbypass){
//help needed
}
const result = (await super.canActivate(context)) as boolean
await super.logIn(req)
return result
}
}
I've tried to inject a repository into the guard which extends from RateLimiterGuard nestjs-rate-limiter but I got an error when call super.canActivate(ctx). It said that this.reflector.get is not a function. Is there any mistake that I have?
Here is my code:
import { RateLimiterGuard, RateLimiterOptions } from 'nestjs-rate-limiter';
import type { Request } from 'express';
import config from 'config';
import { ExecutionContext, Injectable } from '#nestjs/common';
import { MockAccountRepository } from '../modules/mock/mock-accounts/accounts-mock.repository';
import { Reflector } from '#nestjs/core';
import { UserIdentifierType } from 'src/modules/users/user.types';
const ipHeader = config.get<string>('server.ipHeader');
#Injectable()
export class ForwardedIpAddressRateLimiterGuard extends RateLimiterGuard {
constructor(
reflector: Reflector,
options: RateLimiterOptions,
private readonly mockAccRepo: MockAccountRepository,
) {
super(options, reflector);
}
protected getIpFromRequest(request: Request): string {
return request.get(ipHeader) || request.ip;
}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest();
const phoneNumber = req.body?.phoneNumber || req.params?.phoneNumber;
// If mock phone number, always allow
if (
await this.mockAccRepo.findOne({
identifier: phoneNumber,
identifierType: UserIdentifierType.PHONE_NUMBER,
})
) {
return true;
}
// Otherwise, apply rate limiting
return super.canActivate(ctx);
}
}
i did Inject Repository(User) but it did not work for me .
i want to call : this.users Repository.create
but give this error :
Type Error: this.users Repository.create is not a function
......
i did Inject Repository(User) but it did not work for me .
i want to call : this.users Repository.create
but give this error :
Type Error: this.users Repository.create is not a function
Service :
import { HttpException, HttpStatus, Inject, Injectable,forwardRef } from '#nestjs/common';
import { AuthenticationService } from 'src/authentication/authentication.service';
import { Repository } from 'typeorm';
import CreateUserDto from './dto/create-user.dto';
import { InjectRepository } from '#nestjs/typeorm';
import User from './entities/user.entity';
#Injectable()
export class UserService {
constructor(
#Inject(forwardRef(() => AuthenticationService))
// #Inject(User)
// private usersRepository: Repository<User>
#InjectRepository(User) private usersRepository: Repository<User>,
private readonly authenticationService: AuthenticationService,
) {}
async getByEmail(email: string) {
const user = await this.usersRepository.findOne({ email });
if (user) {
return user;
}
throw new HttpException('User with this email does not exist', HttpStatus.NOT_FOUND);
}
async getById(id: number) {
const user = await this.usersRepository.findOne({ id });
if (user) {
return user;
}
throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND);
}
async create(userData: CreateUserDto) {
const newUser = await this.usersRepository.create(userData);
await this.usersRepository.save(newUser);
return newUser;
}
}
Module :
import { Module,forwardRef } from '#nestjs/common';
import { UserService } from './users.service';
import { TypeOrmModule } from '#nestjs/typeorm';
import User from './entities/user.entity';
import { UsersController } from './users.controller';
import { AuthenticationService } from 'src/authentication/authentication.service';
import { ConfigModule, ConfigService } from '#nestjs/config';
import { JwtModule, JwtService } from '#nestjs/jwt';
#Module({
imports: [TypeOrmModule.forFeature([User]),JwtModule.register({})],
providers: [UserService,AuthenticationService,ConfigService],
exports: [UserService,AuthenticationService,ConfigService],
controllers:[UsersController]
})
export class UsersModule {}
Entity :
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
#Entity('User')
class User {
#PrimaryGeneratedColumn()
public id?: number;
#Column({ unique: true })
public email: string;
#Column()
public name: string;
#Column()
public password: string;
}
export default User;
I found a solution by creating a custom repository and extend it with built in works fine.
Here is link :
https://clownhacker.tistory.com/250
I think it happened because I did changes with my entity class, I hope it will help someone.
In my case in this.repository, this was not pointing to correct instance. Used bind to fix it.
I have a custom class-validator rule:
import { Injectable } from "#nestjs/common";
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from "class-validator";
#ValidatorConstraint({ name: "Foo", async: true })
#Injectable()
export class FooRule implements ValidatorConstraintInterface {
async validate(value: unknown, args: ValidationArguments) {
// how to access request object from here?
}
defaultMessage(args: ValidationArguments) {
return "NOT OK.";
}
}
How do I access the request object inside the validate() method?
I ended up using a custom interceptor:
import { Injectable, NestInterceptor, CallHandler, ExecutionContext } from "#nestjs/common";
import { GqlExecutionContext } from "#nestjs/graphql";
import { ForbiddenError } from "apollo-server-core";
import { Observable } from "rxjs";
#Injectable()
export class FooInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// get gql execution context from http one
const gqlCtx = GqlExecutionContext.create(context);
// holds data passed in a gql input
const args: unknown[] = gqlCtx.getArgs();
// req object (can be used to obtain jwt payload)
const req = gqlCtx.getContext().req;
// validation logic here
const validationPassed = true;
if (validationPassed) {
// invoke the route handler method
return next.handle();
}
// will be caught by nest exceptions layer
throw new ForbiddenError("Not allowed.");
}
}
import { ROLES_KEY } from './role.decorator';
import { UserRole } from '../users/role.enum';
import { CanActivate, Injectable, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const requiredRole = this.reflector.getAllAndOverride<UserRole[]>(
ROLES_KEY,
[context.getClass(), context.getHandler()],
);
if (!requiredRole) {
return true
}
const req = context.switchToHttp().getRequest();
const user = req.user;
console.log('Before');
console.log(user)
return requiredRole.some((role) => {
if(role==="ADMIN"){
console.log("True")
}
return role === 'ADMIN'
});
}
}
The reason after return role==="ADMIN" then user can log but how can i get user before return role==="ADMIN execute ?
I can get user only after the role==="ADMIN" return only
Im using nest js working on RoleGuard