Skip to content

Next.js

Integrate Authany authentication into a Next.js App Router application using the @authgear/nextjs SDK. You will set up login, logout, display user info, and protect a server-side API route.

A complete example application is available at authgear/authgear-example-nextjs.

What you will build:

  • A home page that shows a Login button when unauthenticated, and the user's identity + Logout button when authenticated
  • A protected API route (/api/me) that returns the current user's info

Setting Up Your Application in Authany

Step 1: Create an Application in the Portal

  1. Sign in to the Authany Portal
  2. Select or create a Project
  3. Navigate to Applications in the left menu
  4. Click ⊕ Add Application
  5. Enter an application name and select Single Page Application as the application type
  6. Click Save

Step 2: Configure the Application

  1. Under URIs, find the Authorized Redirect URIs field
  2. Add http://localhost:3000/api/auth/callback
  3. Note down your Client ID and Endpoint (e.g. https://your-project.authanyid.com) — you will need these shortly
  4. Click Save

Building Your Next.js Application

Step 1: Create a Next.js Project

npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app

Step 2: Install the Authany SDK

npm install @authgear/nextjs

Step 3: Configure Environment Variables

Create .env.local with the following content:

AUTHGEAR_ENDPOINT=https://your-project.authanyid.com
AUTHGEAR_CLIENT_ID=your-client-id
AUTHGEAR_REDIRECT_URI=http://localhost:3000/api/auth/callback
SESSION_SECRET=a-random-string-of-at-least-32-characters

Note

SESSION_SECRET encrypts the session cookie stored in the browser. It must be at least 32 characters. Use a random string generator to create one.

Step 4: Create the Shared Authany Config

Create src/lib/authgear.ts. This file holds your Authany configuration and is imported by both server-side and client-side code:

// src/lib/authgear.ts
import type { AuthgearConfig } from "@authgear/nextjs";

export const authgearConfig: AuthgearConfig = {
  endpoint: process.env.AUTHGEAR_ENDPOINT!,
  clientID: process.env.AUTHGEAR_CLIENT_ID!,
  redirectURI: process.env.AUTHGEAR_REDIRECT_URI!,
  sessionSecret: process.env.SESSION_SECRET!,
};

Step 5: Add the OAuth Route Handler

Create src/app/api/auth/[...authgear]/route.ts. This catch-all route handles all Authany auth endpoints automatically:

// src/app/api/auth/[...authgear]/route.ts
import { createAuthgearHandlers } from "@authgear/nextjs";
import { authgearConfig } from "@/lib/authgear";

export const { GET, POST } = createAuthgearHandlers(authgearConfig);

createAuthgearHandlers registers the following routes for you:

Method Path Purpose
GET /api/auth/login Start the OAuth login flow
GET /api/auth/callback Handle the OAuth callback and set the session cookie
GET /api/auth/logout Clear the session and revoke tokens
POST /api/auth/refresh Refresh an expired access token
GET /api/auth/userinfo Return the current user's info
GET /api/auth/open Open a pre-authenticated Authany-hosted page

Step 6: Add AuthgearProvider to Your Layout

AuthgearProvider makes the user's session state available to all Client Components via React context. Because it uses browser APIs, it must be a Client Component.

Create src/app/providers.tsx:

// src/app/providers.tsx
"use client";

import { AuthgearProvider } from "@authgear/nextjs/client";

export default function Providers({ children }: { children: React.ReactNode }) {
  return <AuthgearProvider>{children}</AuthgearProvider>;
}

Then wrap your root layout in src/app/layout.tsx:

// src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Providers from "./providers";

const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Next.js + Authgear",
  description: "Example app demonstrating Authgear authentication with Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

On mount, AuthgearProvider fetches /api/auth/userinfo to check for an existing session. The result is available via the useAuthgear() hook.

Step 7: Implement the Home Page

Replace src/app/page.tsx. The page is a Client Component so it can use useAuthgear() to read session state and react to changes:

// src/app/page.tsx
"use client";

import { useState } from "react";
import { useAuthgear, SignInButton, SignOutButton } from "@authgear/nextjs/client";

export default function Home() {
  const { isAuthenticated, user } = useAuthgear();
  const [apiResult, setApiResult] = useState<string | null>(null);

  async function testProtectedApi() {
    const res = await fetch("/api/me");
    const data = await res.json();
    setApiResult(JSON.stringify(data, null, 2));
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-center gap-6">
      <h1 className="text-3xl font-bold">Next.js + Authgear</h1>

      {isAuthenticated ? (
        <>
          <p className="text-gray-600">
            Logged in as: <span className="font-mono">{user?.sub}</span>
          </p>
          {(user?.email ?? user?.phoneNumber) && (
            <p className="text-gray-600">
              {user?.email ?? user?.phoneNumber}
            </p>
          )}
          <button
            onClick={testProtectedApi}
            className="rounded-md bg-green-600 px-6 py-2 text-white hover:bg-green-700"
          >
            Test Protected API
          </button>
          {apiResult && (
            <pre className="rounded-md bg-gray-100 dark:bg-gray-800 p-4 text-sm">{apiResult}</pre>
          )}
          <SignOutButton className="rounded-md bg-red-600 px-6 py-2 text-white hover:bg-red-700">
            Logout
          </SignOutButton>
        </>
      ) : (
        <SignInButton className="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">
          Login
        </SignInButton>
      )}
    </main>
  );
}

Key hooks and components:

  • useAuthgear() — returns { isAuthenticated, user, state, isLoaded, signIn, signOut }. user is a UserInfo object with sub, email, phoneNumber, and other profile fields.
  • <SignInButton> — navigates to /api/auth/login on click, starting the OAuth flow.
  • <SignOutButton> — navigates to /api/auth/logout on click, clearing the session.

Step 8: Create a Protected API Route

Server-side route handlers can verify authentication using currentUser() from @authgear/nextjs/server. It reads the encrypted session cookie and returns the current user, or null if not authenticated.

Create src/app/api/me/route.ts:

// src/app/api/me/route.ts
import { currentUser } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
import { NextResponse } from "next/server";

export async function GET() {
  const user = await currentUser(authgearConfig);

  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json({ user });
}

currentUser() automatically refreshes the access token if expired before fetching user info from the Authany userinfo endpoint.


Running the Application

npm run dev

Open http://localhost:3000 in your browser.


Testing the Integration

Login flow

  1. Visit http://localhost:3000 — you should see a Login button
  2. Click Login — you are redirected to the Authany hosted login page
  3. Sign in with your credentials
  4. You are redirected back to the home page, now showing your user ID and email/phone number

Logout flow

  1. Click Logout — the session is cleared and you are returned to the login screen

Protected API

  1. After logging in, click Test Protected API
  2. The page calls GET /api/me, which verifies your session and returns your user info:
{
  "user": {
    "sub": "...",
    "email": "...",
    "emailVerified": true
  }
}
  1. To verify the route is protected, open an incognito window and run:
curl http://localhost:3000/api/me

Expected response with status 401:

{ "error": "Unauthorized" }

Project Structure

src/
├── app/
│   ├── api/
│   │   ├── auth/
│   │   │   └── [...authgear]/
│   │   │       └── route.ts      # OAuth route handler
│   │   └── me/
│   │       └── route.ts          # Protected API route
│   ├── layout.tsx                # Root layout with AuthgearProvider
│   ├── page.tsx                  # Home page with login/logout UI
│   └── providers.tsx             # Client component wrapping AuthgearProvider
└── lib/
    └── authgear.ts               # Shared Authgear config

Additional Topics

Reading the Session in Server Components

Use auth() to read the raw session (state + tokens) without making a network request:

import { auth } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";

export default async function MyServerComponent() {
  const session = await auth(authgearConfig);

  if (session.state !== "AUTHENTICATED") {
    return <p>Not logged in</p>;
  }

  return <p>Access token expires at: {session.expiresAt}</p>;
}

Use currentUser() when you need the full user profile — it makes an extra request to the userinfo endpoint and handles token refresh automatically.

Verifying a Bearer Token in an External API

If you have a separate API server that receives Authany-issued access tokens (e.g. from a mobile app), use verifyAccessToken() to validate them:

import { verifyAccessToken } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const authHeader = request.headers.get("authorization");
  if (!authHeader?.startsWith("Bearer ")) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const token = authHeader.slice(7);

  try {
    const payload = await verifyAccessToken(token, authgearConfig);
    return NextResponse.json({ sub: payload.sub });
  } catch {
    return NextResponse.json({ error: "Invalid token" }, { status: 401 });
  }
}

Note

verifyAccessToken validates the JWT signature and expiry locally using the Authany JWKS endpoint — no session cookie is involved.

Checking Session State

useAuthgear() returns isLoaded to indicate whether the initial session check has completed. Use it to avoid a flash of unauthenticated UI:

const { isAuthenticated, isLoaded } = useAuthgear();

if (!isLoaded) return <p>Loading...</p>;

Opening User Settings

The <UserSettingsButton> component opens Authany's hosted Settings page in a new tab with the current user already authenticated — no Server Action required.

Add the button to your component:

import { UserSettingsButton } from "@authgear/nextjs/client";

export default function Dashboard() {
  return (
    <UserSettingsButton>Account Settings</UserSettingsButton>
  );
}

The button must be rendered inside <AuthgearProvider>. It exchanges the user's session for a short-lived token and opens the pre-authenticated Settings URL in a new tab.

Style it like any button<UserSettingsButton> accepts all standard HTML button props:

<UserSettingsButton
  className="btn btn-primary"
  style={{ padding: "0.5rem 1rem" }}
  disabled={!isAuthenticated}
>
  Account Settings
</UserSettingsButton>

The default label is "Account Settings" if no children are provided.

Customising the route path

By default, <UserSettingsButton> sends requests to /api/auth/open. If your auth routes live at a different path, set openPagePath on <AuthgearProvider>:

<AuthgearProvider openPagePath="/api/my-auth/open">
  {children}
</AuthgearProvider>

Error behaviour

If the user is not authenticated or their session has no refresh token, the new tab will display an error rather than the Settings page. Guard the button with an authentication check to avoid this:

const { isAuthenticated } = useAuthgear();

<UserSettingsButton disabled={!isAuthenticated}>
  Account Settings
</UserSettingsButton>

Controlling Single Sign-On Behaviour

By default, Authany reuses its server-side session across sign-ins (SSO enabled). You can change this globally via isSSOEnabled in createAuthgearHandlers, or override it per sign-in call using the prompt option.

Disable SSO globally — always show the login form:

// src/app/api/auth/[...authgear]/route.ts
import { createAuthgearHandlers } from "@authgear/nextjs";
import { authgearConfig } from "@/lib/authgear";

export const { GET, POST } = createAuthgearHandlers({
  ...authgearConfig,
  isSSOEnabled: false,
});

Override per sign-in call using PromptOption:

import { PromptOption } from "@authgear/nextjs";

// via hook
const { signIn } = useAuthgear();
signIn({ prompt: PromptOption.Login }); // always show login form

// via component
<SignInButton signInOptions={{ prompt: PromptOption.Login }}>Sign In</SignInButton>
PromptOption Effect
PromptOption.Login Always show the login form
PromptOption.None Never show the login form; error if not authenticated

Per-call prompt takes precedence over the global isSSOEnabled setting.

Next Steps