# Auth0 Authentication in Webflow

## **What do you need to add Auth0 authentication to a Webflow site?**

You need an Auth0 account configured as a Regular Web Application, a Webflow Cloud App, and three environment variables before writing any code.

Here are the prerequisites:

- **An [Auth0 account](https://auth0.com/):** Create it at auth0.com if you don't have one. In the Auth0 dashboard, create a Regular Web Application. Regular Web App issues a client secret that the server uses to exchange the auth code for tokens server-side.
- **A [Webflow Cloud App](/content/blog/webflow-cloud-full-stack-apps/index.html) already scaffolded:** Using Next.js (this guide uses Next.js App Router, but the same approach works with Astro); run `webflow auth login && webflow cloud init` if you haven't yet.
- **[Custom code access](https://help.webflow.com/hc/en-us/articles/33961332238611-Custom-code-embed) on your Webflow site:** For the login/logout button wiring in the last step. Available on any paid Site plan.

Once these are in place, feel free to proceed to the setup.

## **5 steps to add Auth0 authentication to a Webflow site with Webflow Cloud**

The setup splits cleanly across three systems:

- Auth0 owns identity: hosted login, session issuance, token signing.
- The Webflow Cloud App owns the OAuth dance and the protected data: it exchanges codes for tokens, validates JWTs on every request, and serves gated content only when validation passes.
- The Webflow site owns the UI shell: login and logout buttons, plus a thin client that calls the Cloud App to read auth state and render accordingly.

### **1. Configure the Auth0 Regular Web Application**

In the Auth0 dashboard, open your Regular Web Application and navigate to Settings. You need to configure three URL fields and copy three values.

Under Application URIs, fill in:

```
Allowed Callback URLs:
http://localhost:3000/app/auth/callback, https://yoursite.webflow.io/app/auth/callback, https://yourdomain.com/app/auth/callback

Allowed Logout URLs:
http://localhost:3000, https://yoursite.webflow.io, https://yourdomain.com

Allowed Web Origins:
http://localhost:3000, https://yoursite.webflow.io, https://yourdomain.com
```

Register both your `.webflow.io` staging domain and your production custom domain in each field. Every URL must match exactly, including the protocol and path. If Auth0 receives a callback URL that isn't registered here, it rejects the request.

Click Save Changes.

Then, still on the Settings tab, copy the values for Domain, Client ID, and Client Secret. These will be added to your Cloud App environment in the next step.

### **2. Add Auth0 credentials to your Webflow Cloud environment**

Your [Cloud App](/content/blog/getting-started-webflow-cms-api/index.html) needs four environment variables.

Add them to `.env.local` for local development:

```
# .env.local
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=your_client_id_here
AUTH0_CLIENT_SECRET=your_client_secret_here
AUTH0_CALLBACK_URL=http://localhost:3000/app/auth/callback
```

In your Webflow site settings, open Webflow Cloud, select your environment, and add the same four variables under Environment Variables. `AUTH0_CLIENT_SECRET` gives anyone who holds it the ability to exchange auth codes for tokens on your behalf. Keep it out of client-side code, git history, and any public-facing endpoint.

**Checkpoint:** Run `next dev` locally and confirm `process.env.AUTH0_DOMAIN` resolves.

### **3. Build the auth Route Handlers**

You need to create three Route Handlers:

- One to start the login flow
- One to handle the Auth0 callback
- One to log users out

Create `lib/auth.ts` first with the JWT verification helper you'll use in every protected endpoint:

```javascript
// lib/auth.ts
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose'

const domain = process.env.AUTH0_DOMAIN!
export const JWKS = createRemoteJWKSet(
  new URL(`https://${domain}/.well-known/jwks.json`)
)

export async function verifyToken(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: `https://${domain}/`,
    audience: process.env.AUTH0_CLIENT_ID,
  })
  return payload
}

export function getTokenFromCookie(cookieHeader: string | null): string | null {
  if (!cookieHeader) return null
  const match = cookieHeader.match(/auth_token=([^;]+)/)
  return match ? decodeURIComponent(match[1]) : null
}
```

Now create the three Route Handlers:

```javascript
// app/auth/login/route.ts
export const runtime = 'edge'

export async function GET() {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.AUTH0_CLIENT_ID!,
    redirect_uri: process.env.AUTH0_CALLBACK_URL!,
    scope: 'openid profile email',
  })

return Response.redirect(
    `https://${process.env.AUTH0_DOMAIN}/authorize?${params}`
  )
}
```

The login route just constructs Auth0's `/authorize` URL with the right query parameters and redirects the browser to it. Auth0 takes over the login UI from there. No client secret is exposed because the secret isn't used in this leg of the flow.

### **4. Protect your Cloud App content endpoints**

Any Route Handler that returns content requiring login must verify the token before responding. This is the data-layer check that makes protection real.

Here's an example protected endpoint that returns user-specific data:

```javascript
// app/api/me/route.ts
export const runtime = 'edge'

import { verifyToken, getTokenFromCookie } from '@/lib/auth'

export async function GET(request: Request) {
  const token = getTokenFromCookie(request.headers.get('cookie'))

if (!token) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

try {
    const payload = await verifyToken(token)

return Response.json({
      name: payload.name,
      email: payload.email,
      sub: payload.sub,
    })
  } catch {
    return Response.json({ error: 'Invalid session' }, { status: 401 })
  }
}
```

For any Route Handler that serves gated content, follow the same pattern: call `getTokenFromCookie()`, call `verifyToken()`, return 401 if either step fails. Content is sent only if the JWT is valid.

### **5. Wire login and logout on the Webflow site**

Add the following script to a Custom Code block on any page that has login/logout buttons. The buttons need IDs: `login-btn` for the login action and `logout-btn` for the logout action.

```javascript
(function () {
  const loginBtn = document.getElementById('login-btn')
  const logoutBtn = document.getElementById('logout-btn')
  const userDisplay = document.getElementById('user-display')

// Wire login and logout buttons
  if (loginBtn) {
    loginBtn.addEventListener('click', function () {
      window.location.href = '/app/auth/login'
    })
  }

if (logoutBtn) {
    logoutBtn.addEventListener('click', function () {
      window.location.href = '/app/auth/logout'
    })
  }

// Fetch user info from the authenticated Cloud App endpoint
  async function loadUserInfo() {
    try {
      const res = await fetch('/app/api/me', { credentials: 'include' })

if (!res.ok) {
        // Not authenticated: show login state
        if (loginBtn) loginBtn.style.display = 'block'
        if (logoutBtn) logoutBtn.style.display = 'none'
        return
      }

const user = await res.json()

// Authenticated: update UI
      if (loginBtn) loginBtn.style.display = 'none'
      if (logoutBtn) logoutBtn.style.display = 'block'
      if (userDisplay) userDisplay.textContent = `Hi, ${user.name}`
    } catch {
      // Network error: show login state as fallback
      if (loginBtn) loginBtn.style.display = 'block'
      if (logoutBtn) logoutBtn.style.display = 'none'
    }
  }

loadUserInfo()
})()
```

## **What causes Auth0 authentication to fail on a Webflow Cloud App?**

Most failures stem from one of three causes:

- Misconfigured callback URLs on the Auth0 side
- A token audience mismatch in the JWT verification step
- Reading environment variables that weren't actually injected into the Webflow Cloud environment

### **Auth0 rejects the callback with "callback URL mismatch"**

This means the URL your app is redirecting to after login isn't registered in the Allowed Callback URLs field.

**Checkpoint:** Validate your URLs and settings.

### **Token verification throws "jwt audience invalid"**

This means the `audience` parameter in `jwtVerify()` doesn't match the `aud` claim in the token.

**Checkpoint:** Verify your Auth0 Client ID.
