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 usercontext.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:
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:
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();
});