NestJS, Passport, Auth0, JWT and anonymous access
Introduction
In this short article, I will try to show you how you can use the mentioned stack together to get authentication working in your application quickly and easily, also including allowing anonymous access, while using global guard.
There are bits and pieces about these topics around docs, blogs and Github issues/gists, but I think it may make life easier to some of you to bring it together in one place.
What I will not write about?
I will not introduce you into NestJS, creating Auth0 account or so. I will focus on bringing these parts together in your application.
Let's do it!
Prerequisites
You have to have ready:
- NestJS application.
- Auth0 account with created API and Client app.
Bring it together
First, you have to install @nestjs/passport
and also jwks-rsa
.
Nest module is pretty self explaining — it helps you to bring Nest and Passport together easier, but second is not that obvious. This library exposes passportJwtSecret function which lets us to bring the keys from Auth0 to our application in a way which Passport understands. It will also cache it, so you will not download the keys on every request which would be really bad in terms of performance.
Passport strategy
Passport on its own and as a consequence, inside NestJS too, uses the concept of strategies. We have to create our custom strategy which will make it work with Auth0:
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://yourAuth0TenantDomain.auth0.com/.well-known/jwks.json`,
handleSigningKeyError: (err) => console.error(err), // do it better in real app!
}),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
audience: // your client app ID or empty in case of multiple audiences,
issuer: `https://yourAuth0TenantDomain.auth0.com/`,
algorithms: ['RS256'],
});
}
There is one thing which needs explanation — audience.
Yes, you want to validate the audience for sure, but this stack does not provide the possibility of multiple audiences out of the box, so we have to handle it in a custom way.
If your API is accessed by only one client — just put your clientId which you can get from Auth0 dashboard, but if you (as myself) need to use multiple clients (e.g. mobile app, web app, SPA application), leave audience field empty in the options, but don’t forget to validate it later on!
validate
function
In the strategy, you can provide a custom implementation of the validate
function, which allows you to do further validation of the token payload.
When the execution reaches this place, you are sure that the signature and expiration is validated, so the payload is safe to use.
If you need any dependency, like user’s repository, just add them to the constructor of the strategy and you are good to go, but have in mind that this function is called each and every time user makes a call to your API, so you definitely want to cache in memory some results, so you will not query your database every time.
async validate(payload: any) {
const { aud } = payload;
// get the LIST of audiences from config
if (!audiencesFromConfig.some((a) => a === aud)) {
throw new UnauthorizedException('Invalid audience.');
}
// This is available through your application as req.user
return {
externalId: payload[`${metaNamespace}guid`],
email: payload.email,
roles: payload[`${metaNamespace}roles`],
};
}
The payload is basically the decoded token, which you can for testing/learning purposes get by pasting your JWT token into the form on www.jwt.io.
You did probably spot roles: payload[`${metaNamespace}roles`]
.
You can add e.g. roles to your Auth0 tokens, but each and every addition should be namespace, which is always mentioned in Auth0 tutorials.
Adding roles to the token is pretty straight forward, just add similar code as a new rule in Auth0:
function (user, context, callback) {
context.idToken['https://your-namespace./roles'] = user.app_metadata.roles;
callback(null, user, context);
}
Remember: the value returned by the validate
function is injected into the requested object, so it is available through the application in req.user
, so inside the validate function you can make any custom roles, permissions resolve and then you can use these values in your custom guards.
NestJS Guard
Once you have your Strategy ready, you can create the guard. It is not super clear in the docs in terms of customizing the guards with dependency injection, so options could make your life hard, but the following solution will work and you can easily add any dependency which you need in the constructor.
export class JwtGuard extends AuthGuard('jwt') {
constructor(
@Optional() protected readonly options: AuthModuleOptions,
private readonly reflector: Reflector,
) {
super(options);
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// Handle anonymous access
const isAnonymousAllowed =
this.reflector.get<boolean>(
ALLOW_ANONYMOUS_META_KEY,
context.getHandler(),
) ||
this.reflector.get<boolean>(ALLOW_ANONYMOUS_META_KEY, context.getClass());
if (isAnonymousAllowed) {
return true;
}
return super.canActivate(context);
}
}
Anonymous Access
To explain the guard, we have to instantly jump into @AllowAnonymous
decorator.
import { SetMetadata } from '@nestjs/common';
export const ALLOW_ANONYMOUS_META_KEY = 'allowAnonymous';
export const AllowAnonymous = () => SetMetadata(ALLOW_ANONYMOUS_META_KEY, true);
All together — guard and the decorator, lets you use APP_GUARD
by default, while having some routes public.
The guard will work in a way, that it will skip any authorization process, when the route or the controller has @AllowAnonymous
decorator applied.
There is one catch which has to be mentioned here — while route is open to anonymous access this way, req.user
will not be set, as the strategy and validate
the function has not been executed. This can be mitigated in the way that will inject the user while still having the endpoint publicly available, but as IMO it is edge case I have decided to skip it at this moment, so if you need it, please let me know in comments, so I can cover it.
Set application guard
It is nicely explained in the docs, so I will just give you a short snippet to save you time if you don’t need anything more:
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
],
providers: [
JwtStrategy,
{ provide: APP_GUARD, useClass: JwtGuard },
],
})
export class AuthzModule {}
This is the basic Authentication/Authorization module setup. Add the providers/imports while you add dependencies to your guard or strategy.
Summary
I hope you have enjoyed this quick article and you now know how to bring NestJS, Auth0 and Passport together quickly and easily.