DEV Community

Yinka Adedire
Yinka Adedire

Posted on

Implementing Authorization with Clerk in a tRPC app running on a Cloudflare Worker

While integrating integrating Clerk into a tRPC app on NextJS is documented in the guide Using tRPC and Next.js. I wanted to deploy my tRPC server as a standalone service. So the API could be used for non-browser clients, in my case, a React Native app. I opted to use Cloudflare Workers.

I was able to get the tRPC app up and running, but I ran into some issues with the Clerk integration. I was able to get it working, but I had to do some digging to figure out how to get it working. I figured I'd share what I learned.

Prerequisites

  • Basic knowledge of tRPC is assumed. If you're not familiar with tRPC, I recommend checking out tRPC docs
  • A Clerk account. You'll need to create a Clerk app and get your Clerk API key.
  • A Cloudflare account to deploy the API.

Authorization Strategy

To integrate Clerk into tRPC, we'll use the following authorization strategy:

  1. The user authenticates on the frontend and receives a Clerk session Token.
  2. User sends the session token along with every request to the tRPC API through the Authorization header.
  3. tRPC validates the session token and adds the user data to the tRPC context if the token is valid or null if the token is invalid.
  4. A tRPC auth middleware checks if the user data is in the tRPC context. If it is, the user is authenticated and the request is allowed to continue. If not, the user is not authenticated and the request is rejected.
  5. A protectedProcedure is created which uses the tRPC auth middleware to protect the procedure. If the user is authenticated, the procedure is allowed to continue. If not, the procedure is rejected.

Set up tRPC

After your project is initialized, you'll need to install the tRPC server package and zod for validation. Navigate to your project directory and run the following command:

Install tRPC

npm install @trpc/server zod
Enter fullscreen mode Exit fullscreen mode

Next, create a trpc.ts file in the src directory. This is where we'll set up our tRPC server and context. Add the following code to the file:

import { initTRPC, inferAsyncReturnType } from "@trpc/server";
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";

// context holds data that all of your tRPC procedures will have access to
export const createContext = async (options: FetchCreateContextFnOptions) => {
  return {};
};

// initialize tRPC
const t = initTRPC.context<typeof createContext>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
Enter fullscreen mode Exit fullscreen mode

Create a tRPC router

Next, create a router.ts file in the src directory. This is where we'll define our tRPC routers. Add the following code to the file:

import { z } from "zod";
import { router, publicProcedure } from "./trpc";

// example router
export const exampleRouter = router({
  sayHello: publicProcedure
    .input(
      z.object({
        name: z.string(),
      })
    )
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.name}`,
      };
    }),
});

// app router
export const appRouter = router({
  example: exampleRouter,
});

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode

Create a Cloudflare Worker project

You can initialize a new Cloudflare Worker project by running the npm create cloudflare@2 -- <project-name> command in your terminal and answering the prompts that follow. I named my project trpc-clerk-cloudflare-worker. Choose the hello-world template when prompted. In my case, I also chose to use TypeScript.

npm create cloudflare@2 -- trpc-clerk-cloudflare-worker
Enter fullscreen mode Exit fullscreen mode

Startup the tRPC server with Cloudflare Worker

Next, navigate to the entry point of your project, which is located at src/index.ts by default. Add the following code to the file:

import { appRouter } from "./router";
import { createContext } from "./trpc";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

export default {
  async fetch(request: Request): Promise<Response> {
    const response = await fetchRequestHandler({
      req: request,
      createContext,
      endpoint: "/trpc",
      router: appRouter,
      onError({ error, path }) {
        console.error(`tRPC Error on '${path}'`, error);
      },
    });

    return response;
  },
};
Enter fullscreen mode Exit fullscreen mode

Handle CORS and CORS preflight requests

Lastly, we need to handle CORS and CORS preflight requests. Add the following code to the src/index.ts file:

import { appRouter } from "./router";
import { createContext } from "./trpc";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method === "OPTIONS") {
      return handleCORSPreflight();
    }

    const response = await fetchRequestHandler({
      req: request,
      createContext,
      endpoint: "/trpc",
      router: appRouter,
      onError({ error, path }) {
        console.error(`tRPC Error on '${path}'`, error);
      },
    });

    return addCORSHeaders(response);
  },
};

const addCORSHeaders = (res: Response) => {
  const response = new Response(res.body, res);
  response.headers.set("Access-Control-Allow-Origin", "*");
  response.headers.set("Access-Control-Allow-Headers", "*");
  response.headers.set("Access-Control-Allow-Credentials", "true");
  response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");

  return response;
};

const handleCORSPreflight = () => {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Credentials": "true",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

To test that everything is working, run npm run start in your terminal. If everything goes well, you should see an output similar to this:

$ wrangler dev
⛅️ wrangler 3.8.0
------------------

.....

⎔ Starting local server...
[mf:inf] Ready on http://0.0.0.0:xxxxx
Enter fullscreen mode Exit fullscreen mode

You can now test your tRPC server by making a request to http://localhost:xxxxx/trpc. You should see a response similar to this:

{
  "error": {
    "message": "No "query"-procedure on path",
    "code": -32004,
    "data": {
      "code": "NOT_FOUND",
      "httpStatus": 404,
      "stack": "...blah...blah",
      "path": ""
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Integrate Clerk with tRPC

Install Clerk Backend SDK

Next, install the Clerk backend SDK package by running the following command in your terminal:

npm install @clerk/backend
Enter fullscreen mode Exit fullscreen mode

Set environment secrets

Go to your Clerk dashboard and create a new Clerk app. Once your app is created, you'll need to get your Clerk API key. We'll utilize Cloudflare Workers Secrets to securely store our Clerk API key. To do this, create a .dev.vars file in the root of your project and add the API key to the file like this:

CLERK_API_KEY=<your-clerk-api-key>
Enter fullscreen mode Exit fullscreen mode

Go to your Cloudflare worker entry point, src/index.ts, and modify the fetch function to look like this:

// ...imports

export interface Env {
  CLERK_API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === "OPTIONS") {
      return handleCORSPreflight();
    }

    const headers = new Headers();

    const response = await fetchRequestHandler({
      req: request,
      //   env is passed to the createContext function
      createContext: () =>
        createContext({ env: env, req: request, resHeaders: headers }),
      endpoint: "/trpc",
      router: appRouter,
      onError({ error, path }) {
        console.error(`tRPC Error on '${path}'`, error);
      },
    });

    return addCORSHeaders(response);
  },
};

// ...rest of the code
Enter fullscreen mode Exit fullscreen mode

Using Clerk in your tRPC Context

Navigate to trpc.ts where we created a createContext function earlier. We'll need to update the createContext function to use the Clerk backend SDK to validate the session token and add the user session data to the tRPC context. We also need to modify the createContext function to accept an env parameter. Add the following code to the trpc.ts file:

// import the Env interface
import { Env } from ".";
import { Clerk } from "@clerk/backend";
import { initTRPC, inferAsyncReturnType, TRPCError } from "@trpc/server";
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";

// update the createContext function to accept an env parameter
interface Context extends FetchCreateContextFnOptions {
  env: Env;
}

// context holds data that all of your tRPC procedures will have access to
export const createContext = async ({ req, env }: Context) => {
  const clerk = Clerk({ apiKey: env.CLERK_API_KEY });
  const userId = req.headers.get("authorization");

  //If there is no session token, return null
  if (!userId) return { session: null };

  // otherwise, get the session
  const user = await clerk.users.getUser(userId);
  return {
    user,
  };
};

// ...rest of the code
Enter fullscreen mode Exit fullscreen mode

Create a tRPC auth middleware

The next step is to create a tRPC auth middleware. This middleware will check if the user session data is in the tRPC context. We'll also create a protectedProcedure which uses the tRPC auth middleware. We can then use the protectedProcedure to protect any procedure we want to require authentication. Add the following code to the trpc.ts file:

// ... rest of the code

const isAuthenticated = t.middleware(({ ctx, next }) => {
  if (!ctx.user?.id) throw new TRPCError({ code: "UNAUTHORIZED" });

  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const protectedProcedure = t.procedure.use(isAuthenticated);
Enter fullscreen mode Exit fullscreen mode

Create a route that requires authentication

Now that we have our auth middleware, we can create a route that requires authentication. Add the following code to the router.ts file:

// Example protected procedure
export const exampleProtectedRouter = router({
  // this is accessible only if the user is authenticated
  getSecret: protectedProcedure.query(({ ctx }) => {
    return {
      secret: `User ID: ${ctx.user.id}`,
    };
  }),
});

export const appRouter = router({
  example: exampleRouter,
  // add new router to app router
  protectedExample: exampleProtectedRouter,
});
Enter fullscreen mode Exit fullscreen mode

Deploy your tRPC API

The next step is to deploy your tRPC API. Before we do that, we need to add our Clerk API key to our Cloudflare Worker environment. To do this, run npx wrangler secret put CLERK_API_KEY in your terminal, you'll be prompted to enter your Clerk API key. Once you've added your Clerk API key, you can deploy your tRPC API by running npm run deploy in your terminal. If everything goes well, you should see an output similar to this:

> trpc-clerk-cloudflare-worker@0.0.0 deploy
> wrangler deploy

 ⛅️ wrangler 3.8.0
------------------
Total Upload: 257.98 KiB / gzip: 50.85 KiB
Uploaded trpc-clerk-cloudflare-worker (2.75 sec)
Published trpc-clerk-cloudflare-worker (3.32 sec)
https://trpc-clerk-cloudflare-worker.xxx.workers.dev
Current Deployment ID: xxxx-8443-xxx-b0a6-37xxxx0d4b5f13
Enter fullscreen mode Exit fullscreen mode

Testing everything out

To make sure everything works as expected, we'll create a simple tRPC NextJS frontend app that uses the tRPC API we just created. We'll use the Clerk's Next.js SDK to authenticate the user and get the session token. We'll then use the session token to make a request to the tRPC API.

Since one of the main advantages of tRPC is to share types between the frontend and backend, we'll create a parent folder that will hold both the frontend and backend code. We'll call this folder trpc-clerk.

Create a NextJS app

Create a new NextJS app by running npx create-next-app@latest in your terminal and answering the prompts that follow. I named my app trpc-clerk-nextjs and also chose to use the App router.

Our folder structure should now look like this:

trpc-clerk
│
├── trpc-clerk-nextjs
│
└── trpc-clerk-cloudflare-worker
Enter fullscreen mode Exit fullscreen mode

Set Up Clerk In Your NextJS App

Follow Clerk's NextJS Guide to set up Clerk in your NextJS app. Once you've completed the guide, you should have a working NextJS app with Clerk authentication.

Setup tRPC in your NextJS app

You'll need to install the tRPC client package and its dependencies. Navigate to your NextJS app directory and run the following command:

npm install @trpc/client @trpc/server @trpc/react-query @tanstack/react-query @tanstack/react-query-devtools @tanstack/react-query-next-experimental zod
Enter fullscreen mode Exit fullscreen mode

Create tRPC hooks

Next, we need to create our strongly-typed React hooks using the createTRPCReact function from @trpc/react-query and our AppRouter type definition from our tRPC API. Create a trpc.ts file in the app directory of our NextJS app and add the following code to the file:

import { createTRPCReact } from "@trpc/react-query";
// import our AppRouter type definition
import type { AppRouter } from "../../trpc-clerk-cloudflare-worker/src/router";

export const trpc = createTRPCReact<AppRouter>();
Enter fullscreen mode Exit fullscreen mode

Setup tRPC Provider

First, we add the URL of your tRPC API to your .env.local file: NEXT_PUBLIC_API_URL=<your-trpc-api-url>. Next, we need to create a tRPC client and wrap our NextJS app in a tRPC provider. To do that, create a provider.tsxfile in theapp directory of our NextJS app and add the following code to the file:

"use client";
import React from "react";
import { trpc } from "./trpc";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";

interface Props {
  headers: Headers;
  userId: string | null;
  children: React.ReactNode;
}

const API_URL = const API_URL = process.env.NEXT_PUBLIC_API_URL;

export const AppProvider: React.FC<Props> = (props) => {
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 5 * 1000,
          },
        },
      })
  );

  const [trpcClient] = React.useState(() =>
    trpc.createClient({
      links: [
        loggerLink({
          enabled: (opts) =>
            process.env.NODE_ENV === "development" ||
            (opts.direction === "down" && opts.result instanceof Error),
        }),
        unstable_httpBatchStreamLink({
          url: `${API_URL}/trpc`,
          headers() {
            const headers = new Map(props.headers);
            // ypu can set any http headers you want here
            headers.set("x-trpc-source", "nextjs");
            headers.set("authorization", `${props.userId}`);
            return Object.fromEntries(headers);
          },
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <ReactQueryStreamedHydration>
          {props.children}
        </ReactQueryStreamedHydration>
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </trpc.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Wrap your NextJS app in the tRPC provider

Now that we have our tRPC provider, we need to wrap our NextJS app in the provider, pass in the userId and headers props, and pass the props to the AppProvider component. To do this, open the layout.tsx file in the app directory of our NextJS app and add the following code to the file:

import "@/app/globals.css";
import { headers } from "next/headers";
import { AppProvider } from "@/app/provider";
import { ClerkProvider, auth } from "@clerk/nextjs";

interface Props {
  children: React.ReactNode;
}

export default async function RootLayout({ children }: Props) {
  const { userId } = auth();

  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <AppProvider headers={headers()} userId={userId}>
            {children}
          </AppProvider>
        </body>
      </html>
    </ClerkProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Make a request to your tRPC API

Finally! We're ready to make a request to our tRPC API. To do this, open the page.tsx file in the app directory of our NextJS app and add the following code to the file:

"use client";
import { trpc } from "./trpc";
import { UserButton } from "@clerk/nextjs";

export default function Home() {
  const greet = trpc.example.sayHello.useQuery(
    {
      name: "John Doe",
    },
    {
      enabled: false,
    }
  );

  const getProtectedSecret = trpc.protectedExample.getSecret.useQuery(
    undefined,
    {
      enabled: false,
    }
  );

  return (
    <div>
      <UserButton afterSignOutUrl="/" />

      <div>
        <div>
          <div>
            {getProtectedSecret.data
              ? getProtectedSecret.data?.secret
              : "************"}
          </div>
          <button onClick={() => getProtectedSecret.refetch()}>
            {getProtectedSecret.isFetching ? "Loading..." : "Get Secret"}
          </button>
        </div>

        <div>
          <div>{greet.data ? greet.data?.greeting : "************"}</div>
          <button onClick={() => greet.refetch()}>
            {greet.isFetching ? "Loading..." : "Say Hello"}
          </button>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Wrapping up

That's it! You should now have a working tRPC API that uses Clerk for authentication. You can find the complete code for this tutorial here

Resources

Top comments (4)

Collapse
 
piotrkulpinski profile image
Piotr Kulpinski

I'm no security expert, but this doesn't seem to be too secure since user_id can be obtain by third-party or, even worse, exposed to the client and I don't see any token validation here, so if you have the user_id you could do anything this user can via the API, right?

Collapse
 
piotrkulpinski profile image
Piotr Kulpinski • Edited

I think better way to handle this would be to pass the token and sessionId from the app to the api endpoint via headers and verify it like that:

const createContext = async ({ req, env }: CreateContextOptions) => {
  const clerk = Clerk({ secretKey: env.CLERK_SECRET_KEY });
  const token = req.headers.get("x-clerk-auth-token");
  const sessionId = req.headers.get("x-clerk-auth-session-id");

  if (!token || !sessionId) {
    return null
  }

  return await clerk.sessions.verifySession(sessionId, token);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
yinks profile image
Yinka Adedire

I think you're right! Thank you so much, I'll update the article accordingly.

Collapse
 
walosha profile image
Olawale Afuye

any auth0 implemetation or how to do so ?