Handle Clerk Authentication for back-end in TRPC + Next.js
Tags: nextjs, trpc, javascript, backenddevelopment
Published: 2024-05-17 17:50:51

## Introduction:
Hey fellow devs, hope you are rocking 💥. From the title, you know what the post is about because if you guys have used Clerk then you know the unclarity in their docs for the manual JWT verification for the backend API especially dealing with auth tokens in trpc along with refreshing the tokens as clerk `currentUser()` and `auth()` function doesn't return the current session values in trpc environment.

This article will clear all those doubts with code snippets so that you won't face the pitfalls like I did.

![No mistakes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qxkxw706dgdpvcl9j46z.gif)


## Pre-requisites:
- Intermediate at Next.js (with AppRouter).

- Set up Clerk account.

- Good knowledge about TRPC.

- Knowledge about JS, React, Next.js AppRouter, and RSC's.

## Setup:
Install Next.js, Clerk, and TRPC.

**Note:** For the clerk, I am using the old version with `authMiddleware()` function provided in the clerk's old version.

```bash
bunx create-next-app@latest | bun add @clerk/nextjs@^4.29.5 @clerk/themes@^1.7.9 @trpc/server @trpc/client @trpc/next jose axios superjson
```
This will create a new next.js project and during the setup **<u>please select the AppRouter version of next js</u>** otherwise things will break.

Then hit ```bash
bun run dev
``` to start the next.js dev server. 

Now once that's done go to your clerk's dashboard and get the secret and publishable keys, JWKS URL, JWT template name, and backend URL.

You can check out their [docs]("https://clerk.com/docs/backend-requests/making/jwt-templates") how to grab them is simple.

Also to get the publishable key check this [link out]("https://clerk.com/docs/quickstarts/nextjs") from the clerk docs.

Then go to your next js app in vs code and in your `.env` file add these variables and write their values accordingly.

```js
CLERK_SECRET_KEY = 'YOUR CLERK SECRET'
CLERK_JWKS_URL = 'FROM CLERK DASHBOARD'
CLERK_BACKEND_URL = 'FROM CLERK DASHBOARD'
CLERK_JWT_TEMPLATE = 'FROM CLERK DASHBOARD'
```

With all that done the folder structure of your app should be like this:


Our folder structure will be as follows:
```js
  ./src
├── app
│   ├── api
│   │   └── trpc
│   │       └── [...trpc]
│   │           └── route.ts
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── middleware.ts
└── server
    ├── context.ts
    ├── root.ts
    ├── routers
    │   ├── router.ts
    ├── index.ts
    └── utils
```

Now in the next section, we will set up clerk with next js and get the auth in the client side up and running.

## Clerk setup:

With a base empty project in hand lets add the magic of clerk to have auth running and it's as simple as adding components in our code just we are used to and taking full leverage of what clerk has to offer.

Inside the `layout.tsx` file inside the `./app` directory add the following piece of code: 

```tsx
import type { Metadata } from "next";
import { DM_Sans, Modak } from "next/font/google";
import "./globals.css";
import { ClerkProvider } from "@clerk/nextjs";
import { dark } from "@clerk/themes";

const font = DM_Sans({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Demo app",
  description: "Wiring up auth with clerk",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={font.className}>
        <ClerkProvider appearance={{ baseTheme: dark }}>
            {children}
        </ClerkProvider>
      </body>
    </html>
  );
}

```

So by adding this, we want to manage auth in the root level of our application and by adding this provider as `<ClerkProvider appearance={{ baseTheme: dark }}>` our server components will get access to session info managed by the clerk anywhere in our application.

With `appearance={{ baseTheme: dark }}` we configure to get a dark variant of the clerk dialogue screen for oAuth. You can choose light or system values based on your needs.

Now in your `middleware.ts` file, we will add logic to control what routes should be public and some pre and post-auth hooks that the clerk offers us out of the box.

**Note**: The method `authMiddleware()` function used in the `middleware.ts` file is deprecated but since you can have more granular control with this method I used this one.

So add the following code in `middleware.ts` file:

```tsx
import { authMiddleware } from "@clerk/nextjs";
import { NextResponse } from "next/server";

export default authMiddleware({
  publicRoutes: ["/", /api/trpc/(.*)"],
  async beforeAuth(auth, req) {},
  async afterAuth(auth, req) {
    // you can do anything here based on your needs.
    return NextResponse.next();
  },
});

export const config = {
  matcher: ["/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)", "/"],
};

```

With this, you made only `"/"` and any route in `"/api/trpc/"` as public routes, and any other route will be treated as a private route with no auth access and with 

```ts
export const config = {
  matcher: ["/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)", "/"],
};

```

this code next.js will only run the middleware function for routes having `/trpc` somewhere in the URL. 

Rest all other requests will simply pass and next.js won't run the middleware. 

Woah!! Now you are just one step away from adding the final piece to the puzzle and adding authentication in your app.


![Bare with me](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qj1qt4l8e66cwptqddp6.gif)

Go to your page.tsx file and now we will add a simple piece of code that shows how easy it is to wire up auth.

Then paste the following code:

```js

import React from 'react'
import { currentUser, SignIn } from '@clerk/nextjs'

const page = () => {
  const user = await currentUser();
  return (
   <>
    <SignIn />
    {user? <span>Auth works</span> : <span> Unauthenticated!!</span>}
   </>
  )
}

export default page
```
Now when users click the **sign in** button they will be redirected to the clerk's sign-in page (which you can customize on their dashboard).

See how easy it is to get user info after sign-in. This code only works on the server side so no leakage of sensitive stuff and now based on the user you can hide or show stuff in your UI.

You can also use `<SignedIn>` `<SignedOut>` components of the clerk to show UI before or after authentication. So neat isn't it?

Things become challenging when working with trpc as `currentUser()` function returns undefined and I don't know why this happens so now I will show you a way that seems a little complex but trust me I have put pieces together for you so you don't waste your time figuring what to do in that situation.

I am assuming you know how to structure trpc with next.js (AppRouter) and how it works. So I won't waste your time in explaining that.

## TRPC setup:

Okay so go to `server/context.ts` to make a context which is kinda like a state which can be used in our trpc routes to share data.

Paste this code in that file.

```js
import "server-only";

import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import { RequestCookie } from "next/dist/compiled/@edge-runtime/cookies";
import { cookies } from "next/headers";
import { db } from "@/lib/db";
import { User } from "@clerk/nextjs/server";
import { TRPCError } from "@trpc/server";
import { ClerkJWTStrategy } from "./utils/ClerkJWTStrategy";

interface ClerkAuthContext {
  clerkAuth?: { user?: User };
  clerkSessionCookie?: RequestCookie;
  strategy?: ClerkJWTStrategy;
  isRefreshing?: boolean;
}

interface CreateInnerContext {
  clerkSessionCookie?: RequestCookie;
  req?: Request;
  resHeaders?: Headers;
}

interface InnerContextReturnType extends ClerkAuthContext {
  db: SomeDBClient;
  resHeaders?: Headers;
}

export const createContextInner = async ({
  clerkSessionCookie,
  req,
  resHeaders,
}: CreateInnerContext): Promise<InnerContextReturnType> => {
  const sessToken = clerkSessionCookie?.value!;
  const jwksUrl = process.env.CLERK_JWKS_URL!;
  const bearerToken = process.env.CLERK_SECRET_KEY!;
  const clerkBEUrL = process.env.CLERK_BACKEND_URL!;

  const strategy = new ClerkJWTStrategy(sessToken, jwksUrl, clerkBEUrL, redis);

  if (req?.url.includes("refresh"))
    return {
      clerkSessionCookie,
      db,
      strategy,
      clerkAuth: { user: undefined },
      isRefreshing: true,
      resHeaders,
    };

  try {
    const payload = await strategy.startPayloadVerification();

    // you can use caching here to cache user by using any in-memory database

    return {
      clerkSessionCookie,
      db,
      clerkAuth: { user },
      strategy,
      resHeaders,
    };
  } catch (e) {
    const error = e as TRPCError;
    if (error.name === "JWTExpired") {
      throw new TRPCError({
        code: "PARSE_ERROR",
        cause: "Expired",
        message: "Token is expired need refresh",
      });
    }

    throw new TRPCError({
      code: error.code,
      cause: error.cause,
      message: error.message,
    });
  }
};

export const createContext = async (opts: FetchCreateContextFnOptions) => {
  const clerkSessionCookie = cookies().get("__session");
  const innerContext = await createContextInner({
    clerkSessionCookie,
    req: opts.req,
    resHeaders: opts.resHeaders,
  });

  return {
    ...innerContext,
  };
};

export type Context = Awaited<typeof createContext>;
```

We will handle this import `import { ClerkJWTStrategy } from "./utils/ClerkJWTStrategy";` later.

First we define interfaces for our context objects. The `ClerkAuthContext` interface includes user information and session cookies, while the `CreateInnerContext` interface handles request and response headers.

**Core Functions**: The `createContextInner` function is responsible for verifying user sessions and fetching user data. It uses Clerk's JWT strategy to verify the session token and retrieve user data either from a cache (for optimization)or directly from Clerk's backend using **JWKS endpoint** which is the alternative to `currentUser()` call. Comprehensive error handling is included to manage JWT expiration and other authentication errors.

The `createContext()` function wraps `createContextInner()` by integrating request-specific data, such as cookies and headers, to return a complete context for TRPC resolvers.

Let's handle `ClerkJWTStrategy` which is a class to manage strategy to manually manage JWT tokens. Go into `utils` and create a file `ClerkJWTStrategy.ts` and paste the following code:

```js
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
import { TRPCError } from "@trpc/server";
import { User } from "@clerk/nextjs/server";
import { convertResponse } from "@/lib/snakeCaseToCamel";
import axios from "axios";
import clerk from "@clerk/clerk-sdk-node";

export class ClerkJWTStrategy {
  constructor(
    private sessToken: string,
    private jwksUrl: string,
    private backendUrl: string
  ) {
    if (!sessToken) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
        message: "No session token provided. Request denied",
      });
    }
  }

  private generateJWKSSet = () => createRemoteJWKSet(new URL(this.jwksUrl));

  checkSession = () => {
    if (!this.sessToken) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
        message: "No session token provided. Request denied",
      });
    }
  };

  startPayloadVerification = async () => {
    const { payload } = await jwtVerify(this.sessToken, this.generateJWKSSet());
    const currentTimestamp = new Date().getTime() / 1000;

    if (!payload) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
        message: "Payload verification failed",
      });
    } else if (payload?.exp && currentTimestamp > payload?.exp) {
      throw new TRPCError({
        code: "PARSE_ERROR",
        cause: "TOKEN EXPIRED",
        message: "Session token expired, refresh token",
      });
    }

    return payload;
  };

  getUser = async (payload: JWTPayload, token: string) => {
    const response = await axios.get<User[]>(`${this.backendUrl}/users`, {
      params: {
        user_id: payload.sub,
      },
      headers: {
        Authorization: `Bearer ${token}`,
      },
      withCredentials: true,
    });

    const user = convertResponse(response.data[0]);
    return user;
  };

  refreshToken = async (sid: string, template: string) => {
    const jwtToken = (await clerk.sessions.getToken(sid, template)) as string;
    return jwtToken;
  };
}

```
We start by importing essential libraries and modules, such as jose for JWT verification, axios for HTTP requests, and Clerk's SDK for session management. Additionally, we import utilities to handle responses and error handling.

ClerkJWTStrategy class is the core of our authentication strategy. It takes a session token, JWKS URL, and backend URL as parameters. Upon initialization, it checks for the presence of a session token, throwing an unauthorized error if it's missing.

Methods:

`generateJWKSSet()`: Creates a JSON Web Key Set (JWKS) for verifying JWTs using the provided JWKS URL.

`checkSession()`: Ensures that a session token is present, throwing an error if it's not.

`startPayloadVerification()`: Verifies the session token's payload using the JWKS. It checks for the presence and expiration of the payload, throwing appropriate errors if verification fails or the token is expired.

`getUser()`: Retrieves user information based on the JWT payload. It sends a request to the Clerk's backend to fetch user data, which is then processed and returned.

`refreshToken()`: Retrieves a new JWT for a given session ID using Clerk's session management capabilities.


With that, our context is ready which was the difficult part and now we just simply create a trpc instance passing the context and then start working with the routers.

## Instantiate TRPC and TRPC Router:

Now go to `server/root.ts` file and paste the following code:

```js
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { Context } from "./context";

const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const {
  createCallerFactory,
  mergeRouters,
  router,
  procedure: publicProcedure,
} = t;

export const protectedProcedure = publicProcedure.use(async ({ ctx, next }) => {

// import and pass db if you have one...
  const { db, clerkAuth, clerkSessionCookie, strategy, isRefreshing } = ctx;

  if (isRefreshing) {
    if (clerkSessionCookie)
      return next({
        ctx: { clerkSessionCookie, db, strategy },
      });

    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "You are not allowed to access this route",
    });
  } 
  
  else if (!clerkAuth?.user)
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "You are not allowed to access this route",
    });

  return next({
    ctx: { clerkSessionCookie, db, clerkAuth, strategy },
  });
});
```

Using `initTRPC()`, we create a TRPC instance configured with **superjson** as the transformer. We define a custom errorFormatter to handle ZodError by flattening its structure for better error messages.

Next, we export key TRPC utilities including createCallerFactory, mergeRouters, router, and publicProcedure for defining public routes.

The `protectedProcedure()` is a middleware ensuring that only authenticated users can access certain routes. It checks the authentication context (clerkAuth, clerkSessionCookie, strategy, isRefreshing). If a user is refreshing their session, it proceeds with a minimal context. If the user is not authenticated, it throws an `UNAUTHORIZED` error.

This setup ensures secure and efficient handling of both public and protected routes in a TRPC server, with robust error formatting and user authentication.

Now let's create our first router which will just check whether user is authenticated or not.

So go to `server/routers.router.ts` and create a router.

```js
import { router } from "../root";

const appRouter = router({
  test: router({
    privateRoute: protectedProcedure.query(async({ctx}) => "Hello"),,
});

export type AppRouter = typeof appRouter;
export default appRouter;
```

Now this private procedure will only return "Hello" if user is authenticated otherwise `TRPCError` is thrown.

`ctx`: This is the context which holds our user and other data which needs to be shared between routes.


Let's now create an API client which we can use to call our API function.

Go to `server/index.ts` file and paste this code which will expose TRPC API client based on the needs.

```js
import "server-only";

import { RequestCookie } from "next/dist/compiled/@edge-runtime/cookies";
import { createCallerFactory } from "./root";
import appRouter from "./routers/router";

// if you have a db...
import { db } from "@/lib/db";

export const createAPICaller = (cookie?: RequestCookie) => {
  const createCaller = createCallerFactory(appRouter);
  return createCaller({ clerkSessionCookie: cookie, db });
};
```

also we need to make sure next.js understands TRPC requests and treat it as an api request so we also need to create a route handler.

Go to your `app/api/trpc/[...trpc]/route.ts` and paste the following code:

```js
import appRouter from "@/server/routers/router";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { createContext } from "@/server/context";


// serverless func
const handler = (req: Request) =>
  fetchRequestHandler({
    router: appRouter,
    endpoint: "/api/trpc",
    req,
    createContext,
    responseMeta(opts) {
      const { ctx, paths, errors, type, data } = opts;
      console.log("REQUEST TO PATHs:", paths);

      const allOk = errors.length === 0;
      const isQuery = type === "query";

      if (allOk && isQuery) {
        const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
        return {
          headers: {
            "cache-control": `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
          },
        };
      }

      return {};
    },

    onError({ error, path }) {
      // also logging service can be used to catch errors and 
         dev or prod
      console.error(`>>> tRPC Error on '${path}'`, error.message);
    },
    batching: {
      enabled: true,
    },
  });

export { handler as GET, handler as POST };
```

Here the serverless handler function processes incoming requests using fetchRequestHandler. It specifies:

**Router**: Uses appRouter for routing.

**Endpoint**: Sets the API endpoint to "/api/trpc".

**Context**: Uses `createContext` to build the request context.

**Response Metadata**: A `responseMeta()` function sets cache control headers for successful query requests, enabling one-day stale-while-revalidate caching. Here you can also redirect the request based on query params or route segment.

**Error Handling**: The `onError()` function logs any errors encountered, specifying the error path and message. You can also use this as a global error logger and connect it to your logging service.

**Batching**: Enables request batching to optimize network performance.

The handler is exported for both GET and POST requests.

Now everything is in place and we can use the `createAPICaller()` function to get access to api client to make api requests.

## Api Request:

Go to `app/page.tsx` file and do the following:

```js
import React from 'react'
import { currentUser, SignIn } from '@clerk/nextjs'
import { createAPICaller } from "@/server";

const page = () => {
  const user = await currentUser();

  // get clerk session cookie key to get its value
  const api = createAPICaller(cookies().get("__session")); 

  console.log(await api.test.privateRoute())  // "Hello"
  return (
   <>
    <SignIn />
    {user? <span>Auth works</span> : <span> Unauthenticated!!</span>}
   </>
  )
}

export default page
```

We now simply import the `createAPICaller()` pass the `__session` key and only this key as the clerk uses it to maintain the user's session. 

And now the API variable can be used to call our public or private trpc routes and `await api.test.privateRoute()` should return a "Hello" string.

You can also test this in your browser by typing the URL as `localhost:3000/api/trpc/test.privateRoute` and this should return you an object with a message property with the value "Hello". 

And congrats my friend that you have learned how to work with the clerk in a next.js (AppRouter) and TRPC environment.

## Conclusion:

Well now I think at this point you're a pro at trpc but I haven't covered how to refresh the tokens I have added the code in the TRPC section I think you can figure it out on your own. Thanks for reading.

Happy Coding!