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;
}
}
Related
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.
I have an entity Audit Like below in Nestjs app with typeorm for mongodb:
#Entity()
export class Audit {
#Column()
createdBy: string;
#BeforeInsert()
setAudit() {
this.createdBy = ???
}
}
#Entity()
export class Post extends Audit {
#Column()
title: string;
...
}
My other entities extend Audit and in my application i'm using jwt to authenticate users.
The problem is that when i want to save an entity i don't know how to set createdBy with #BeforeInsert hook...
I know we have user in request but i don't know what is the correct way of bringing user into setAudit method?
You can't access the request directly from inside a TypeORM entity, as they are in separate contexts. You could, instead, fill the createdBy field through a service.
#Injectable()
class DataService {
// ...
async create({ request, user }): Promise<any> {
await this.repository.create({...request.body, createdBy: user.id });
}
// ...
}
Another solution would be creating a common service that can work for every entity, and inside that service you could fill the createdBy property.
/*
* DataService
? Service to be able to fetch any kind of TypeORM entity from the controllers
? or another services.
*/
#Injectable()
export class DataService {
/*
* _.getRepository
? Method to get a repository by having only the type of an entity.
* #param type The type of the entity
* #returns a repository of the specified entity.
*/
private getRepository<T>(type: new () => T) {
return getRepository(type)
}
/*
* _.save
? A method to save an entity regarding its entity type.
* #param type the type of the entity
* #param model the model to save. It should have an ID.
* #param user the user creating this entity
* #returns a DeepPartial object representing the edited or created object.
*/
public save<T extends Audit>(type: new () => T, model: DeepPartial<T>, user: User): Promise<DeepPartial<T> & T> {
const repository: Repository<T> = this.getRepository(type);
return repository.save({...model, createdBy: user.id});
}
}
Then, you will be able to do something like this...
// ...
#Post('create')
#UseInterceptor(FillUser)
public async create(#Body() body:any, #Req() request:any) {
const { user } = request.session.user // <--- Just for example.
return await this.service.create(Post, body, user)
}
// ...
I would like to share my solution regarding this issue.
Since EventSubscribers can't be request scoped you have to use some kind of storage to store the user information coming from the request. This module can help you separate context for every single request. The problem with other implementation I tried was that my scope was global, so other requests could overwrite the user, while the original request was handled. I could have thought that...
Base entity with audit columns (other entities will extend this class):
export class BaseEntity extends DateEntity {
#Column({ nullable: false })
createdBy: string;
#Column({ nullable: true })
updatedBy: string;
}
Interceptor (this is where you can set your user information):
#Injectable()
export class UserInterceptor implements NestInterceptor {
constructor(private readonly cls: ClsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const user = request.user;
this.cls.set('user', user?.username);
return next.handle();
}
}
EventSubscriber (this will listen to every insert and update event for the entities extended from BaseEntity):
#EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<BaseEntity> {
constructor(dataSource: DataSource, private readonly cls: ClsService) {
dataSource.subscribers.push(this);
}
listenTo() {
return BaseEntity;
}
beforeInsert(event: InsertEvent<BaseEntity>) {
event.entity.createdBy = this.cls.get('user');
}
beforeUpdate(event: UpdateEvent<BaseEntity>) {
event.entity.updatedBy = this.cls.get('user');
}
}
Register the subscriber and the interceptor as a provider in your app.module globally (or use it with #UseInterceptor():
providers: [
UserSubscriber,
{
provide: APP_INTERCEPTOR,
useClass: UserInterceptor,
},
],
Also you have to import the ClsModule for your app:
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
I hope this will help. For further information please see the docs.
I followed a lot of threads on this particular problem. For me, the issue is that the application uses URL versioning combined with REQUEST-scoped database connections.
My goal was to capture the authorized user id into a SESSION variable at the start of each transaction. The application has a trigger operation that captures the before/after images of row-level changes. By referencing that SESSION variable in the trigger, it would become possible to record what user made the change, without having to modify existing tables to include that property for capture. Even if the row was modified to include that metadata, there is no provision to capture the user id in the DELETE event.
Since REQUEST-scoped injection cannot be mixed with TypeORM EventSubscribers, not a lot of the existing docs was very promising. I followed Masterchief example above and got pretty far.
Ultimately, I came to the conclusion that I needed to register the EventSubscriber twice.
In the first instantiation, by way of the app global module, the EventSubscriber gets access to the properly DI-injected ClsService.
But, because the connection is REQUEST-scoped, there is no datasource passed to the EventSubscriber to subscribe to.
To overcome that, the EventSubscriber is also present in the TypeORM config to inject it as a subscriber.
The second instantiation comes from the TypeORM code.
As a result, both Nestjs and TypeORM instantiate the EventSubscriber, but through a simple hack, we return the first instance to TypeORM, giving it the properly DI-injected, REQUEST-scoped EventSubscriber.
import { Inject, Injectable } from '#nestjs/common';
import { ClsService } from 'nestjs-cls';
import { EntitySubscriberInterface, EventSubscriber, TransactionStartEvent } from 'typeorm';
import { BaseEntity } from '../../../base.entity';
let instance: UserIdSubscriber;
#EventSubscriber()
#Injectable()
export class UserIdSubscriber implements EntitySubscriberInterface<BaseEntity> {
constructor(#Inject(ClsService) public cls: ClsService) {
instance ||= this;
return instance;
}
async beforeTransactionStart(event: TransactionStartEvent): Promise<void> {
await event.queryRunner.query(
`SET SESSION auditing_trigger_parameters.user_id = ${this.cls.get('userId')}`,
);
}
}
I am trying to set up an integration test which will grab some data from a backend API service using ngrx/data entities.
I have this StackBlitz set up: https://stackblitz.com/edit/ngrxdata-testing-not-working-pxfmkb?file=src/main.ts
It should run tests on startup - there are no expectations in my test cases, however I am looking in the console logs and expecting it to show the log in ClientDataService (/src/app/data/client/client-data.service.ts), that is:
console.log('never goes here :(');
In the integration test (data.integration.spec.ts) I am configuring the module, defining the Client entity type and including the AppDataServiceModule which in turn does this:
import { NgModule } from '#angular/core';
import { ClientDataService } from './client/client-data.service';
import { EntityDataService, EntityDefinitionService } from '#ngrx/data';
#NgModule({
providers: [
ClientDataService,
],
})
export class AppDataServiceModule {
constructor(
entityDataService: EntityDataService,
clientDataService: ClientDataService
) {
entityDataService.registerService('Client', clientDataService);
}
}
As you can see I am registering the data service as suggested by the ngrx docs, here
I feel like I am pretty close but just need a nudge in the right direction to get it working!
A custom DataService has to extend the DefaultDataService. Should look something like this:
export class ClientDataService extends DefaultDataService<Client> {
constructor(
http: HttpClient, httpUrlGenerator: HttpUrlGenerator
) {
super('Client', http, httpUrlGenerator);
}
public getAll(): Observable<any> {
// get Data here
}
}
The BackendService has to return the Observable:
public getClients(): Observable<Array<Client>> {
// will be mocked
return of([
{
id: '1: Will not return as it will be mocked'
},
{
id: '2: Will not return as it will be mocked'
}
])
}
There are two more things which look suspicious to me:
There is no subscription in the code, so I assume your Observable is cold.
The clientResolve.resolve({}, {}) call expects an ActivatedRouteSnapshot as first parameter. I'm not so familiar with the Resolve interface but maybe thats an issue too.
i want to show the page after getting the results of two
different service calls.service1 and service2 are two different
services
don't want to use second service call inside of first service
subscribe.service1 and service2 are two different services.
this.service1.getProfile1(id).subscribe((data1) => {
console.log(data1);
});
this.service2.getProfile2(id).subscribe((data2) => {
console.log(data2);
});
how to i found i got both service calls ?
You can use forkJoin from rxjs https://www.learnrxjs.io/operators/combination/forkjoin.html
import { forkJoin } from 'rxjs';
forkJoin(
this.service1.getProfile1(id),
this.service2.getProfile2(id)
).subscribe(([profile1, profile2]) => {
console.log(profile1, profile2);
});
You can merge the two observables with a fork join in this way:
import { forkJoin } from 'rxjs';
forkJoin([
this.service1.getProfile1(id),
this.service2.getProfile2(id),
]).subscribe(r => {
const data1 = r[0];
const data2 = r[1];
console.log(data1);
console.log(data2);
});
The calls are serialized, and then observable returns with an array of results. The position of the items reflects the order you created the forkJoin.
Actually in this particular use case you are navigating the user and trying to make a service call with two endpoints. Once you receive a response from those end point you are trying to merge it with fork join and send it to component as a observable
But in your question you are looking for a way to make the http calls even before redirecting to the page. There is a good approach for this use case in angular router.
You can specify what is the http calls which you want to perform even before user taken to a page.
Implement resolve in your service level and define that service under route resolve.
Example :
import { Injectable } from '#angular/core';
import { APIService } from './api.service';
import { Resolve } from '#angular/router';
import { ActivatedRouteSnapshot } from '#angular/router';
#Injectable()
export class APIResolver implements Resolve<any> {
constructor(private apiService: APIService) {}
resolve(route: ActivatedRouteSnapshot) {
return this.apiService.getItems(route.params.date);
}
}
Routes :
{
path: 'items/:date',
component: ItemsComponent,
resolve: { items: APIResolver }
}
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.