As TypeScript continues to gain popularity in modern web development, tools that leverage its strengths become increasingly valuable. tRPC (TypeScript Remote Procedure Call) is an open-source framework designed to help you build type-safe APIs, offering developers a streamlined approach to creating and consuming endpoints with full-type inference. 

tRPC provides several crucial advantages for TypeScript projects:

  • Statically typed API endpoints

  • Seamless type sharing between server and client

  • Simplified development process

These features enable developers to construct robust, type-safe APIs efficiently. However, the need for effective authentication and authorization escalates as applications become more complex. As more developers adopt tRPC, effectively securing these endpoints should be among their top priorities. 

However, adding clunky and high-maintenance authentication methods to protect these endpoints would defeat the purpose of tRPC’s speed and simplicity. This framework's beauty is its lack of unnecessary complexity, which calls for a similarly unencumbered authentication solution. 

Enter Descope, a no-code platform that simplifies user management and authentication for developers. By adding Descope for tRPC authentication, you can protect your API endpoints and ensure they are only accessible under specific conditions. This simple integration enhances your application's security and user experience without compromising the inherently streamlined framework.

This tutorial will take you through the process of integrating Descope with your tRPC application, covering:

  1. Setting up Descope in a tRPC project

  2. Creating protected procedures for authenticated users

  3. Implementing public endpoints alongside secured ones

  4. Utilizing Descope's session management

By the end of this guide, you'll know how to implement secure authentication in your tRPC application, combining the strengths of tRPC's type safety with Descope's flexible security.

Also read: Authenticating APIs With JWT Authorizers and OIDC

Prerequisites

The code for this tutorial is available in the tRPC Next.js Sample App repository. The README provides instructions for getting started, including how to install tRPC and Descope dependencies.

You'll need a Descope account and project to integrate Descope authentication into your application. You can sign up for a Free Forever account.

Adding Descope authentication to your tRPC project

For this project, you can use any Descope login flow of your choice. You can either customize your own flow or use one of the predefined flows. For project-specific setup instructions, refer to the Descope quick start guide.

After following the quick start steps, make sure to add your environment variables in a .env.local file. You’ll need to include the NEXT_PUBLIC_DESCOPE_PROJECT_ID and NEXT_PUBLIC_DESCOPE_FLOW_ID fields. Once these variables are defined, you can proceed with integrating tRPC.

Setting up your tRPC procedures

All necessary files for adding Descope integration are in the app folder.

In app/server/context.ts, set up the authorization condition for the protected procedure. 

export async function createContext({ req }:
   trpcNext.CreateNextContextOptions) {
       async function getSessionAuth() {
           const hasSessionHeader = req.headers["x-descope-session"];
           if (!hasSessionHeader) return false;
           const descopeSession = await session();
           return Boolean(descopeSession?.token.isAuthenticated);
   }  
   const auth = getSessionAuth();
   return { auth };

Within this function, we first check if the request headers include the x-descope-session field to determine if the user is in an active session.

const hasSessionHeader = req.headers["x-descope-session"];
if (!hasSessionHeader) return false;

If there is an active session, we use the Descope Next.js server to check if the session token is authenticated:

const descopeSession = await session();
return Boolean(descopeSession?.token.isAuthenticated);

After setting up your authorization condition, you can define your protected procedure in your app/server/trpc.ts file. First, import your TRPC initialization function and the context you have just set up. Then, initialize the router.

import { initTRPC, TRPCError } from '@trpc/server';
import type { Context } from '../server/context';


const t = initTRPC.context<Context>().create({})
export const router = t.router;

Using these values, you can set up a protected procedure that can only be accessed under certain authorization conditions. The isAuthed function returns the authentication context, and it can be used as a parameter for the TRPC procedure to reflect the current state of authentication. 

const isAuthed = t.middleware(({ next, ctx }) => {
   console.log(ctx.auth);
   if (!ctx.auth) {
     throw new TRPCError({ code: 'UNAUTHORIZED' })
   }
   return next({
     ctx: {
       auth: ctx.auth,
     },
   })
 })

Export your protected procedure using your newly defined isAuth function.

export const protectedProcedure = t.procedure.use(isAuthed)

Define your procedures in the app/api/trpc/[trpc]/[trpc].ts file. You can add procedures following the same format as the `hello` procedure below. With Descope authentication integrated, only signed-in users can access protected procedures. In this file, you can also define public procedures that do not require a valid Descope session.

 hello: protectedProcedure.query(() => {
   return {
     secret: `Congratulations! You are successfully accessing a protected procedure
using Descope Authentication!`,
   }
 }),
});

Conclusion

Integrating Descope to protect your tRPC endpoints is a powerful way to enhance your application's security. It allows you to easily create and modify APIs while implementing access control with minimal overhead.

You can designate certain endpoints as protected and others as public, providing flexibility within your application. Descope’s integration with tRPC improves both security and user experience, streamlining your development process. To learn more about how Descope can reduce dev time and smooth out user journeys, check out how we use our own product to power authentication and user management across our web properties.