Documentation

Authentication & Authorization

Learn how to protect your API endpoints with authentication and authorization.

Procedure Types

The project provides three base procedures with different authentication levels, defined in orpc/procedures.ts:

1. Public Procedure

No authentication required - accessible to everyone:

import { publicProcedure } from "../../../orpc/procedures";

export const publicAction = publicProcedure
  .route({
    method: "GET",
    path: "/public-data",
    tags: ["Public"],
    summary: "Get public data",
  })
  .handler(async () => {
    // Anyone can access this
    return { data: "Public information" };
  });

Use cases:

  • Public content retrieval
  • Newsletter subscription
  • Contact form submission
  • Health checks

2. Protected Procedure

Requires user authentication:

import { protectedProcedure } from "../../../orpc/procedures";

export const userAction = protectedProcedure
  .route({
    method: "POST",
    path: "/user-action",
    tags: ["User"],
    summary: "Perform user action",
  })
  .handler(async ({ context }) => {
    // Access authenticated user
    const userId = context.user.id;
    const userEmail = context.user.email;
    const session = context.session;

    return {
      message: `Hello, ${userEmail}!`,
      userId,
    };
  });

Context available:

  • context.user - Current authenticated user
  • context.session - Current session data

Use cases:

  • User profile operations
  • User-specific data retrieval
  • Personal settings management

3. Admin Procedure

Requires admin role:

import { adminProcedure } from "../../../orpc/procedures";

export const adminAction = adminProcedure
  .route({
    method: "DELETE",
    path: "/admin/delete-user/:id",
    tags: ["Admin"],
    summary: "Delete user (admin only)",
  })
  .input(z.object({
    id: z.string(),
  }))
  .handler(async ({ input, context }) => {
    // Only users with role === "admin" can access
    const adminUser = context.user;

    await deleteUser(input.id);

    return { success: true };
  });

Requirements:

  • User must be authenticated
  • User role must be "admin"

Use cases:

  • User management
  • System configuration
  • Analytics and reporting
  • Content moderation

How Authentication Works

Protected Procedure Implementation

From orpc/procedures.ts:8-25:

export const protectedProcedure = publicProcedure.use(
  async ({ context, next }) => {
    const session = await auth.api.getSession({
      headers: context.headers,
    });

    if (!session) {
      throw new ORPCError("UNAUTHORIZED");
    }

    return await next({
      context: {
        session: session.session,
        user: session.user,
      },
    });
  },
);

Admin Procedure Implementation

From orpc/procedures.ts:27-35:

export const adminProcedure = protectedProcedure.use(
  async ({ context, next }) => {
    if (context.user.role !== "admin") {
      throw new ORPCError("FORBIDDEN");
    }

    return await next();
  },
);

Custom Authorization

Resource-Based Authorization

Check ownership before allowing access:

export const updatePost = protectedProcedure
  .route({
    method: "PUT",
    path: "/posts/:id",
  })
  .input(z.object({
    id: z.string(),
    title: z.string(),
    content: z.string(),
  }))
  .handler(async ({ input, context }) => {
    const post = await getPost(input.id);

    // Check if user owns the post
    if (post.authorId !== context.user.id) {
      throw new ORPCError("FORBIDDEN", {
        message: "You can only edit your own posts",
      });
    }

    return await updatePost(input.id, {
      title: input.title,
      content: input.content,
    });
  });

Role-Based Authorization

Create custom role checks:

export const moderateContent = protectedProcedure
  .route({
    method: "POST",
    path: "/moderate/:id",
  })
  .handler(async ({ input, context }) => {
    const allowedRoles = ["admin", "moderator"];

    if (!allowedRoles.includes(context.user.role)) {
      throw new ORPCError("FORBIDDEN", {
        message: "Moderator access required",
      });
    }

    // Perform moderation
    return await moderateContent(input.id);
  });

Team-Based Authorization

Check team membership:

export const updateOrganization = protectedProcedure
  .route({
    method: "PUT",
    path: "/organizations/:id",
  })
  .input(z.object({
    id: z.string(),
    name: z.string(),
  }))
  .handler(async ({ input, context }) => {
    const membership = await getOrganizationMembership(
      input.id,
      context.user.id
    );

    if (!membership || membership.role !== "owner") {
      throw new ORPCError("FORBIDDEN", {
        message: "Organization owner access required",
      });
    }

    return await updateOrganization(input.id, {
      name: input.name,
    });
  });

Custom Authentication Middleware

Create reusable authorization middleware:

// packages/api/orpc/middleware/team-member-middleware.ts
import { os } from "@orpc/server";
import { ORPCError } from "@orpc/server";

export const teamMemberMiddleware = os
  .$context<{
    user: { id: string };
  }>()
  .middleware(async ({ context, next }) => {
    // Assume team ID is passed in input
    const teamId = context.teamId;

    const isMember = await isTeamMember(teamId, context.user.id);

    if (!isMember) {
      throw new ORPCError("FORBIDDEN", {
        message: "Team membership required",
      });
    }

    return await next({
      context: {
        teamId,
      },
    });
  });

Use the custom middleware:

export const teamAction = protectedProcedure
  .use(teamMemberMiddleware)
  .route({
    method: "POST",
    path: "/teams/:teamId/action",
  })
  .handler(async ({ context }) => {
    // User is guaranteed to be a team member
    const teamId = context.teamId;
    return { success: true };
  });

API Key Authentication

For third-party integrations, implement API key authentication:

packages/api/orpc/middleware/api-key-middleware.ts
import { os } from "@orpc/server";
import { ORPCError } from "@orpc/server";

export const apiKeyMiddleware = os
  .$context<{ headers: Headers }>()
  .middleware(async ({ context, next }) => {
    const apiKey = context.headers.get("X-API-Key");

    if (!apiKey) {
      throw new ORPCError("UNAUTHORIZED", {
        message: "API key required",
      });
    }

    const client = await validateApiKey(apiKey);

    if (!client) {
      throw new ORPCError("UNAUTHORIZED", {
        message: "Invalid API key",
      });
    }

    return await next({
      context: {
        client,
      },
    });
  });

Use API key middleware:

export const apiEndpoint = publicProcedure
  .use(apiKeyMiddleware)
  .route({
    method: "GET",
    path: "/api/external/data",
  })
  .handler(async ({ context }) => {
    // Access client information
    const clientId = context.client.id;
    return { data: "API response" };
  });

Rate Limiting

Protect your APIs with rate limiting:

packages/api/orpc/middleware/rate-limit-middleware.ts
import { os } from "@orpc/server";
import { ORPCError } from "@orpc/server";

export const rateLimitMiddleware = os
  .$context<{ headers: Headers }>()
  .middleware(async ({ context, next }) => {
    const ip = context.headers.get("x-forwarded-for") ?? "unknown";
    const key = `rate-limit:${ip}`;

    const current = await redis.incr(key);

    if (current === 1) {
      await redis.expire(key, 60); // 60 seconds window
    }

    if (current > 100) { // 100 requests per minute
      throw new ORPCError("TOO_MANY_REQUESTS", {
        message: "Rate limit exceeded",
      });
    }

    return await next();
  });