---
title: How to add user authentication to a Next.js app with Whop OAuth
slug: add-user-authentication
excerpt: You can add user authentication to your app using Next.js and Whop by building a couple of new routes and dropping in the login and logout elements in your app.
customExcerpt: You can add user authentication to your app using Next.js and Whop by building a couple of new routes and dropping in the login and logout elements in your app.
featureImage: "https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/06/user-authentication-thumbnail.webp"
status: published
publishedAt: "2026-06-06T04:13:31.000Z"
updatedAt: "2026-06-06T05:00:54.000Z"
createdAt: "2026-05-29T06:00:32.423Z"
tags:
  - { name: Tutorials, slug: tutorials }
  - { name: Developers, slug: developers }
authors:
  - { name: East, slug: east }
  - { name: Destinee Walston, slug: destinee }
---

# How to add user authentication to a Next.js app with Whop OAuth

## Key takeaways

- Whop OAuth lets Next.js developers add user authentication without building login forms or managing credentials themselves.
- The integration relies on PKCE flow with short-lived cookies for state and encrypted iron-session cookies for persistent user identity.
- Developers should build and test in Whop's sandbox environment before flipping the WHOP_SANDBOX flag for production deployment.

You acn add user authentication to an existing Next.js using Whop OAuth, and it's much simpler than other user authentication methods.

When it comes to implementing such feature, it usually means building login forms, storing and handling credentials, and securing all that data. Luckily for us, Whop handles all that, and the only thing we need to do is to wire up Whop OAuth to our project.

By the end of this tutorial, your app will have a "Sign in with Whop" button, users will be redirected back to `/api/auth/callback` once the login is complete, the user details like id, email, name, and profile picture will live in an encrypted iron-session cookie, and more.

You can see how the simple OAuth flow works in our [companion demo](https://nextjs-whop-oauth-demo.vercel.app/), and hover over elements in the profile (after signing in) to see where and how we got specific information.

## Prerequisites

This article assumes your app already runs on the Next.js, has a `lib/` folder for utilities, and isn't wired to another auth provider. For the integration, we add new files under `lib/`, `app/api/auth/`, and a `proxy.ts` at the project root.

Before any integration code, let's complete the prerequisites like the Whop app, the redirect URIs, and more.

### Create the Whop app

First, go to Sandbox.Whop.com, this is the sandbox environment of Whop where you can simulate everything from user authentication to payments without affecting the live, production build. We'll walk you through switching to production in the last section.

After signign in, create a new whop and go to its dashboard using the **Dashboard** button at the left navigation bar of your whop. There, find the **Developers** page.

In the **Developers** page, click the **Create app** button under the Apps section and give your app a name. This will take you to your app dashboard. There, click on the OAuth tab follow the steps below:

- Add two redirect URIs (exact match, including protocol and path): `http://localhost:3000/api/auth/callback` and `https://yourURL.com/api/auth/callback`
- Copy the **Client ID** (starts with `app_`) and the **Client Secret**
- Switch to the **Permissions** tab on the app dashboard and click **Add permission**, then enable `oauth:token_exchange` and click **Add**

Since the app lives in the sandbox, we'll set `WHOP_SANDBOX=true` in our env file in the next section. This tells our OAuth helper to hit `sandbox-api.whop.com` instead of the production `api.whop.com`. We'll flip it off in the production checklist at the end.

### Install packages

We need to install two packages, `iron-session` for the encrypted session cookie and `zod` for env and response validation, using the command below.

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">Terminal</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-bash">npm install iron-session zod</code></pre>
  </div>
</div>

### Environment variables

Add these to `.env.local` (and Vercel's environment variables when we deploy). Treat Vercel as the source of truth and pull locally with `vercel env pull .env.local`.

<table>
<tbody><tr><th>Variable</th><th>Example</th><th>How to get it</th></tr>
<tr><td><code>WHOP_CLIENT_ID</code></td><td><code>app_...</code></td><td>Whop dashboard → Developer → Apps → OAuth section.</td></tr>
<tr><td><code>WHOP_CLIENT_SECRET</code></td><td><code>...</code></td><td>Same OAuth section. Shown once on creation; regenerate if lost.</td></tr>
<tr><td><code>WHOP_SANDBOX</code></td><td><code>true</code></td><td>Set manually. <code>true</code> in development; remove or set to <code>false</code> in production.</td></tr>
<tr><td><code>SESSION_SECRET</code></td><td><code>...</code></td><td>Generate with <code>openssl rand -base64 32</code>. 32+ chars. iron-session uses it to encrypt the cookie.</td></tr>
<tr><td><code>NEXT_PUBLIC_APP_URL</code></td><td><code>http://localhost:3000</code></td><td>Our app origin. Must match a registered redirect URI's protocol and domain exactly.</td></tr>
</tbody></table>

### Validate env vars at startup

Go to `lib/` and create a file called `env.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">env.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { z } from &quot;zod&quot;;

const envSchema = z.object({
  WHOP_CLIENT_ID: z.string().startsWith(&quot;app_&quot;),
  WHOP_CLIENT_SECRET: z.string().min(1),
  WHOP_SANDBOX: z
    .string()
    .optional()
    .transform((v) =&gt; v === &quot;true&quot;),
  SESSION_SECRET: z.string().min(32),
  NEXT_PUBLIC_APP_URL: z.string().url(),
});

type Env = z.infer&lt;typeof envSchema&gt;;

let cached: Env | null = null;

function parseAll(): Env {
  if (cached) return cached;
  cached = envSchema.parse({
    WHOP_CLIENT_ID: process.env.WHOP_CLIENT_ID,
    WHOP_CLIENT_SECRET: process.env.WHOP_CLIENT_SECRET,
    WHOP_SANDBOX: process.env.WHOP_SANDBOX,
    SESSION_SECRET: process.env.SESSION_SECRET,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  });
  return cached;
}

export const env = new Proxy({} as Env, {
  get(_target, prop: string) {
    return parseAll()[prop as keyof Env];
  },
});</code></pre>
  </div>
</div>

## The auth flow

Six things happen between the click and the dashboard render:

- The user clicks **Sign in with Whop**, which is a link to `/api/auth/login`
- The login route generates a PKCE verifier, a random `state`, and a random `nonce`, stores all three in a short-lived httpOnly cookie, and redirects to `https://api.whop.com/oauth/authorize?...`
- Whop shows the consent screen (or skips it for returning users) and redirects back to `/api/auth/callback?code=...&state=...`
- The callback route reads the PKCE cookie, verifies the returned `state` matches, and POSTs to `https://api.whop.com/oauth/token` with the `code`, `code_verifier`, and `client_secret`
- We use the returned `access_token` to call `https://api.whop.com/oauth/userinfo` and read the user profile
- We write the user profile and tokens into an iron-session cookie, clear the PKCE cookie, and redirect to `/dashboard`

After that, protected pages call `requireUser()` to check the session and either show the page or send the user to the login screen.

## The OAuth helper

Now, let's build the OAuth helper for PKCE generation, URL builder authorization, token exchange, user information fetching, and call revokes. The login, callback, and logout routes will import from here.

Go to `lib/` and create a file called `whop-oauth.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">whop-oauth.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { z } from &quot;zod&quot;;
import { env } from &quot;@/lib/env&quot;;

function getBaseUrl(): string {
  return env.WHOP_SANDBOX
    ? &quot;https://sandbox-api.whop.com&quot;
    : &quot;https://api.whop.com&quot;;
}

const tokenResponseSchema = z.object({
  access_token: z.string(),
  refresh_token: z.string(),
  id_token: z.string().optional(),
  token_type: z.string(),
  expires_in: z.number(),
});

const userInfoSchema = z.object({
  sub: z.string(),
  name: z.string().optional(),
  preferred_username: z.string().optional(),
  picture: z.string().url().optional(),
  email: z.string().email().optional(),
  email_verified: z.boolean().optional(),
});

export type WhopTokens = z.infer&lt;typeof tokenResponseSchema&gt;;
export type WhopUserInfo = z.infer&lt;typeof userInfoSchema&gt;;

export interface PkceState {
  codeVerifier: string;
  state: string;
  nonce: string;
}

function base64url(bytes: Uint8Array): string {
  return Buffer.from(bytes)
    .toString(&quot;base64&quot;)
    .replace(/\+/g, &quot;-&quot;)
    .replace(/\//g, &quot;_&quot;)
    .replace(/=+$/, &quot;&quot;);
}

export function randomString(length: number): string {
  return base64url(crypto.getRandomValues(new Uint8Array(length)));
}

export async function sha256Challenge(verifier: string): Promise&lt;string&gt; {
  const data = new TextEncoder().encode(verifier);
  const digest = await crypto.subtle.digest(&quot;SHA-256&quot;, data);
  return base64url(new Uint8Array(digest));
}

export async function buildAuthorizeUrl(pkce: PkceState): Promise&lt;string&gt; {
  const params = new URLSearchParams({
    response_type: &quot;code&quot;,
    client_id: env.WHOP_CLIENT_ID,
    redirect_uri: `${env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
    scope: &quot;openid profile email&quot;,
    state: pkce.state,
    nonce: pkce.nonce,
    code_challenge: await sha256Challenge(pkce.codeVerifier),
    code_challenge_method: &quot;S256&quot;,
  });
  return `${getBaseUrl()}/oauth/authorize?${params.toString()}`;
}

export async function exchangeCodeForTokens(
  code: string,
  codeVerifier: string,
): Promise&lt;WhopTokens&gt; {
  const response = await fetch(`${getBaseUrl()}/oauth/token`, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
    body: JSON.stringify({
      grant_type: &quot;authorization_code&quot;,
      code,
      redirect_uri: `${env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
      client_id: env.WHOP_CLIENT_ID,
      client_secret: env.WHOP_CLIENT_SECRET,
      code_verifier: codeVerifier,
    }),
  });

  if (!response.ok) {
    const err: unknown = await response.json().catch(() =&gt; ({}));
    throw new Error(
      `Token exchange failed (${response.status}): ${JSON.stringify(err)}`,
    );
  }

  return tokenResponseSchema.parse(await response.json());
}

export async function fetchUserInfo(
  accessToken: string,
): Promise&lt;WhopUserInfo&gt; {
  const response = await fetch(`${getBaseUrl()}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  if (!response.ok) {
    throw new Error(`Userinfo failed (${response.status})`);
  }
  return userInfoSchema.parse(await response.json());
}

export async function revokeToken(refreshToken: string): Promise&lt;void&gt; {
  await fetch(`${getBaseUrl()}/oauth/revoke`, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
    body: JSON.stringify({
      token: refreshToken,
      client_id: env.WHOP_CLIENT_ID,
    }),
  });
}</code></pre>
  </div>
</div>

> 

## The cookies

Now, we need to create two helpers for two cookies. First one, the PKCE cookie, is a short-lived cookie and we only use it between the redirect to Whop and the callback. The session cookie is the persistent user identitfy afterwards.

### The PKCE cookie

Go to `lib/` and create a file called `pkce-cookie.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">pkce-cookie.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { cookies } from &quot;next/headers&quot;;
import { z } from &quot;zod&quot;;
import type { PkceState } from &quot;@/lib/whop-oauth&quot;;

const PKCE_COOKIE = &quot;whop_pkce&quot;;

const pkceSchema = z.object({
  codeVerifier: z.string(),
  state: z.string(),
  nonce: z.string(),
});

export async function setPkceCookie(pkce: PkceState): Promise&lt;void&gt; {
  const store = await cookies();
  store.set(PKCE_COOKIE, JSON.stringify(pkce), {
    httpOnly: true,
    secure: process.env.NODE_ENV === &quot;production&quot;,
    sameSite: &quot;lax&quot;,
    path: &quot;/&quot;,
    maxAge: 600,
  });
}

export async function readPkceCookie(): Promise&lt;PkceState | null&gt; {
  const store = await cookies();
  const value = store.get(PKCE_COOKIE)?.value;
  if (!value) return null;
  try {
    return pkceSchema.parse(JSON.parse(value));
  } catch {
    return null;
  }
}

export async function clearPkceCookie(): Promise&lt;void&gt; {
  const store = await cookies();
  store.delete(PKCE_COOKIE);
}</code></pre>
  </div>
</div>

### The session cookie

Go to `lib/` and create a file called `session.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">session.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import {
  getIronSession,
  type IronSession,
  type SessionOptions,
} from &quot;iron-session&quot;;
import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { env } from &quot;@/lib/env&quot;;
import type { WhopTokens, WhopUserInfo } from &quot;@/lib/whop-oauth&quot;;

export interface SessionData {
  user?: WhopUserInfo;
  tokens?: WhopTokens;
  tokensObtainedAt?: number;
}

export function sessionOptions(): SessionOptions {
  return {
    password: env.SESSION_SECRET,
    cookieName: &quot;whop_session&quot;,
    cookieOptions: {
      httpOnly: true,
      secure: process.env.NODE_ENV === &quot;production&quot;,
      sameSite: &quot;lax&quot;,
      path: &quot;/&quot;,
    },
  };
}

export async function getSession(): Promise&lt;IronSession&lt;SessionData&gt;&gt; {
  const store = await cookies();
  return getIronSession&lt;SessionData&gt;(store, sessionOptions());
}

export async function getCurrentUser(): Promise&lt;WhopUserInfo | null&gt; {
  const session = await getSession();
  return session.user ?? null;
}

export async function requireUser(): Promise&lt;WhopUserInfo&gt; {
  const user = await getCurrentUser();
  if (!user) redirect(&quot;/&quot;);
  return user;
}</code></pre>
  </div>
</div>

`requireUser()` returns the signed-in user or redirects to `/`. Since it never returns `null`, you can read `user.email` directly without a null check.

## Start the flow

The login route generates fresh PKCE state, stores it in the PKCE cookie, and redirects to Whop. Go to `app/api/auth/login/` and create a file called `route.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">route.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { NextResponse } from &quot;next/server&quot;;
import {
  buildAuthorizeUrl,
  randomString,
  type PkceState,
} from &quot;@/lib/whop-oauth&quot;;
import { setPkceCookie } from &quot;@/lib/pkce-cookie&quot;;

export async function GET(): Promise&lt;NextResponse&gt; {
  const pkce: PkceState = {
    codeVerifier: randomString(32),
    state: randomString(16),
    nonce: randomString(16),
  };

  await setPkceCookie(pkce);
  const authorizeUrl = await buildAuthorizeUrl(pkce);
  return NextResponse.redirect(authorizeUrl);
}</code></pre>
  </div>
</div>

A "Sign in with Whop" button is a plain link to this route. Use it anywhere in a page where you want to render a sign-in:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">Sign in element</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-html">&lt;a href=&quot;/api/auth/login&quot;&gt;Sign in with Whop&lt;/a&gt;</code></pre>
  </div>
</div>

## Handle the callback

After the user approves the information request your app makes in the Whop hosted login page, Whop redirects them to `/api/auth/callback?code=...&state=...`.

Then, we verify the state, exchange the code, get the profile, write the session, and clear the PKCE cookie.

Go to `app/api/auth/callback/` and create a file called `route.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">route.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { NextResponse, type NextRequest } from &quot;next/server&quot;;
import { clearPkceCookie, readPkceCookie } from &quot;@/lib/pkce-cookie&quot;;
import { exchangeCodeForTokens, fetchUserInfo } from &quot;@/lib/whop-oauth&quot;;
import { getSession } from &quot;@/lib/session&quot;;
import { env } from &quot;@/lib/env&quot;;

function redirectTo(path: string): NextResponse {
  return NextResponse.redirect(`${env.NEXT_PUBLIC_APP_URL}${path}`);
}

export async function GET(request: NextRequest): Promise&lt;NextResponse&gt; {
  const url = new URL(request.url);
  const code = url.searchParams.get(&quot;code&quot;);
  const returnedState = url.searchParams.get(&quot;state&quot;);
  const error = url.searchParams.get(&quot;error&quot;);

  if (error) {
    console.error(
      &quot;[oauth] callback error:&quot;,
      error,
      url.searchParams.get(&quot;error_description&quot;),
    );
    return redirectTo(`/?error=${encodeURIComponent(error)}`);
  }

  if (!code || !returnedState) {
    return redirectTo(&quot;/?error=missing_params&quot;);
  }

  const pkce = await readPkceCookie();
  if (!pkce || pkce.state !== returnedState) {
    return redirectTo(&quot;/?error=state_mismatch&quot;);
  }

  try {
    const tokens = await exchangeCodeForTokens(code, pkce.codeVerifier);
    const user = await fetchUserInfo(tokens.access_token);

    const session = await getSession();
    session.user = user;
    session.tokens = tokens;
    session.tokensObtainedAt = Date.now();
    await session.save();

    await clearPkceCookie();
    return redirectTo(&quot;/dashboard&quot;);
  } catch (err) {
    console.error(&quot;[oauth] callback failed:&quot;, err);
    return redirectTo(&quot;/?error=oauth_failed&quot;);
  }
}</code></pre>
  </div>
</div>

## Protect routes

We will secure the routes using two separate layers so that users who are not logged in cannot view the protected pages, and so that the system can verify that logged-in users are who they claim to be.

### Proxy

Go to the project root and create a file called `proxy.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">proxy.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { NextResponse, type NextRequest } from &quot;next/server&quot;;
import { getIronSession } from &quot;iron-session&quot;;
import { sessionOptions, type SessionData } from &quot;@/lib/session&quot;;

export async function proxy(request: NextRequest) {
  const response = NextResponse.next();

  const session = await getIronSession&lt;SessionData&gt;(
    request,
    response,
    sessionOptions(),
  );

  if (!session.user) {
    return NextResponse.redirect(new URL(&quot;/&quot;, request.url));
  }

  return response;
}

export const config = {
  matcher: [&quot;/dashboard/:path*&quot;],
};</code></pre>
  </div>
</div>

### Using `requireUser()` in a page

From now on, you should call the `await requireUser()` function at the top of any server component that should be gated. If you don't already have one, a simple `app/dashboard/page.tsx` looks like:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">page.tsx</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { requireUser } from &quot;@/lib/session&quot;;

export default async function DashboardPage() {
  const user = await requireUser();
  return (
    &lt;main&gt;
      &lt;h1&gt;Welcome, {user.name ?? user.preferred_username ?? user.email}&lt;/h1&gt;
      &lt;p&gt;Whop user ID: {user.sub}&lt;/p&gt;
    &lt;/main&gt;
  );
}</code></pre>
  </div>
</div>

## Logout

The logout route makes the refresh token on Whop's side invalid so that if a token is compromised, it can't be reused, and destroys the local session.

Go to `app/api/auth/logout/` and create a file called `route.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">route.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import { NextResponse } from &quot;next/server&quot;;
import { getSession } from &quot;@/lib/session&quot;;
import { revokeToken } from &quot;@/lib/whop-oauth&quot;;
import { env } from &quot;@/lib/env&quot;;

export async function POST(): Promise&lt;NextResponse&gt; {
  const session = await getSession();

  if (session.tokens?.refresh_token) {
    try {
      await revokeToken(session.tokens.refresh_token);
    } catch (err) {
      console.error(&quot;[oauth] revoke failed:&quot;, err);
    }
  }

  session.destroy();
  return NextResponse.redirect(`${env.NEXT_PUBLIC_APP_URL}/`, {
    status: 303,
  });
}</code></pre>
  </div>
</div>

Then, you can use the logout element anywhere you want by adding:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">Sign out element</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-html">&lt;form action=&quot;/api/auth/logout&quot; method=&quot;POST&quot;&gt;
  &lt;button type=&quot;submit&quot;&gt;Sign out&lt;/button&gt;
&lt;/form&gt;</code></pre>
  </div>
</div>

## Sandbox to production

To take your user authentication live, you need to switch from the sandbox environment to production. You can do this by:

- Creating a new Whop app - Go to Whop.com, create a whop, and a new app under it. Register the production redirect URI, enable the same OAuth scopes and `oauth:token_exchange` permission, and copy the new client ID and secret.
- Update your environment variables for the production environment:
- - `WHOP_CLIENT_ID` - production client ID
  - `WHOP_CLIENT_SECRET` - production client secret
  - `WHOP_SANDBOX` - remove it or set to `false
  - `NEXT_PUBLIC_APP_URL` - production URL (must match a registered redirect URI exactly)
  - `SESSION_SECRET` - a fresh value (rotating it logs everyone out, which is the right move on the first production deploy)

## Use the Whop infrastructure in your platform

Now, you know everything you need to implement a user authentication to your app using the Whop infrastructure, but that's not the only thing our infrastructure offers.

With Whop, you can add everything from a [payments](https://whop.com/blog/add-checkout-to-nextjs-app/) to live chats to your app. To learn more about the capabilities of Whop and how you can use them, check out our other [tutorials](https://whop.com/blog/t/tutorials/) and the [Whop developer documentation](https://docs.whop.com/).

**[Go to Whop developer docs](https://docs.whop.com/)**
