withOrganizationAuthRequired
Secure API routes with organization authentication and role-based access control
withOrganizationAuthRequired 🔒
Protect your API routes with organization-level authentication and role-based access control! ✨
What It Does 🎯
The withOrganizationAuthRequired
wrapper:
- 🔐 Verifies user is authenticated
- 🏢 Checks user belongs to the organization
- 👮 Enforces minimum role requirements
- 📦 Provides organization data in context
- ⚡ Returns 401/403 errors automatically
Server-Side Only
This wrapper is for API routes only. For client components, use the useOrganization
hook!
Basic Usage 📝
Without Role Requirement
Any organization member can access:
// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
// Fetch projects for this organization
const projects = await getProjects(organization.id)
return NextResponse.json(projects)
}
)
With Role Requirement
Only admins and owners can access:
// src/app/api/app/org/[slug]/settings/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const body = await req.json()
// Update organization settings
await updateOrganizationSettings(organization.id, body)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin // Requires admin or owner
)
Context Object 📦
The wrapper provides a context
object with:
{
session: {
expires: string
user: Promise<{
id: string
email: string
name: string
image?: string
// ... other user fields
}>
organization: Promise<{
id: string
name: string
slug: string
image?: string
role: 'owner' | 'admin' | 'user'
plan: {
id: string
name: string
codename: string
default: boolean
quotas: object
requiredCouponCount: number
} | null
}>
}
params: Promise<{
slug?: string
// ... other route params
}>
}
User and Organization are Promises
The user
and organization
properties are async getters (Promises). You must await them before accessing their properties!
Common Patterns 🚀
GET - Fetch Organization Data
// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { projects } from '@/db/schema'
import { eq } from 'drizzle-orm'
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
// Fetch projects for this organization
const orgProjects = await db
.select()
.from(projects)
.where(eq(projects.organizationId, organization.id))
return NextResponse.json(orgProjects)
}
)
POST - Create Resource
// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { projects } from '@/db/schema'
export const POST = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const user = await context.session.user
const body = await req.json()
// Validate input
if (!body.name || body.name.length < 3) {
return NextResponse.json(
{ error: 'Name must be at least 3 characters' },
{ status: 400 }
)
}
// Create project
const newProject = await db.insert(projects).values({
organizationId: organization.id,
name: body.name,
createdBy: user.id,
}).returning()
return NextResponse.json(newProject[0])
},
OrganizationRole.enum.admin // Only admins can create
)
PATCH - Update Resource
// src/app/api/app/org/[slug]/settings/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { organizations } from '@/db/schema'
import { eq } from 'drizzle-orm'
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const body = await req.json()
// Update organization
await db
.update(organizations)
.set({
name: body.name,
image: body.image,
updatedAt: new Date(),
})
.where(eq(organizations.id, organization.id))
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin
)
DELETE - Remove Resource
// src/app/api/app/org/[slug]/delete/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
// Delete organization and all related data
await deleteOrganization(organization.id)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.owner // Only owners can delete
)
Role-Based Examples 🎭
Owner-Only Routes
// src/app/api/app/org/[slug]/billing/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
// Fetch billing information
const billing = await getBillingInfo(organization.id)
return NextResponse.json(billing)
},
OrganizationRole.enum.owner // Owner only
)
Admin-Level Routes
// src/app/api/app/org/[slug]/members/[userId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
// Remove member (admin or owner)
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const params = await context.params
const { userId } = params
// Remove member
await removeMember(organization.id, userId)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin
)
// Change member role (admin or owner)
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const params = await context.params
const { userId } = params
const { role } = await req.json()
// Update member role
await updateMemberRole(organization.id, userId, role)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin
)
User-Level Routes
// src/app/api/app/org/[slug]/profile/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'
// Any member can view their profile
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const { user, organization } = context.session
// Get user's profile in this organization
const profile = await getUserProfile(user.id, organization.id)
return NextResponse.json(profile)
}
// No role requirement = any member can access
)
Advanced Use Cases 💡
Check Plan Quotas
// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
export const POST = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const body = await req.json()
// Check quota
const currentProjects = await getProjectCount(organization.id)
const maxProjects = organization.plan.quotas.projects
if (currentProjects >= maxProjects) {
return NextResponse.json(
{ error: 'Project limit reached. Please upgrade your plan.' },
{ status: 403 }
)
}
// Create project
const project = await createProject({
organizationId: organization.id,
...body,
})
return NextResponse.json(project)
},
OrganizationRole.enum.admin
)
Custom Role Logic
// src/app/api/app/org/[slug]/projects/[projectId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const user = await context.session.user
const params = await context.params
const { projectId } = params
// Get project
const project = await getProject(projectId)
// Check permissions: Owner, Admin, or project creator
const canDelete =
organization.role === 'owner' ||
organization.role === 'admin' ||
project.createdBy === user.id
if (!canDelete) {
return NextResponse.json(
{ error: 'Insufficient permissions' },
{ status: 403 }
)
}
// Delete project
await deleteProject(projectId)
return NextResponse.json({ success: true })
}
)
Validate Organization Ownership
// src/app/api/app/org/[slug]/resources/[resourceId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
const params = await context.params
const { resourceId } = params
// Fetch resource
const resource = await getResource(resourceId)
// Verify resource belongs to this organization
if (resource.organizationId !== organization.id) {
return NextResponse.json(
{ error: 'Resource not found' },
{ status: 404 }
)
}
return NextResponse.json(resource)
}
)
Best Practices 💡
1. Always Validate Input
export const POST = withOrganizationAuthRequired(
async (req, context) => {
const body = await req.json()
// ✅ Validate input
if (!body.name || body.name.length < 3) {
return NextResponse.json(
{ error: 'Invalid input' },
{ status: 400 }
)
}
// Process valid data
}
)
2. Use Appropriate Roles
// ❌ Bad - No role check for sensitive operation
export const DELETE = withOrganizationAuthRequired(async (req, context) => {
await deleteOrganization()
})
// ✅ Good - Require owner for deletion
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
await deleteOrganization()
},
OrganizationRole.enum.owner
)
3. Check Resource Ownership
// ✅ Verify resource belongs to organization
const resource = await getResource(resourceId)
if (resource.organizationId !== organization.id) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
4. Return Proper Status Codes
// 400 - Bad Request
return NextResponse.json({ error: 'Invalid data' }, { status: 400 })
// 403 - Forbidden (authenticated but no permission)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
// 404 - Not Found
return NextResponse.json({ error: 'Not found' }, { status: 404 })
5. Check Plan Quotas
// ✅ Enforce plan limits
if (currentUsage >= plan.quotas.limit) {
return NextResponse.json(
{ error: 'Plan limit reached' },
{ status: 403 }
)
}
Error Responses 🚨
The wrapper automatically handles these errors:
401 Unauthorized
User is not logged in:
{
"error": "Unauthorized",
"message": "You must be logged in to access this resource"
}
403 Forbidden
User doesn't have required role:
{
"error": "Forbidden",
"message": "Insufficient permissions"
}
404 Not Found
Organization not found or user not a member:
{
"error": "Not Found",
"message": "Organization not found"
}
Troubleshooting 🔧
"Unauthorized" Error
Make sure user is logged in and has session:
// Test with a valid session token
const response = await fetch('/api/app/org/my-org/projects', {
headers: {
'Cookie': 'session=...'
}
})
"Forbidden" Error
Check if user has the required role:
// Verify user's role
const { membership } = await getOrganization(slug)
console.log('User role:', membership.role)
// Make sure role meets requirements
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
// ...
},
OrganizationRole.enum.admin // Requires admin or owner
)
"Organization not found"
Verify the slug in the URL matches an existing organization:
// Check URL: /api/app/org/[slug]/...
// Slug must match an organization the user belongs to
Context is Undefined
Make sure you're accessing context correctly:
// ✅ Correct
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const organization = await context.session.organization
}
)
// ❌ Wrong - missing context parameter
export const GET = withOrganizationAuthRequired(
async (req) => {
// context is missing!
}
)
API Reference 📚
Function Signature
withOrganizationAuthRequired(
handler: (req: Request, context: Context) => Promise<Response>,
requiredRole: OrganizationRole
): (req: Request, context: RouteContext) => Promise<Response>
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
handler | Function | Yes | Your route handler function |
requiredRole | OrganizationRole | Yes | Minimum role required (OrganizationRole.enum.user/admin/owner) |
Context Type
{
session: {
expires: string
user: Promise<User>
organization: Promise<Organization & { role: string, plan: Plan | null }>
}
params: Promise<Record<string, string>>
}
Remember to Await
Both session.user
, session.organization
, and params
are Promises! Always await
them before use.
Summary 📋
You now know how to:
- ✅ Protect API routes with organization authentication
- ✅ Enforce role-based access control
- ✅ Access organization data in routes
- ✅ Handle different HTTP methods
- ✅ Validate input and check quotas
- ✅ Return proper error responses
- ✅ Build secure multi-tenant APIs
Secure by Default
Always use withOrganizationAuthRequired
for routes that access organization data. Security first! 🔒