I am trying to make use of the AuthGuard
decorator, and the passport JWT strategy, following the documentation.
Everything in the documentation works great.
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.
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.
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';
}