If you're like me. You already love using Clerk to handle all Authentication for you in the Frontend.
However, I recently needed to connect my NextJs+Clerk frontend with a NestJs backend. The docs do a good job explaining how to implement a Manual JWT Verification for the backend endpoints. But I couldn't find specific info on how to implement it with NestJs.
Here is the solution I made using PassportJs
Get your Clerk Secret Keys
To connect our NestJs application with Clerk, we'll need the and the JWT public key from the Api Keys section
Copy and save the JWT public key without the "/.well-known/jwks.json" part.
We will need it to validate the JWT coming from the frontend.
NestJs basic setup
Let's create a new Nestjs project.
$ nest new clerk-w-nest
I'm gonna go ahead and choose npm but u can choose any you like.
REST endpoint
Now let's create some rest endpoints.
$ nest g resource rest-endpoints
Let's go ahead and create a new endpoint called pingOne under our Controller Class.
// src/rest-endpoints/rest-endpoints.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { RestEndpointsService } from './rest-endpoints.service';
@Controller('rest-endpoints')
export class RestEndpointsController {
constructor(private readonly restEndpointsService: RestEndpointsService) {}
@Get(':id')
pingOne(@Param('id') id: string) {
return this.restEndpointsService.pingOne(id);
}
}
And modify our Service Class.
// src/rest-endpoints/rest-endpoints.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class RestEndpointsService {
pingOne(id: string) {
return `REST Ping #${id}!`;
}
}
Now lets test the endpoint
Start the server npm run start:dev
And test it
$ curl http://localhost:3000/rest-endpoints/12345
REST Ping #12345!
Good! Now let's do the GraphQL Endpoint
GraphQL Endpoint
$ nest g resource graphql-endpoints
You'll also have to install these to use NestJs with GraphQL per the official documentation.
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
And add this line to your ./app.module.ts
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ApolloDriver } from '@nestjs/apollo';
import { RestEndpointsModule } from './rest-endpoints/rest-endpoints.module';
import { GraphqlEndpointsModule } from './graphql-endpoints/graphql-endpoints.module';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({ autoSchemaFile: true, driver: ApolloDriver }),
RestEndpointsModule,
GraphqlEndpointsModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
And now let's repeat the process from rest-endpoints
For your Resolver Class
// src/graphql-endpoints/graphql-endpoints.resolver.ts
import { Resolver, Query, Args } from '@nestjs/graphql';
import { GraphqlEndpointsService } from './graphql-endpoints.service';
import { GraphqlEndpoint } from './entities/graphql-endpoint.entity';
@Resolver(() => GraphqlEndpoint)
export class GraphqlEndpointsResolver {
constructor(
private readonly graphqlEndpointsService: GraphqlEndpointsService,
) {}
@Query(() => String, { name: 'pingOne' })
pingOne(@Args('id') id: string) {
return this.graphqlEndpointsService.pingOne(id);
}
}
For your Service Class
// src/graphql-endpoints/graphql-endpoints.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class GraphqlEndpointsService {
pingOne(id: number) {
return `GraphQL Ping #${id}!`;
}
}
Now lets go to http://localhost:3000/graphql
and test it
Good! Now to the interesting part
Creating Passport Auth Module
Let's start by creating a Nest Module for the Authentication
nest generate module authentication
Setting up Passport
Passport helps us setup our desired Authentication Strategy. We'll use the JSON Web Token (JWT) Passport Strategy to connect it with Clerk.
$ npm i passport @nestjs/passport passport-jwt jwks-rsa
The @nestjs/passport module helps us wrap this Strategy for NestJs.
To implement the Passport Strategy, first add the JWT issuer Url we copied from our Clerk Dashboard Api Keys section to our .env
file.
Next you'll need to install @nestjs/config
to access the env variable.
$ npm install @nestjs/config
Lets update our App module to use the .env
file.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ApolloDriver } from '@nestjs/apollo';
import { RestEndpointsModule } from './rest-endpoints/rest-endpoints.module';
import { GraphqlEndpointsModule } from './graphql-endpoints/graphql-endpoints.module';
import { GraphQLModule } from '@nestjs/graphql';
import { AuthenticationModule } from './authentication/authentication.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: [`.env`],
isGlobal: true,
}),
GraphQLModule.forRoot({ autoSchemaFile: true, driver: ApolloDriver }),
RestEndpointsModule,
GraphqlEndpointsModule,
AuthenticationModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
And now we can create our Passport Strategy
// src/authentication/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';
import * as dotenv from 'dotenv';
import { ConfigService } from '@nestjs/config';
dotenv.config();
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${configService.get(
'CLERK_ISSUER_URL',
)}/.well-known/jwks.json`,
}),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
issuer: `${configService.get('CLERK_ISSUER_URL')}`,
algorithms: ['RS256'],
});
console.log('JwtStrategy initialized');
}
validate(payload: unknown): unknown {
// This one is really useful to check the jwt payload!
// console.log('Validating payload:', payload);
return payload;
}
}
In NestJS, to make a Passport Strategy, you simply extend the class you get from the PassportStrategy function, which comes from @nestjs/passport. You tell it which strategy you're using by giving it the JWT Strategy from passport-jwt.
You pass the config for the strategy in the constructor via super(). This setup ensures we can decode JWT tokens and sets the API to recognize tokens with the RS256 sign.
RS256 is chosen because it's a widely-accepted RSA signature-based algorithm that provides strong security by using a private key for signing and a public key for verification, ensuring the JWT wasn't altered after its creation.
NestJS replaces the usual verify callback with a validate() method. But here's the cool part: Clerk does the hard part of authenticating the user. By the time validate()
is called, Clerk already knows who the user is and gives you their details in the payload. Your app then attaches this to the request, so you can use it anywhere, like in controllers or middleware.
The next step is to create our custom Authentication Guard that extends from Passport's AuthGuard.
// src/authentication/jwt-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
The JwtAuthGuard is a custom guard tailored for both REST and GraphQL in a NestJS application. It extends the default AuthGuard and specifies the 'jwt' strategy. The getRequest
method part helps integrate the guard with GraphQL. When invoked, it takes the current ExecutionContext, converts it to a GraphQL-specific context using GqlExecutionContext.create, and then extracts and returns the request (req) object. This ensures that our JWT authentication works seamlessly across both RESTful routes and GraphQL queries or mutations.
Lastly lets update our Authentication Module
// src/authentication/authentication.module.ts
import { Module } from '@nestjs/common';
import { JwtStrategy } from './jwt.strategy';
import { JwtAuthGuard } from './jwt-auth.guard';
@Module({
providers: [JwtStrategy, JwtAuthGuard],
})
export class AuthenticationModule {}
Good! We are done. Now lets add our custom guards to our endpoints to test them out.
In our src/rest-endpoints/rest-endpoints.controller.ts
we add the guard to our ping endpoint.
// src/rest-endpoints/rest-endpoints.controller.ts
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { RestEndpointsService } from './rest-endpoints.service';
import { JwtAuthGuard } from 'src/authentication/jwt-auth.guard';
@Controller('rest-endpoints')
export class RestEndpointsController {
constructor(private readonly restEndpointsService: RestEndpointsService) {}
@UseGuards(JwtAuthGuard) // This extra line
@Get(':id')
pingOne(@Param('id') id: string) {
return this.restEndpointsService.pingOne(id);
}
}
And that's it! If we run
$ curl http://localhost:3000/rest-endpoints/12345
We'll get a
{"message":"Unauthorized","statusCode":401}
But when we add a Bearer Token from Clerk
$ curl -H "Authorization: Bearer <CLERK_BEARER_TOKEN_HERE>" http://localhost:3000/rest-endpoints/12345
We get
REST Ping #12345!
If we try it out with our GraphQL Endpoint
// src/graphql-endpoints/graphql-endpoints.resolver.ts
import { Resolver, Query, Args } from '@nestjs/graphql';
import { GraphqlEndpointsService } from './graphql-endpoints.service';
import { GraphqlEndpoint } from './entities/graphql-endpoint.entity';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/authentication/jwt-auth.guard';
@Resolver(() => GraphqlEndpoint)
export class GraphqlEndpointsResolver {
constructor(
private readonly graphqlEndpointsService: GraphqlEndpointsService,
) {}
@UseGuards(JwtAuthGuard)
@Query(() => String, { name: 'pingOne' })
pingOne(@Args('id') id: string) {
return this.graphqlEndpointsService.pingOne(id);
}
}
We get the expected behaviour!
And that's it! Hope this was useful. I'll be posting more about Clerk and NestJs in the coming weeks so if ur into that follow me on Twitter
https://twitter.com/RobertoYamanaka
NOTE: Getting a Bearer Token from Clerk
You can find your Clerk Bearer token under localstorage
in your frontend as clerk-db-jwt
Top comments (0)