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.tsxtype-checked

// 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.tstype-checked

// 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.

components/billing-menu.tsxtype-checked

'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 roles and permissions.
  • has({ permission: 'invoices:read' }) matches exactly, through a resource:* 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/check with your API key and { user_id, permission | role, organization_id }.