NestJS: Adding verification options to AuthGuard with JWT - javascript

I am trying to make use of the AuthGuard decorator, and the passport JWT strategy, following the documentation.
Everything in the documentation works great. But I now want to protect a route with a scope contained in the JWT. So here is a basic jwt payload generated by my application:
{
"user": {
"id": "20189c4f-1183-4216-8b48-333ddb825de8",
"username": "user.test#gmail.com"
},
"scope": [
"manage_server"
],
"iat": 1534766258,
"exp": 1534771258,
"iss": "15f2463d-8810-44f9-a908-801872ded159",
"sub": "20189c4f-1183-4216-8b48-333ddb825de8",
"jti": "078047bc-fc1f-4c35-8abe-72834f7bcc44"
}
Here is the basic protected route being guarded by the AuthGuard decorator:
#Get('protected')
#UseGuards(AuthGuard('jwt'))
async protected(): Promise<string> {
return 'Hello Protected World';
}
I would like to add options and restrict the access of that route to the people having the manager_server scope into their JWT. So after reading a little bit of the AuthGuard code, I thought that I was able to write something like:
#Get('protected')
#UseGuards(AuthGuard('jwt', {
scope: 'manage_server'
}))
async protected(): Promise<string> {
return 'Hello Protected World';
}
However, I can't see in the documentation where I could make use of this option.
I thought that adding an option argument to the validate function of the JWTStrategy could make the trick, but it does not. Here is my validate function (contained in the jwt.strategy.ts file):
async validate(payload: JwtPayload, done: ((err: any, value: any) => void)) {
const user = await this.authService.validateUser(payload);
if (!user) {
return done(new UnauthorizedException(), false);
}
done(null, user);
}
Thank you very much for your help and don't hesitate to ask me for more informations in the comments if you need so.

When you look at the code of the AuthGuard, it seems like the options.callback function is the only possible customization.
I think instead of writing your own AuthGuard that supports scope checks, it is cleaner to have a ScopesGuard (or RolesGuard) with its own decorater like #Scopes('manage_server') instead. For this, you can just follow the RolesGuard example in the docs, which also just checks an attribute of the JWT payload under the user property in the request.
Essential steps
Create a #Scopes() decorator:
export const Scopes = (...scopes: string[]) => SetMetadata('scopes', scopes);
Create a ScopesGuard:
#Injectable()
export class ScopesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const scopes = this.reflector.get<string[]>('scopes', context.getHandler());
if (!scopes) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const hasScope = () => user.scopes.some((scope) => scopes.includes(scope));
return user && user.scopes && hasScope();
}
}
Use the ScopesGuard as a global guard for all routes (returns true when no scopes are given):
#Module({
providers: [
{
provide: APP_GUARD,
useClass: ScopesGuard,
},
],
})
export class ApplicationModule {}
And then use it on an endpoint:
#Get('protected')
#UseGuards(AuthGuard('jwt'))
#Scopes('manage_server')
async protected(): Promise<string> {
return 'Hello Protected World';
}

I tried a slightly different approach, by extending the AuthGuard guard. I wanted to maintain the ability to use different Passport Strategies, so I included a mixin. Feedback is appreciated.
In your Jwt strategy you could simply return the JwtPaylozd so that the user has a scopes attribute. Then the custom AuthGuard looks like this:
import { UnauthorizedException, mixin } from "#nestjs/common";
import { AuthGuard } from "#nestjs/passport";
export function AuthScopes(scopes: string[], type?: string | string[]) {
return mixin(class ScopesAuth extends AuthGuard(type) {
protected readonly scopes = scopes;
handleRequest(err, user, info, context) {
if (err || !user) {
throw err || new UnauthorizedException();
}
if(!this.scopes.some(s => user.scopes.split(' ').includes(s)))
{
throw new UnauthorizedException(`JWT does not possess one of the required scopes (${this.scopes.join(',')})`);
}
return user;
}
});
}
You can then use this guard like so:
#Get('protected')
#UseGuards(AuthScopes(['secret:read'], 'jwt'))
async protected(): Promise<string> {
return 'Hello Protected World';
}
'jwt' represents the strategy.

Related

What would be the best way to create an isAuthor guard for nestjs?

I have been taking online course from udemy and playing around with the guard middleware
I also created the admin.guard auth.guard suggested by following the tutorial but I am thinking what if I want to add an isAuthor.guard that not only the admin can make changes to post or whatever but the original author is also able to make edits...
What would be a better way to create this? Should it be a guard? or middleware would be better?
P.S. I tried accessing services through guard with this post Inject service into guard in Nest.JS but didn't work for me.
Edit: Also, is it possible to have or guards?
For example isAdmin / isAuthor so it can be used flexible instead of having a isAdminOrAuthor
Thanks in advance for any suggestions / advices.
I do not know if it is the best way, but this one is one that seems practical (it is applicable to the larger scope than just isAdmin/isAuthor case). NOTE: If only isAdmin isAuthor case is needed, please move appropriate logic from PostRelationResolver up to RolesGuard and skip a whole generic approach.
A generic approach is provided here because it allows covering a far, far wider range of cases, that are of the same nature (there are users and any specific entity - relationship-based restriction needs to be applied).
So, to cover it.
Suppose that reading posts (just as an example) is restricted in a way that the Admin can see all of them and the author can see only their own posts.
It can be implemented like this:
#Get('read-post/:postId')
#UseGuards(RolesGuard)
#SetMetadata('general-roles', [GeneralRole.ADMIN])
#SetMetadata('relation-roles', [RelationRole.POST_AUTHOR])
readPostAsAuthor(
#Param('postId') postId: number,
) {
return this.postRepository.findPostById(postId);
}
And for a listing of posts, something like this:
#Get('read-all-posts')
async readAllPosts(
#Req() request
) {
const posts = await this.postRepository.findAll();
return this.rbacService.filterList(
request,
posts,
[GeneralRole.ADMIN],
[RelationRole.POST_AUTHOR]
);
}
Note for listing filter: One should make sure that implementation doesn't even respond with unallowed posts and this filter should be only utilized as backup (since the request doesn't contain enough information to restrict call).
For this to work, RolesGuard implementation is needed:
import { CanActivate, ExecutionContext, Injectable } from "#nestjs/common";
import { Reflector } from "#nestjs/core";
import { GeneralRole } from "../role/general-role";
import { RelationRole } from "../role/relation-role";
import { RbacService } from "../rbac.service";
#Injectable()
export class RolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private rbacService: RbacService,
) {
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const contextHandler = context.getHandler();
const request = context.switchToHttp().getRequest();
const requestedGeneralRoles = this.reflector.get<GeneralRole[]>('general-roles', contextHandler);
const requestedRelationRoles = this.reflector.get<RelationRole[]>('relation-roles', contextHandler);
return this.rbacService.authorize(request, requestedGeneralRoles, requestedRelationRoles);
}
}
The logic for actual authorization is contained in rbacService, given here:
import { Injectable } from "#nestjs/common";
import { GeneralRole } from "./role/general-role";
import { RelationRole } from "./role/relation-role";
import { UserRepository } from "./repository/user.repository";
import { CoreRelationResolver } from "./relation-resolver/core.relation-resolver";
#Injectable()
export class RbacService {
constructor(
private userRepository: UserRepository,
private coreRelationResolver: CoreRelationResolver,
) {
}
// NOTE: This method should be implemented however token to user mapping is done - based on business requirement.
async getUserByToken(token: string) {
return await this.userRepository.findByToken(token);
}
async authorize(request: any, requestedGeneralRoles: GeneralRole[], requestedRelationRoles: RelationRole[]) {
const user = await this.getUserByToken(request.headers['token']);
if (!user) {
return false;
}
if (requestedGeneralRoles && requestedGeneralRoles.indexOf(user.role) !== -1) {
// If user is of general role, it is simply allowed - regardless of relationRoles.
return true;
}
// Relation roles handling (user is not ADMIN - for example - but is author of post)
if (requestedRelationRoles) {
const relationRoles = await this.coreRelationResolver.getRelationRoles(user, requestedRelationRoles, request);
return this.isAllowed(requestedRelationRoles, relationRoles);
}
return false;
}
isAllowed(requestedRelationRoles: RelationRole[], containingRelationRoles: RelationRole[]) {
const matches = containingRelationRoles.filter(sr => {
return !!requestedRelationRoles.find(rr => rr === sr);
});
return !!matches.length;
}
async filterList(
request: any,
entities: any[],
requestedGeneralRoles: GeneralRole[],
requestedRelationRoles: RelationRole[]
): Promise<any[]> {
const user = await this.getUserByToken(request.headers['token']);
if (!user) {
return [];
}
if (requestedGeneralRoles && requestedGeneralRoles.indexOf(user.role) !== -1) {
return entities;
}
const result = [];
const relationResolver = await this.coreRelationResolver.findRelationResolver(requestedRelationRoles);
for (const entity of entities) {
const singleEntityRelations = await relationResolver.getRelations(user, entity);
if (this.isAllowed(requestedRelationRoles, singleEntityRelations)) {
result.push(entity);
} else {
console.warn("WARNING: Check next entity and query that responds with it. It shouldn't be here!");
console.warn(entity);
}
}
return result;
}
}
Allow me to provide a small description here before proceeding with the rest of the logic.
Authorization logic stops in RbacService.
CoreRelationResolver service is all about recognizing relations between the User that utilizes the application (makes a request) and the entity that is an object of the given operation (upon which operation is executed).
Possible relations between Users and specific entities are described with RelationalRoles. With RelationalRoles restriction is defined as: "only AUTHOR and COLLABORATOR of given Post can see it".
CoreRelationResolver implementation is provided here:
import { Injectable } from "#nestjs/common";
import { RelationRole } from "../role/relation-role";
import { IRelationResolver } from "./i-relation-resolver";
import { PostRelationResolver } from "./post.relation-resolver";
import { UserEntity } from "../entity/user.entity";
import { ClientAppRelationResolver } from "./client-app.relation-resolver";
#Injectable()
export class CoreRelationResolver {
private relationResolvers: IRelationResolver<UserEntity, unknown>[];
constructor(
private postRelationAuthorization: PostRelationResolver,
private clientAppRelationResolver: ClientAppRelationResolver,
) {
this.relationResolvers = [
this.postRelationAuthorization,
this.clientAppRelationResolver,
];
}
async getRelationRoles(user: UserEntity, requiredRelations: RelationRole[], request: any): Promise<RelationRole[]> {
let relationRoles = [];
const relationResolver = await this.findRelationResolver(requiredRelations);
if (relationResolver) {
const relatedObject = await relationResolver.getRelatedObject(request);
if (relatedObject) {
relationRoles = await relationResolver.getRelations(user, relatedObject);
}
}
return relationRoles;
}
async findRelationResolver(requiredRelations: RelationRole[]): Promise<IRelationResolver<UserEntity, unknown>> {
let result = null;
for (const relationResolver of this.relationResolvers) {
const supportedRelations = await relationResolver.getSupportedRelations();
const matches = supportedRelations.filter(sr => {
return !!requiredRelations.find(rr => rr === sr);
});
if (matches.length) {
result = relationResolver;
break;
}
}
return result;
}
}
It is designed in a way that in its constructor any RelationResolver (IRelationResolver interface) should be registered and properly implemented.
IRelationResolver interface:
import { RelationRole } from "../role/relation-role";
/**
* T - Type of user
* U - Type of relatedObject
*/
export interface IRelationResolver<T, U> {
/**
* Return RelationRoles that this resolver is responsible to handle.
*/
getSupportedRelations(): Promise<RelationRole[]>;
/**
* Retrieve related object from the request data.
*/
getRelatedObject(request: any): Promise<U>;
/**
* Calculate and provide relation between user and related object.
*/
getRelations(user: T, relatedObject: U): Promise<RelationRole[]>;
}
And finally, retrieving the related object and recognizing the relation between the user and the given object, is implemented here:
import { IRelationResolver } from "./i-relation-resolver";
import { Injectable } from "#nestjs/common";
import { RelationRole } from "../role/relation-role";
import { UserEntity } from "../entity/user.entity";
import { PostEntity } from "../entity/post.entity";
import { PostRepository } from "../repository/post.repository";
#Injectable()
export class PostRelationResolver implements IRelationResolver<UserEntity, PostEntity> {
constructor(
private postRepository: PostRepository
) {
}
async getSupportedRelations(): Promise<RelationRole[]> {
return [RelationRole.POST_AUTHOR];
}
async getRelatedObject(request: any): Promise<PostEntity> {
const postId: string = request.params.postId;
return await this.postRepository.findPostById(parseInt(postId));
}
async getRelations(user: UserEntity, relatedObject: PostEntity): Promise<RelationRole[]> {
const relations = [];
if (relatedObject.authorId === user.id) {
relations.push(RelationRole.POST_AUTHOR);
}
return relations;
}
}
Obviously, freedom is to implement here whatever is needed and however the relationship is defined.
For all next RBAC cases (for different entity types), one should create RelationResolver, implement it, and register it in the constructor of CoreRelationResolver.
All in all, considering the usability range, this approach should be flexible enough to be applied to many RBAC scenarios (and please consider it conceptual - there are no robustness features added).

Inject service into NestJS guard

We use ThrottlerGuard in our NestJS application from #nestjs/throttler package to rate limit connections and it's implemented like this:
#Injectable()
export class RateLimiter extends ThrottlerGuard {
RATE_LIMIT_EXCEEDED = 'Rate limit exceeded.';
async handleRequest(context: ExecutionContext, limit: number, ttl: number) {
const wsData = context.switchToWs().getData();
const metadata: MetadataObject = wsData.internalRepr;
const clientTokenMetadata = metadata.get(TOKEN_NAME) as MetadataValue[];
const clientToken = clientTokenMetadata[0].toString();
const tokensArray = await this.storageService.getRecord(clientToken);
if (tokensArray.length >= limit) {
throw new RpcException({
code: Status.UNAVAILABLE,
message: this.RATE_LIMIT_EXCEEDED,
});
}
await this.storageService.addRecord(clientToken, ttl);
return true;
}
}
Now, I need to inject another service, let's say ConfigService with the usage of constructor, but because it is an extended class, I need to call super(); method which required three more original arguments inherited from ThrottlerGuard, but they are not exported from the package. Is there any other way to make it work besides the method with DI in constructor?
What you can do, instead of extending ThrottleGuard is injecting it in your constructor and call canActivate() in handleRequest(). Benefit when not extending you don'
I haven't tested this but you can try the following:
import {ExecutionContext, Injectable} from "#nestjs/common";
import {ConfigService} from "#nestjs/config";
import {InjectThrottlerStorage, ThrottlerGuard, ThrottlerStorage} from '#nestjs/throttler'
#Injectable()
export class RateLimiter {
constructor(
private throttle: ThrottlerGuard,
private configService: ConfigService,
#InjectThrottlerStorage() private storage: ThrottlerStorage) {
}
async handleRequest(context: ExecutionContext, limit: number, ttl: number) {
// TODO something with ConfigService
return this.throttle.canActivate(context);
}
}
ThrottleGuard source code

NestJS setMetadata not working from authGuard

I have a few decorators that when called, I want to setMetadata to use it in my logging,
In my controller, I have these:
#Post("somePath")
#Permission("somePermission")
#UseGuards(JwtAuthGuard)
#HttpCode(200)
#Grafana(
"endpoint",
"functionalmap"
)
async getSubscriptionFromPwebFormFilter(
#Body(ValidationPipe) someDto: someDtoType
): Promise<ISuccessResponse> {
// some logic
}
In my decorators I want to set some data into the metadata to use in my logging inteceptor,
Grafana decorator:
export const Grafana = (functionalMap: string, endpoint: string) =>
applyDecorators(
SetMetadata("endpoint", endpoint),
SetMetadata("functionalMap", functionalMap)
);
AuthGuard decorator:
#Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(
private readonly reflector: Reflector,
private readonly someService: SomeService
) {
super();
}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const role = this.reflector.get<string>("permission", context.getHandler());
const request = context.switchToHttp().getRequest();
const { valueToLog } = request.body;
const jwtToken = request.headers.authorization;
console.log("check value exist", valueToLog);
SetMetadata("valueToLog", valueToLog);
}
Now, in my logging interceptor, I am getting all the values of the metadata this way:
#Injectable()
export default class LoggingInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler) {
const executionStart = Date.now();
return next.handle().pipe(
tap((responseData) => {
const response = context.switchToHttp().getResponse<ServerResponse>();
const { statusCode } = response;
const valueToLog = this.reflector.get(
"valueToLog",
context.getHandler()
); // this is undefined
const endpoint = this.reflector.get("endpoint", context.getHandler()); // this have value
const functionalMap = this.reflector.get(
"functionalMap",
context.getHandler()
); // this have value
...
// some logic
})
);
}
}
In my case, the value of endpoint and functionalMap can be retrieved from the reflector, however, valueToLog is appearing as undefined,
Is setting of metadata not working for auth guard decorator?
I will be using the term "Decorator" in this explanation so I will first define it.
Decorators are functions that accept information about the decorated declaration.
Ex:
function ClassDecorator(target: typeof DecoratedClass) {
// do stuff
}
// it can then be used like this
#ClassDecorator
class DecoratedClass {}
when calling #SetMetadata(key, value) it returns a decorator. Functions like SetMetadata are referred to as Decorator Factories.
Decorators use the form #expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.
In your example you called SetMetadata("valueToLog", valueToLog). This returns a decorator. Decorators must be called with information about the decorated declaration. To actually attach the metadata you can do something like this:
SetMetadata("valueToLog", valueToLog)(context.getHandler());

AuthGuard loaded before AuthService

I'm working on a small personal app. I'll explain what I did until now and in the end my problem and my question.
I have created a Node server and an Angular app.
When the Angular app is booting I'm checking if the user is logged in (via http get request to the server, the request is made in app.component.ts)
ngOnInit(): void {
this.authService.checkIfUserSignedIn();
}
Inside the checkIfUserSignedIn method after that I'm getting the relevant authentication information I notify to the interested components with the auth state.
this.userAuthDetailsSubject.next(this.userAuthDetails);
Additionally, I'm having an AuthGuard that restrict the entry to the "create-list" component only to authenticated users.
In the AuthGurad I'm checking the auth state:
const authStatus = this.authService.isAuth();
return authStatus;
In the menu html component I have the following code:
<span routerLink="create-list" *ngIf="userIsAuthenticated"> New List</span>
Which works fine.
My problem is when i'm visiting manually localhost:4200/create-list
The AuthGuard is probably loaded before auth state is updated and therefore the user has no access to the "create-list" component, although he is signed in eventually.
I thought about two solutions but I'm not sure if they are good and how to implement them, and would like to hear your opinion.
using localStorage (It may be an overkill solution for this tiny problem)
make the HTTP get request to the server (for the auth state) inside the authGuard or maybe subscribe to an observer in the auth service (if so, how to implement that?)
Any ideas/solutions?
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | import("#angular/router").UrlTree | import("rxjs").Observable<boolean | import("#angular/router").UrlTree> | Promise<boolean | import("#angular/router").UrlTree> {
const authStatus = this.authService.isAuth();
if (authStatus) {
return true;
} else {
this.router.navigate(['/login']);
}
}
auth.service.ts
#Injectable()
export class AuthService {
userAuthDetailsSubject = new Subject<UserAuthDetails>();
userAuthDetails: UserAuthDetails = null;
private isAuthenticated = false;
constructor(#Inject(DOCUMENT) private document: Document, private http: HttpClient) {
};
public isAuth(): boolean {
console.log({
isAuth: this.isAuthenticated
})
return this.isAuthenticated;
}
signIn() {
// redirect to signin..
this.document.location.href = '/auth/google';
}
signOut() {
this.document.location.href = '/auth/logout';
}
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated = true;
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
}
For this particular problem you can make the 'isAuthenticated' field a subject just like 'userAuthDetailsSubject' and update its value when the server responds.
auth.service.ts
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated.next(true); //update the value
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
Now change your authguard so it does not return true or false synchronously.
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
boolean | import("#angular/router").UrlTree |
import("rxjs").Observable<boolean | import("#angular/router").UrlTree>| Promise<boolean | import("#angular/router").UrlTree> {
return this.authService.isAuth().subscribe((logged)=>{
if (logged) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
})
}
Off topic:
Why do you use import("#angular/router").UrlTree? You can use import like import { UrlTree } from '#angular/router';
CanActive support UrlTree return. return this.router.createUrlTree(['/login']); and not create a new async process in your canActivate
On Topic:
If you call direct link, you have to resolve authentication. If you call link or F5 reload browser will lost every data from memory. If you use any token to auth it be worth saving into localStore and restore from here.
Ofc, After authentication if you open new tab, this new tab will new without auth default like you used F5 on current tab. It lives a separate life for each tabs.

NestJs - Unable to get user context in RolesGuard

I'm using NestJS as the framework for a client API. Within the framework we are using a pretty standard Passport/JWT auth infrastructure that is working fine. Our AuthGuard is firing when the bearer token is found and, in secure API endpoints, I can inject the HTTP context via '#Res() request' and get access to the 'request.user' property which contains the payload of my Jwt token.
On top of this we are attempting to implement a 'RolesGuard' in a very similar fashion to the sample code provided in the documentation and some of the sample projects on GitHub (none of which actually use this guard but they include it as a sample guard).
Our issue is that our AuthGuard fires and validates the Jwt token and THEN our RolesGuard fires but the request object it is passed does not have the user meta-data attached to the request.
The key code in our RolesGuard is:
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
In the above snipped the user is always false. Has anyone written a role/permission based guard in Nest that successfully gets access to the scope of the current user? All the code is firing and everything appears registered correctly.
-Kevin
Ultimately this appears to be an ordering issue with the guards and it doesn't look like it can be easily resolved (without the framework allowing some control over the ordering).
My hope was to register the RolesGuard globally but that causes it to be registered first and fire first.
#UseGuards(AuthGuard('jwt'), RolesGuard)
#Roles('admin')
If I register it at the endpoint level and put it after the AuthGuard then it fires second and I get the user context I am expecting within the guard itself. It isn't perfect but it works.
-Kevin
register RoleGuard at the endpoint level and put it after the AuthGuard then it fires second and I get the user context I am expecting within the guard itself.
don't register RoleGuard at module causes it'll be registered first and fire first.
*.module.ts
imports: [],
providers: [{provide: APP_GUARD, useClass: RolesGuard} ,], // remove guard
controllers: [],
exports: [],
Make your RolesGuard extend AuthGuard('StrategyName') and then call super.canActivate for example:
#Injectable()
export class RolesGuard extends AuthGuard('jwt') {
async canActivate(context: ExecutionContext): Promise<boolean> {
// call AuthGuard in order to ensure user is injected in request
const baseGuardResult = await super.canActivate(context);
if(!baseGuardResult){
// unsuccessful authentication return false
return false;
}
// successfull authentication, user is injected
const {user} = context.switchToHttp().getRequest();
}
}
In other words you have to Authenticate first then Authorize
If anyone else stumbles across this question: putting multiple guards into one #UseGuards decorator works, but if you want to keep them separated (say, if you use a custom decorator), you can give the 2nd guard access to req.user by placing it before the #UseGuards call that puts the user on the request object, as in this example:
#RestrictTo(UserAuthorities.admin)
#UseGuards(JwtAuthGuard)
#Get("/your-route")
It seems that this is a consequence of how decorators work in TypeScript.
You can also use multiple roles for role-based Authentication.
In UserResolver
import { Args, Mutation, Query, Resolver } from '#nestjs/graphql';
import { UseGuards } from '#nestjs/common';
import { RolesGuard } from 'src/guards/auth.guard';
#UseGuards(new RolesGuard(['admin']))
#Resolver()
export class UserResolver { ... }
In RolesGuard
import { ExecutionContext, Injectable, UnauthorizedException } from '#nestjs/common';
import { ExecutionContextHost } from '#nestjs/core/helpers/execution-context-host';
import { GqlExecutionContext } from '#nestjs/graphql';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class RolesGuard extends AuthGuard('jwt') {
constructor(private roles: string[] | null) {
super();
}
canActivate(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const { req } = ctx.getContext();
return super.canActivate(new ExecutionContextHost([req]));
}
handleRequest(err: any, user: any, info: string) {
if (!this.roles) {
return user || null;
}
if (!user) {
throw new UnauthorizedException('Not Valid User.');
}
const role = user.role;
const doesRoleMatch = this.roles.some(r => r === role);
if (!doesRoleMatch) {
throw new UnauthorizedException('Not Valid User.');
}
return user;
}
}

Categories

Resources