Sessions and access
Protecting routes
Middleware, Server Components, API routes, Express and UI gating with roles and permissions.
Server-side checks#
Start with the middleware from the quickstart: it sends signed-out visitors to sign-in. Then check permissions where the data is read.
// app/billing/page.tsx — a Server Component
import { notFound, redirect } from 'next/navigation';
import { getAuth } from '@3een/auth/nextjs';
import { authConfig } from '@/lib/auth-config';
export default async function BillingPage() {
// Verified locally against the environment's published keys (no API call).
const auth = await getAuth(authConfig);
if (!auth) redirect('/api/auth/signin?redirect_uri=/billing');
// Roles and permissions come from the verified token.
if (!auth.has({ permission: 'billing:read' })) notFound();
return <h1>Billing for {auth.claims.email}</h1>;
}API routes#
Answer 401 without a session and 403 without the permission. getAuth also reads an Authorization: Bearer header, so the same route serves your frontend and other services.
// app/api/invoices/route.ts — protect an API route
import { NextResponse } from 'next/server';
import { getAuth } from '@3een/auth/nextjs';
import { authConfig } from '@/lib/auth-config';
export async function GET() {
// Reads the session cookie, or an `Authorization: Bearer <token>` header.
const auth = await getAuth(authConfig);
if (!auth) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
if (!auth.has({ permission: 'invoices:read' })) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json({ userId: auth.userId, invoices: [] });
}Express#
requirePermission(config, 'posts:delete') or requirePermission(config, { role: 'admin' }); see the full Express server.
Frontend gating#
SignedIn, SignedOut and Protect from @3een/auth/components render by session state, role or permission. They hide UI; they don't protect data.
'use client';
// Hide or show UI by role or permission. This does not protect data: the
// route behind it must check too (getAuth().has() or requirePermission).
import { OrganizationSwitcher, Protect, signOut, useSession } from '@3een/auth/components';
export function BillingMenu() {
const { user, loading, has } = useSession();
if (loading) return <p>Loading…</p>;
if (!user) return null;
return (
<nav aria-label="Account">
<OrganizationSwitcher onSwitched={(organizationId) => console.info('Now in', organizationId)} />
<Protect permission="billing:read" fallback={<p>Ask an admin for billing access.</p>}>
<a href="/billing">Billing</a>
</Protect>
{has({ role: 'admin' }) && <a href="/admin">Admin</a>}
<button type="button" onClick={() => signOut('/api/auth', '/')}>
Sign out
</button>
</nav>
);
}Roles and permissions#
- Define roles and permissions per environment, and assign roles to members of an organization. The token carries the resulting
rolesandpermissions. has({ permission: 'invoices:read' })matches exactly, through aresource:*wildcard, or*;has({ role: 'admin' })matches a role slug.- Tokens carry permissions as of issue. For a decision that must reflect a change made seconds ago, call
POST /app/environments/:environmentId/authorization/checkwith your API key and{ user_id, permission | role, organization_id }.