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
Related
I implemented roles for my angular/nestjs/postgresql application ('Admin' and 'User') and i activated the the role guard for my getuserlist() to be only showable to admin upon login in yet it keeps guarding the list for both admin and user in addition of server getting shut down with the following error:
TypeError: Cannot read properties of undefined (reading 'roles')
at C:\Users\naceu\Desktop\MyDeveloperHub\projet internat\projet-internat\projet-internat1\backend\src\users\models\roles.guard.ts:23:45
//I made the guard with the following files :
//role.enum.ts
//role.decorator.ts
/role.guard.ts
//and in the controller file containing the getuserlist()
//this is the role.guard.ts:
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { Observable } from 'rxjs';
import { User } from './entities/user.entity';
import Role from './role.enum';
import { ROLES_KEY } from './roles.decorator';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requireRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requireRoles) {
return true;
}
// const { user } = context.switchToHttp().getRequest();
const request = context.switchToHttp().getRequest();
const user = request.user;
// return matchRoles(roles, user.roles);
return requireRoles.some((role) => user.roles.includes(role));
}
}
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);
}
}
How to to insert current logged in user to createdBy & lastChangedBy fields after creating/updating entity?
In my BaseEntity i've tried
#BeforeInsert()
async insertUser(#GetAuthUserPayload() userPayload: User) {
const user = await this.usersService.findOne({
where: { username: userPayload.username },
});
this.createdBy = user;
this.lastChangedBy = user;
}
But i've found out decorators work only in controllers(in entity they return undefined). Is there any other way than updating DTO in controller or using session?
Since i am using #nestjsx/crud i haven't found any other method than updating DTO. I've managed to solve this issue by creating BaseService:
import { TypeOrmCrudService } from '#nestjsx/crud-typeorm';
import { InjectRepository } from '#nestjs/typeorm';
import { Inject, Injectable, Scope, Type } from '#nestjs/common';
import { CrudRequest, Override } from '#nestjsx/crud';
import { DeepPartial } from 'typeorm';
import { REQUEST } from '#nestjs/core';
import { User } from '../users/entities/user.entity';
export interface IBaseService<T> {}
type Constructor<I> = new (...args: any[]) => I;
export function BaseService<T>(entity: Constructor<T>): Type<IBaseService<T>> {
#Injectable({
scope: Scope.REQUEST,
})
class BaseServiceHost extends TypeOrmCrudService<T> implements IBaseService<T> {
constructor(#InjectRepository(entity) repo, #Inject(REQUEST) readonly request: any) {
super(repo);
}
#Override()
createOne(req: CrudRequest, dto: DeepPartial<T>): Promise<T> {
return super.createOne(req, this.addCreatedByToDTO(dto));
}
#Override()
replaceOne(req: CrudRequest, dto: DeepPartial<T>): Promise<T> {
return super.replaceOne(req, this.addLastChangedByToDTO(dto));
}
#Override()
updateOne(req: CrudRequest, dto: DeepPartial<T>): Promise<T> {
return super.updateOne(req, this.addLastChangedByToDTO(dto));
}
private addCreatedByToDTO(dto: DeepPartial<T>): DeepPartial<T> {
const userUUID: Partial<User> = this.request.user.userUUID;
return { ...dto, createdBy: userUUID };
}
private addLastChangedByToDTO(dto: DeepPartial<T>): DeepPartial<T> {
const userUUID: Partial<User> = this.request.user.userUUID;
return { ...dto, lastChangedBy: userUUID };
}
}
return BaseServiceHost;
}
Later on i just extend my service like:
#Injectable()
export class ExampleService extends BaseService(ExampleEntity) {}
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
}
}