---
title: Quickstart — Next.js
project: Authaz
updated: 2026-05-07T23:22:31.909Z
---


# Quickstart — Next.js

> [cURL](./curl.md) · **Next.js** · [React](./react.md) · [Hono](./hono.md) · [.NET](./dotnet.md)

This guide gets a Next.js (App Router) app authenticating users through Authaz Sign-In. You'll use [`@authaz/next`](https://www.npmjs.com/package/@authaz/next) on the server and [`@authaz/react`](https://www.npmjs.com/package/@authaz/react) for hooks in client components. About 10 minutes.

## Prerequisites

- Node.js 18+ and a Next.js 14/15 App Router app (`npx create-next-app@latest` if you're starting fresh).
- An Authaz application with `http://localhost:3000/auth/callback` registered as a redirect URI. New to Authaz? [Set up your app in 60 seconds](./setup.md) — you'll come back with the three env vars below.

## Step 1 — Install

```bash
npm install @authaz/next @authaz/react
# pnpm: pnpm add @authaz/next @authaz/react
# yarn: yarn add @authaz/next @authaz/react
# bun:  bun add @authaz/next @authaz/react
```

## Step 2 — Environment variables

`.env.local`:

```bash
AUTHAZ_CLIENT_ID=app_01h...
AUTHAZ_CLIENT_SECRET=secret_...
AUTHAZ_ORGANIZATION_ID=0199...
AUTHAZ_TENANT_ID=0198...    # optional
```

## Step 3 — Mount the auth handler

Create a catch-all route at `app/api/auth/[...authaz]/route.ts`:

```typescript
import { createAuthazHandler } from "@authaz/next";

export const { GET, POST } = createAuthazHandler({
  clientId: process.env.AUTHAZ_CLIENT_ID!,
  clientSecret: process.env.AUTHAZ_CLIENT_SECRET!,
  organizationId: process.env.AUTHAZ_ORGANIZATION_ID!,
  tenantId: process.env.AUTHAZ_TENANT_ID,
  afterLoginUrl: "/dashboard",
  afterLogoutUrl: "/",
});
```

This exposes:

| Route | What it does |
|-------|-------------|
| `GET  /api/auth/login` | Redirects to Authaz Sign-In with PKCE. |
| `POST /api/auth/callback` | Receives the OAuth code, exchanges it, sets the session cookie. |
| `POST /api/auth/logout` | Clears the session and redirects. |
| `GET  /api/auth/me` | Current user, or `401` if not signed in. |
| `POST /api/auth/refresh` | Refreshes the access token. |

## Step 4 — Add the OAuth callback page

Authaz redirects users back to `/auth/callback` as a `GET`, but the handler above expects a `POST` (so the auth code never lives in a URL bar past the first hop). Add a tiny client component that re-POSTs:

```tsx
// app/auth/callback/page.tsx
"use client";

import { Suspense, useEffect } from "react";
import { useSearchParams } from "next/navigation";

const CallbackContent = () => {
  const searchParams = useSearchParams();

  useEffect(() => {
    const form = document.createElement("form");
    form.method = "POST";
    form.action = "/api/auth/callback";

    ["code", "state", "error", "error_description"].forEach((param) => {
      const value = searchParams.get(param);
      if (value) {
        const input = document.createElement("input");
        input.type = "hidden";
        input.name = param;
        input.value = value;
        form.appendChild(input);
      }
    });

    document.body.appendChild(form);
    form.submit();
  }, [searchParams]);

  return <p>Completing login…</p>;
};

export default function CallbackPage() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <CallbackContent />
    </Suspense>
  );
}
```

## Step 5 — Wrap the app in `AuthazProvider`

```tsx
// app/layout.tsx
import { AuthazProvider } from "@authaz/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AuthazProvider basePath="/api/auth" autoRefresh>
          {children}
        </AuthazProvider>
      </body>
    </html>
  );
}
```

`autoRefresh` re-fetches the access token automatically on a 401 — most apps want this.

## Step 6 — Use the hooks

A client component that shows login state and a button:

```tsx
// app/components/AuthButton.tsx
"use client";

import { useAuthaz } from "@authaz/react";

export const AuthButton = () => {
  const { isAuthenticated, isLoading, user, login, logout } = useAuthaz();

  if (isLoading) return <span>…</span>;

  return isAuthenticated ? (
    <div>
      <span>{user?.email}</span>
      <button onClick={() => logout()}>Logout</button>
    </div>
  ) : (
    <button onClick={() => login()}>Login</button>
  );
};
```

## Step 7 — Protect a page

Two ways. Pick the one that fits the page:

**Client-component protection** (redirects if not signed in):

```tsx
// app/dashboard/page.tsx
"use client";

import { useRequireAuth } from "@authaz/react";

export default function Dashboard() {
  useRequireAuth();
  return <h1>Dashboard</h1>;
}
```

**Server-component protection** (redirects before any HTML is rendered):

```tsx
// app/dashboard/page.tsx
import { requireAuth } from "@authaz/next";

export default async function Dashboard() {
  await requireAuth();
  return <h1>Dashboard</h1>;
}
```

## Step 8 — Protect API routes

```typescript
// app/api/protected/route.ts
import { withAuth } from "@authaz/next";
import { NextResponse } from "next/server";

export const GET = withAuth(async () => {
  return NextResponse.json({ secret: "only signed-in users see this" });
});
```

## Step 9 — Run it

```bash
npm run dev
```

Hit `http://localhost:3000`, click **Login**, sign up at Authaz Sign-In. You should land on `/dashboard` and see your email render via the `AuthButton`. Inside any client component, `useAuthaz()` now returns:

```typescript
{
  isAuthenticated: true,
  isLoading: false,
  user: {
    id: "user_01h...",
    email: "you@example.com",
    name: "Your Name",
    mfaEnabled: false,
    organizations: [{ organizationId: "0199...", accessLevel: "owner" }],
  },
  // login, logout, refresh callbacks
}
```

If you see that user object, auth is working end-to-end.

## If something's wrong

| Symptom | Likely cause | Fix |
|---|---|---|
| Login click → `invalid_request` error page | Redirect URI mismatch. | Confirm `http://localhost:3000/auth/callback` is registered exactly in **Settings → Redirect URIs**. |
| Land on `/auth/callback` and see a 404 or blank page | Step 4 callback page missing or has a JS error. | Confirm `app/auth/callback/page.tsx` exists and the form re-POSTs to `/api/auth/callback`. |
| `useAuthaz()` is stuck on `isLoading: true` | `AuthazProvider` not wrapping the tree, or `basePath` doesn't match the handler route. | Check `app/layout.tsx` wraps `{children}`, and `basePath="/api/auth"` matches the catch-all in Step 3. |
| Server logs `invalid_client` on token exchange | `AUTHAZ_CLIENT_SECRET` wrong, or you swapped Application ID and Client Secret. | Re-copy from **Settings → API Keys**. |
| `401` from `/api/auth/me` after a long idle | Access token expired and refresh failed. | Confirm `autoRefresh` is on and that the refresh token cookie is present. |

For everything else, the [errors catalog](../errors.md) covers every code Authaz returns.

## Going further

- **Middleware route guards** — use `createAuthMiddleware` in `middleware.ts` to redirect unauthenticated users before page handlers run.
- **Server Components with the user object** — `requireUser({ authazDomain, apiKey })` returns a helper whose `getOrRedirect()` resolves to the current user.
- **Custom domains** — pass `authazIdentityDomain: "https://auth.yourapp.com"` to `createAuthazHandler` once your custom domain is verified.

## Complete worked example

- [Next.js — first integration (single-tenant)](../recipes/nextjs-single-tenant.md) — the full app, end to end.
- [Next.js — B2B SaaS (multi-tenant)](../recipes/nextjs-multi-tenant.md) — same, with tenant pickers and tenant-scoped tokens.

## Next steps

- [React quickstart](./react.md) — same hooks, no Next.js.
- [Multi-tenancy](../multi-tenancy.md) — tenant pickers and tenant-scoped tokens.
- [API Reference](../api/overview.md) — calling the Management API from your Next.js backend.
