DEV Community

Cover image for Clerk JWT Authentication with NestJs + Passport for REST & GraphQL
Roberto Yamanaka
Roberto Yamanaka

Posted on

Clerk JWT Authentication with NestJs + Passport for REST & GraphQL

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

Clerk Dashboard JWT public key location

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

nest generate REST API

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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}!`;
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Good! Now let's do the GraphQL Endpoint

GraphQL Endpoint

$ nest g resource graphql-endpoints
Enter fullscreen mode Exit fullscreen mode

GraphQL NestJs step

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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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}!`;
  }
}

Enter fullscreen mode Exit fullscreen mode

Now lets go to http://localhost:3000/graphql and test it

Testing GraphQL query

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

And that's it! If we run

$ curl http://localhost:3000/rest-endpoints/12345
Enter fullscreen mode Exit fullscreen mode

We'll get a

{"message":"Unauthorized","statusCode":401}
Enter fullscreen mode Exit fullscreen mode

But when we add a Bearer Token from Clerk

$ curl -H "Authorization: Bearer <CLERK_BEARER_TOKEN_HERE>" http://localhost:3000/rest-endpoints/12345
Enter fullscreen mode Exit fullscreen mode

We get

REST Ping #12345!
Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

We get the expected behaviour!

Testing GraphQL with Clerk JWT

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


Github Repo

https://github.com/robertoyamanaka/clerk-w-nestjs

Top comments (0)