Options
All
  • Public
  • Public/Protected
  • All
Menu

Use with guards

Most of the times, you want to execute some guards before checking CASL policies, to extract some user informations before generating its abilities.

This page will show you the various way to do.

Let's assume you have the following CaslAbilityFactory:

From src/ability-factory.service.ts

import { AbilityBuilder, PureAbility } from '@casl/ability';
import { Injectable } from '@nestjs/common';

import { CaslAbilityFactory } from '@knodes/nest-casl';

@Injectable()
export class AbilityFactory implements CaslAbilityFactory {
// Here, \`request\` is the express or fastify request. You might get infos from it.
public createFromRequest( _request: unknown ): PureAbility {
const { user } = ( _request as any );
const abilityBuilder = new AbilityBuilder( PureAbility );
if( user?.role === 'admin' ) {
abilityBuilder.can( 'admin', 'something' );
}
return abilityBuilder.build();
}
}

The naïve approach

This method is not recommanded. You might skip directly to the recommended approach

Without any more dependencies

This method does not require any external dependency, and is as close to NestJS as possible.

From src/guards/naive.guard.ts

import { CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';

export class NaiveGuard implements CanActivate {
public canActivate( context: ExecutionContext ): boolean {
const request = context.switchToHttp().getRequest<Request>();
const authorization = request.header( 'Authorization' );
if( !authorization ){
// **BEWARE**: if the guard returns \`false\`, Nest will throw a \`ForbiddenException\` which is semantically incorrect here.
// The user is not *missing the right to do*, it is *not authenticated at all*.
throw new UnauthorizedException( 'Missing authorization header' );
}
const user = this._getUserFromAuthorization( authorization );
if( !user ){
return false;
}
( request as any ).user = user;
return true;
}

private _getUserFromAuthorization( authorization?: string ){
if( authorization === 'admin' ){
return { role: 'admin' };
} else if( authorization === 'user' ){
return { role: 'user' };
} else {
return undefined;
}
}
}

Assuming the guard does the following (see above for an example implementation):

  • Users without an Authorization header will see an Unauthorized exception.
  • Users with an invalid Authorization header will see a Forbidden exception.
  • Users with a valid Authorization header will be allowed to continue, and the property user is set on the request.

Yeah, I'm aware that in an ideal world, a Middleware or an Interceptor should take care of setting the user property on the Request. But fact is that @nestjs/passport does it in a guard

From src/controllers/naive.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';

import { Policy } from '@knodes/nest-casl';

import { NaiveGuard } from './naive.guard';

@Controller( '/naive' )
@Policy( { action: 'admin', subject: 'something' } ) // **MUST** be above the guard extracting infos from your request.
@UseGuards( NaiveGuard )
export class NaiveTestController {
@Get()
public method(){
// ...
}
}

Now,

  • Users without an Authorization header will see an Unauthorized exception (from the NaiveGuard).
  • Users with an invalid Authorization header will see a Forbidden exception (from the NaiveGuard).
  • Users with an Authorization header that does not give them access to the ressource will see a Forbidden exception (from the {@link PoliciesGuard PoliciesGuard}).
  • Users with an Authorization header that give them access to the ressource will succeed.

Using @nestjs/passport

If you're using passport, you can delegate the user retrieval. Let's say you have a JWT passport strategy.

From src/strategies/jwt-passport.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt';

@Injectable()
export class JwtPassportStrategy extends PassportStrategy( Strategy, 'jwt' ) {
public static readonly KEY = 'this-is-a-secret';
public constructor(){
super( {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JwtPassportStrategy.KEY,
} as StrategyOptions );
}

public validate( user: any ){
return user;
}
}

Still using the naive approach, you can then declare the controller like this:

From src/controllers/passport-naive.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

import { Policy } from '@knodes/nest-casl';

@Controller( '/passport/naive' )
@Policy( { action: 'admin', subject: 'something' } ) // **MUST** be above the guard extracting infos from your request.
@UseGuards( AuthGuard( 'jwt' ) )
export class PassportNaiveTestController {
@Get()
public method(){
// ...
}
}

The tests

Wanna see the behavior as tests ? Here you are !

From ./test/demo/use-with-guards/naive-test.e2e-spec.ts

import { HttpStatus, INestApplication } from '@nestjs/common';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { Test } from '@nestjs/testing';
import request from 'supertest';

import { CaslModule } from '@knodes/nest-casl';

import { AbilityFactory } from './ability-factory.service';
import { JwtPassportStrategy } from './jwt-passport.strategy';
import { NaiveTestController } from './naive.controller';
import { PassportNaiveTestController } from './passport-naive.controller';

describe( 'Use with guards (naive)', () => {
let app: INestApplication;

describe( 'NaiveTestController', () => {
beforeAll( async () => {
const moduleRef = await Test.createTestingModule( {
imports: [
CaslModule.withConfig( ( { abilityFactory: AbilityFactory } ) ),
],
controllers: [ NaiveTestController ],
} ).compile();

app = moduleRef.createNestApplication();
await app.init();
} );

it( 'should send an UNAUTHORIZED error if no authorization set', () => request( app.getHttpServer() )
.get( '/naive' )
.expect( HttpStatus.UNAUTHORIZED )
.expect( { statusCode: HttpStatus.UNAUTHORIZED, message: 'Missing authorization header', error: 'Unauthorized' } ) );
it( 'should send a FORBIDDEN error if invalid authorization', () => request( app.getHttpServer() )
.get( '/naive' )
.set( 'Authorization', 'invalid' )
.expect( HttpStatus.FORBIDDEN )
.expect( { statusCode: HttpStatus.FORBIDDEN, message: 'Forbidden resource', error: 'Forbidden' } ) );
it( 'should send a FORBIDDEN error if invalid capabilities', () => request( app.getHttpServer() )
.get( '/naive' )
.set( 'Authorization', 'user' )
.expect( HttpStatus.FORBIDDEN )
.expect( { statusCode: HttpStatus.FORBIDDEN, message: 'Invalid authorizations: Can\\'t "admin" on "something"', error: 'Forbidden' } ) );
it( 'should work', () => request( app.getHttpServer() )
.get( '/naive' )
.set( 'Authorization', 'admin' )
.expect( HttpStatus.OK ) );
} );
describe( 'PassportNaiveTestController', () => {
beforeAll( async () => {
const moduleRef = await Test.createTestingModule( {
imports: [
CaslModule.withConfig( ( { abilityFactory: AbilityFactory } ) ),
JwtModule.register( { secret: JwtPassportStrategy.KEY } ),
],
providers: [ JwtPassportStrategy ],
controllers: [ PassportNaiveTestController ],
} ).compile();

app = moduleRef.createNestApplication();
await app.init();
} );

it( 'should send an UNAUTHORIZED error if no authorization set', () => request( app.getHttpServer() )
.get( '/passport/naive' )
.expect( HttpStatus.UNAUTHORIZED )
.expect( { statusCode: HttpStatus.UNAUTHORIZED, message: 'Unauthorized' } ) );
it( 'should send a FORBIDDEN error if invalid capabilities', () => request( app.getHttpServer() )
.get( '/passport/naive' )
.set( 'Authorization', \`Bearer ${app.get( JwtService ).sign( { role: 'user' } )}\` )
.expect( HttpStatus.FORBIDDEN )
.expect( { statusCode: HttpStatus.FORBIDDEN, message: 'Invalid authorizations: Can\\'t "admin" on "something"', error: 'Forbidden' } ) );
it( 'should work', () => request( app.getHttpServer() )
.get( '/passport/naive' )
.set( 'Authorization', \`Bearer ${app.get( JwtService ).sign( { role: 'admin' } )}\` )
.expect( HttpStatus.OK ) );
} );
} );

The recommended approach

What's the problem about the naïve approach above ? Well, there are 2.

  1. The readability: The UseGuards decorator is placed below the Policy decorator, but is ran before it.
  2. The reusability: If you're using multiple authentication strategies in the same app, you might have to apply guards and the policies on each method depending on the context. Moreover, if you want to type-check your policies, you might prefer to have abilities presets.

About readability

You can use Policy.usingGuard or PoliciesMask.usingGuard to prepended guards to the decorator factory. Those calls are cumulative, and you can do multiple subsequent calls to join guards using an and condition. If you provide an array of guards, they be evaluated using an or condition.

From src/controllers/recommended.controller.ts

@Controller( '/recommended' )
@Policy( { action: 'admin', subject: 'something' } )
.usingGuard( AuthGuard( 'jwt' ) ) // The policy will run the guard before doing its own checks.
export class RecommendedTestController {
@Get()
public method(){
// ...
}
}

About reusability

You can even prepare a policy decorator with guards or PolicyDescriptor:

From src/policies.ts

export const AdminViaJwtPolicy = Policy( { action: 'admin', subject: 'something' } )
.usingGuard( AuthGuard( 'jwt' ) );
export const ViaJwtPolicy = Policy.usingGuard( AuthGuard( 'jwt' ) );

Then, use those presets in your controller:

From src/recommended-bound.controller.ts

@Injectable()
export class ExtraGuard1 implements CanActivate {
public canActivate( context: ExecutionContext ): boolean {
if( !context.switchToHttp().getRequest().user.allowed1 ){
throw new BadRequestException( 'Not allowed from ExtraGuard1' );
}
return true;
}
}
@Injectable()
export class ExtraGuard2 implements CanActivate {
public canActivate( context: ExecutionContext ): boolean {
return context.switchToHttp().getRequest().user.allowed2 ?? false;
}
}

@Controller( '/recommended/bound' )
export class RecommendedBoundTestController {
@Get( 'method1' )
@AdminViaJwtPolicy
.usingGuard( [ ExtraGuard1, ExtraGuard2 ] ) // Add guards. When passed as an array, if either one can activate, it will continue
public method1(){
// ...
}

@Get( 'method2' )
@ViaJwtPolicy( { action: 'admin', subject: 'something' } )
.usingGuard( [ ExtraGuard1, ExtraGuard2 ] ) // Add guards. When passed as an array, if either one can activate, it will continue
public method2(){
// ...
}
}

Now, both method1 and method2 will run if

  • the user is authenticated through jwt strategy
  • it passes either ExtraGuard1 or ExtraGuard2
  • and he has admin role.

Note that you can use bindPolicyDecorators to bind both Policy and PoliciesMask

The tests

This is tested, of course. Just see by yourself.

{@codeblock folded use-with-guards/recommended-test.e2e-spec.ts}

What next ?

Better type constraints

Generated using TypeDoc