---
name: auth
description: Implement JWT/cookie authentication and authorization in tRPC using createContext for user extraction, t.middleware with opts.next({ ctx }) for context narrowing to non-null user, protectedProcedure base pattern, client-side Authorization headers via httpBatchLink headers(), WebSocket connectionParams, and SSE auth via cookies or EventSource polyfill custom headers. UNAUTHORIZED vs FORBIDDEN, role checks stacked on protectedProcedure, and why createContext shouldn't throw for a bad token.
requires:
  - server-setup
  - middlewares
  - client-setup
sources:
  - www/v11/docs/server/authorization.md
  - www/v11/docs/client/headers.md
  - www/v11/docs/client/links/httpSubscriptionLink.md
  - www/v11/docs/server/websockets.md
  - www/v11/docs/client/cors.md
metadata:
  type: composition
  library: trpc
  library_version: 11.25.0
---

<!-- Generated from www/v11/docs/server/authorization.md by www/build/skills.ts. Edit the docs instead. -->

# Authorization

From https://trpc.io/docs/server/authorization

The `createContext` function is called for each incoming request, so here you can add contextual information about the calling user from the request object.

## Create context from request headers

```ts no-check title="server/context.ts"
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
import { decodeAndVerifyJwtToken } from './utils';

export async function createContext({ req, res }: CreateHTTPContextOptions) {
  // Create your context based on the request object
  // Will be available as `ctx` in all your resolvers

  // This is just an example of something you might want to do in your ctx fn
  async function getUserFromHeader() {
    if (req.headers.authorization) {
      const user = await decodeAndVerifyJwtToken(
        req.headers.authorization.split(' ')[1],
      );
      return user;
    }
    return null;
  }
  const user = await getUserFromHeader();

  return {
    user,
  };
}
export type Context = Awaited<ReturnType<typeof createContext>>;
```

## Option 1: Authorize using resolver

```ts no-check title="server/routers/_app.ts"
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

type Context = { user: { name: string } | null };

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

const appRouter = t.router({
  // open for anyone
  hello: t.procedure
    .input(z.string().nullish())
    .query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`),
  // checked in resolver
  secret: t.procedure.query((opts) => {
    if (!opts.ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }
    return {
      secret: 'sauce',
    };
  }),
});
```

## Option 2: Authorize using middleware

```ts no-check title="server/routers/_app.ts"
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

type Context = { user: { name: string } | null };

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

// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(
  async function isAuthed(opts) {
    const { ctx } = opts;
    // `ctx.user` is nullable
    if (!ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    return opts.next({
      ctx: {
        // ✅ user value is known to be non-null now
        user: ctx.user,
      },
    });
  },
);

t.router({
  // this is accessible for everyone
  hello: t.procedure
    .input(z.string().nullish())
    .query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`),
  admin: t.router({
    // this is accessible only to admins
    secret: protectedProcedure.query((opts) => {
      return {
        secret: 'sauce',
      };
    }),
  }),
});
```

## Building on a protected procedure

Since a procedure's middlewares run in order, you can add more checks on top of `protectedProcedure`, where `ctx.user` is already non-null:

```ts no-check title="server/trpc.ts"
import { initTRPC, TRPCError } from '@trpc/server';

type Context = { user: { name: string; role: 'admin' | 'user' } | null };

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

export const protectedProcedure = t.procedure.use(async (opts) => {
  if (!opts.ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return opts.next({ ctx: { user: opts.ctx.user } });
});

export const adminProcedure = protectedProcedure.use(async (opts) => {
  if (opts.ctx.user.role !== 'admin') {
    throw new TRPCError({ code: 'FORBIDDEN' });
  }
  return opts.next();
});
```

Throw `UNAUTHORIZED` (401) when the caller isn't signed in, and `FORBIDDEN` (403) when they are, but aren't allowed to do this.

## Sending credentials from the client

- **Cookies** are sent with every request when the client and server are on the same origin. For another origin, see [CORS & Cookies](https://trpc.io/docs/client/cors).
- **Headers** like `Authorization` can be set on any HTTP link with the [`headers`](https://trpc.io/docs/client/headers) option.
- **Subscriptions** over SSE can't send custom headers with the browser's `EventSource`. See [`httpSubscriptionLink`](https://trpc.io/docs/client/links/httpSubscriptionLink#headers-and-authorization--authentication) for the options.
- **WebSockets** send [connection params](https://trpc.io/docs/server/websockets#connectionParams) when the connection opens, which `createContext` reads from `opts.info.connectionParams`.

## Common mistakes

- **Calling `opts.next()` without the narrowed `ctx`.** Procedures after the middleware still see `ctx.user` as nullable. Pass `opts.next({ ctx: { user: opts.ctx.user } })`, as in [Option 2](https://trpc.io/docs/server/authorization#option-2-authorize-using-middleware).
- **Throwing in `createContext` for a missing or invalid token.** That fails every procedure in the request, public ones too. Return `user: null` instead, and let a middleware reject the procedures that need a user.
- **Putting tokens in `httpSubscriptionLink`'s `connectionParams`.** They are sent in the URL, where servers and proxies may log them. Prefer cookies, or headers through an `EventSource` ponyfill.

## Headers

From https://trpc.io/docs/client/headers

The headers option may be used with any of our HTTP links: [`httpBatchLink`](https://trpc.io/docs/client/links/httpBatchLink), [`httpBatchStreamLink`](https://trpc.io/docs/client/links/httpBatchStreamLink), [`httpLink`](https://trpc.io/docs/client/links/httpLink), or [`httpSubscriptionLink`](https://trpc.io/docs/client/links/httpSubscriptionLink).

`headers` can be both an object or a function. If it's a function it will get called dynamically for every HTTP request.

```ts no-check title="utils/trpc.ts"
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

let token: string;

export function setToken(newToken: string) {
  /**
   * You can also save the token to cookies, and initialize from
   * cookies above.
   */
  token = newToken;
}

export const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000',
      /**
       * Headers will be called on each request.
       */
      headers() {
        return {
          Authorization: token,
        };
      },
    }),
  ],
});
```

### Example with auth login

```ts no-check title="auth.ts"
const result = await trpc.auth.login.mutate({ username: 'user', password: 'pass' });
setToken(result.accessToken);
```

The `token` can be whatever you want it to be. It's entirely up to you whether that's just a client-side
variable that you update the value of on success or whether you store the token and pull it from local storage.

## Headers and authorization / authentication

### Web apps

#### Same domain

If you're doing a web application, cookies are sent as part of the request as long as your client is on the same domain as the server.

#### Cross-domain

If the client and server are not on the same domain, you can use `withCredentials: true` ([read more on MDN here](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/withCredentials)).

**Example:**

```tsx no-check
// [...]
httpSubscriptionLink({
  url: 'https://example.com/api/trpc',
  eventSourceOptions() {
    return {
      withCredentials: true, // <---
    };
  },
});
```

### Custom headers through ponyfill

**Recommended for non-web environments**

You can ponyfill `EventSource` and use the `eventSourceOptions` -callback to populate headers.

```tsx no-check
import {
  createTRPCClient,
  httpBatchLink,
  httpSubscriptionLink,
  splitLink,
} from '@trpc/client';
import { EventSourcePolyfill } from 'event-source-polyfill';
import type { AppRouter } from './server';

// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition: (op) => op.type === 'subscription',
      true: httpSubscriptionLink({
        url: 'http://localhost:3000',
        // ponyfill EventSource
        EventSource: EventSourcePolyfill,
        // options to pass to the EventSourcePolyfill constructor
        eventSourceOptions: async ({ op }) => {
          //                          ^ Includes the operation that's being executed
          // you can use this to generate a signature for the operation
          const signature = await getSignature(op);
          return {
            headers: {
              authorization: 'Bearer supersecret',
              'x-signature': signature,
            },
          };
        },
      }),
      false: httpBatchLink({
        url: 'http://localhost:3000',
      }),
    }),
  ],
});
```

### Updating configuration on an active connection

`httpSubscriptionLink` leverages SSE through `EventSource`, ensuring that connections encountering errors like network failures or bad response codes are automatically retried. However, `EventSource` does not allow re-execution of the `eventSourceOptions()` or `url()` options to update its configuration, which is particularly important in scenarios where authentication has expired since the last connection.

To address this limitation, you can use a [`retryLink`](https://trpc.io/docs/client/links/retryLink) in conjunction with `httpSubscriptionLink`. This approach ensures that the connection is re-established with the latest configuration, including any updated authentication details.

> [!WARNING] Caution
> Please note that restarting the connection will result in the `EventSource` being recreated from scratch, which means any previously tracked events will be lost.


```tsx no-check
import {
  createTRPCClient,
  httpBatchLink,
  httpSubscriptionLink,
  retryLink,
  splitLink,
} from '@trpc/client';
import {
  EventSourcePolyfill,
  EventSourcePolyfillInit,
} from 'event-source-polyfill';
import type { AppRouter } from './server';

// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition: (op) => op.type === 'subscription',
      false: httpBatchLink({
        url: 'http://localhost:3000',
      }),
      true: [
        retryLink({
          retry: (opts) => {
            opts.op.type;
            const code = opts.error.data?.code;
            if (!code) {
              // This shouldn't happen as our httpSubscriptionLink will automatically retry within when there's a non-parsable response
              console.error('No error code found, retrying', opts);
              return true;
            }
            if (code === 'UNAUTHORIZED' || code === 'FORBIDDEN') {
              console.log('Retrying due to 401/403 error');
              return true;
            }
            return false;
          },
        }),
        httpSubscriptionLink({
          url: async () => {
            // calculate the latest URL if needed...
            return getAuthenticatedUri();
          },
          // ponyfill EventSource
          EventSource: EventSourcePolyfill,
          eventSourceOptions: async () => {
            // ...or maybe renew an access token
            const token = await auth.getOrRenewToken();

            return {
              headers: {
                authorization: `Bearer ${token}`,
              },
            };
          },
        }),
      ],
    }),
  ],
});
```

### Connection params

In order to authenticate with `EventSource`, you can define `connectionParams` in `httpSubscriptionLink`. This will be sent as part of the URL, which is why other methods are preferred.

```ts no-check title="server/context.ts"
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';

export const createContext = async (opts: CreateHTTPContextOptions) => {
  const token = opts.info.connectionParams?.token;

  // [... authenticate]

  return {};
};

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

```ts no-check title="client/trpc.ts"
import {
  createTRPCClient,
  httpBatchLink,
  httpSubscriptionLink,
  splitLink,
} from '@trpc/client';
import type { AppRouter } from './server';

// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition: (op) => op.type === 'subscription',
      true: httpSubscriptionLink({
        url: 'http://localhost:3000',
        connectionParams: async () => {
          // Will be serialized as part of the URL
          return {
            token: 'supersecret',
          };
        },
      }),
      false: httpBatchLink({
        url: 'http://localhost:3000',
      }),
    }),
  ],
});
```

From https://trpc.io/docs/client/links/httpSubscriptionLink#headers-and-authorization--authentication

## Authentication / connection params

> [!TIP]
> If you're doing a web application, you can ignore this section as the cookies are sent as part of the request.


In order to authenticate with WebSockets, you can define `connectionParams` to `createWSClient`. This will be sent as the first message when the client establishes a WebSocket connection.

```ts no-check title="server/context.ts"
import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws';

export const createContext = async (opts: CreateWSSContextFnOptions) => {
  const token = opts.info.connectionParams?.token;

  // [... authenticate]

  return {};
};

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

```ts no-check title="client/trpc.ts"
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
import type { AppRouter } from './server';
import superjson from 'superjson';

const wsClient = createWSClient({
  url: `ws://localhost:3000`,

  connectionParams: async () => {
    return {
      token: 'supersecret',
    };
  },
});
export const trpc = createTRPCClient<AppRouter>({
  links: [wsLink({ client: wsClient, transformer: superjson })],
});
```

`createContext` runs once per connection (after the connection params arrive, if you send them), and every call over that connection shares its result. If it throws, the server sends the error and closes the connection. A token is only checked when the connection opens, so it stays accepted on that connection after it expires.

From https://trpc.io/docs/server/websockets#connectionParams

## References

- [Send cookies cross-origin](references/client-cors.md)

## Related skills

- `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`
- `middlewares` (https://trpc.io/docs/server/middlewares): `npx @tanstack/intent@latest load @trpc/server#middlewares`
- `client-setup` (https://trpc.io/docs/client/vanilla/setup): `npx @tanstack/intent@latest load @trpc/client#client-setup`
