Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NUXT_AUTH0_DOMAIN=
NUXT_AUTH0_CLIENT_ID=
NUXT_AUTH0_CLIENT_SECRET=
NUXT_AUTH0_SESSION_SECRET=
# Comma-separated allow-list of the origins this single app serves.
# The SDK matches each request's origin against this list.
NUXT_AUTH0_APP_BASE_URL=http://app1.localhost:3000,http://app2.localhost:3000
24 changes: 24 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
75 changes: 75 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Nuxt Dynamic App Base URL Example

This example demonstrates the allow-list mode of the dynamic `appBaseUrl` feature in `@auth0/auth0-nuxt`.

A single Nuxt app serves two distinct domains — `app1.localhost` and `app2.localhost` — on port 3000 using the same Auth0 application. Instead of a single static `appBaseUrl`, the SDK is configured with an allow-list of origins. On each request it matches the incoming origin against the allow-list and uses the correct base URL for the callback `redirect_uri`, post-login redirect, and logout.

## Install dependencies

Install the dependencies using npm:

```bash
npm install
```

## Add `/etc/hosts` entries

So that both hostnames resolve to your machine, add the following to `/etc/hosts`:

```
127.0.0.1 app1.localhost
127.0.0.1 app2.localhost
```

(Many systems already resolve `*.localhost` to `127.0.0.1`, in which case this step can be skipped.)

## Configuration

Rename `.env.example` to `.env` and fill in your Auth0 credentials:

```ts
NUXT_AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
NUXT_AUTH0_CLIENT_ID=YOUR_AUTH0_CLIENT_ID
NUXT_AUTH0_CLIENT_SECRET=YOUR_AUTH0_CLIENT_SECRET
NUXT_AUTH0_SESSION_SECRET=YOUR_AUTH0_SESSION_SECRET
NUXT_AUTH0_APP_BASE_URL=http://app1.localhost:3000,http://app2.localhost:3000
```

The `NUXT_AUTH0_SESSION_SECRET` is the key used to encrypt the session cookie. You can generate a secret using `openssl`:

```shell
openssl rand -hex 64
```

`NUXT_AUTH0_APP_BASE_URL` is a **comma-separated allow-list** of the origins this app serves. The SDK parses this into an array and validates each request's origin against it.

## Configure your Auth0 tenant

Because the app serves two origins, both must be registered in your Auth0 application settings:

- **Allowed Callback URLs:** `http://app1.localhost:3000/auth/callback, http://app2.localhost:3000/auth/callback`
- **Allowed Logout URLs:** `http://app1.localhost:3000, http://app2.localhost:3000`
- **Allowed Web Origins:** `http://app1.localhost:3000, http://app2.localhost:3000`

## Run the app

```bash
npm run start
```

The application has 3 routes:

- `/`: The home route, displaying a message depending on the authentication state.
- `/public`: A public route that can be accessed without authentication.
- `/private`: A private route that can only be accessed by authenticated users. Navigating here while unauthenticated redirects to Auth0 and back.

## Test the dynamic base URL

Open each origin in your browser and walk through login/logout on each:

- http://app1.localhost:3000
- http://app2.localhost:3000

When you log in from `app1.localhost`, the SDK infers that origin, matches it against the allow-list, and uses `http://app1.localhost:3000/auth/callback` as the `redirect_uri`. The same flow on `app2.localhost` uses `http://app2.localhost:3000/auth/callback` — same configuration, correct URL per request. The current host is displayed in a banner at the top of each page so you can confirm which origin you are on.

If a request arrives from an origin that is not in the allow-list, the SDK throws an `InvalidConfigurationError`.
62 changes: 62 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script setup lang="ts">
const { value: user } = await useUser();

// Works on both server and client. The host reflects the origin the current
// request was served from (app1.localhost or app2.localhost), so you can
// confirm which origin you are on.
const host = useRequestURL().host;
</script>

<template>
<nav class="py-2 bg-body-tertiary border-bottom">
<div class="container d-flex flex-wrap">
<a
href="/"
class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none"
>
<img src="/img/auth0.png" width="36" height="36" alt="Auth0 logo" />
</a>
<ul class="nav me-auto">
<li class="nav-item">
<a
href="/public"
class="nav-link link-body-emphasis px-2 active"
aria-current="page"
>Public Page</a
>
</li>
<li class="nav-item">
<a
href="/private"
class="nav-link link-body-emphasis px-2 active"
aria-current="page"
>Private Page</a
>
</li>
</ul>
<ul class="nav">
<li class="nav-item">
<a
v-if="user"
class="nav-link link-body-emphasis px-2"
href="/auth/logout"
>Log out ({{ user?.name }})</a
>

<a
v-if="!user"
class="nav-link link-body-emphasis px-2"
href="/auth/login"
>Log in</a
>
</li>
</ul>
</div>
</nav>
<div class="alert alert-info rounded-0 mb-0 text-center" role="alert">
Serving from <strong>{{ host }}</strong>
</div>
<div class="container py-4">
<NuxtPage />
</div>
</template>
51 changes: 51 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
modules: [
[
'@auth0/auth0-nuxt',
{
mountRoutes: true,
},
],
],
imports: {
autoImport: true,
},
runtimeConfig: {
auth0: {
domain: '', // is overridden by NUXT_AUTH0_DOMAIN environment variable
clientId: '', // is overridden by NUXT_AUTH0_CLIENT_ID environment variable
clientSecret: '', // is overridden by NUXT_AUTH0_CLIENT_SECRET environment variable
sessionSecret: '', // is overridden by NUXT_AUTH0_SESSION_SECRET environment variable
// `appBaseUrl` is intentionally omitted here. It is provided as a
// comma-separated allow-list via the NUXT_AUTH0_APP_BASE_URL environment
// variable (see .env.example). The SDK parses it into an array and, on
// each request, matches the incoming origin against the allow-list to
// resolve the correct base URL for the callback, post-login redirect,
// and logout.
audience: '', // is overridden by NUXT_AUTH0_AUDIENCE environment variable
},
},
app: {
head: {
script: [
{
src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js',
integrity:
'sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz',
crossorigin: 'anonymous',
},
],
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css',
integrity:
'sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH',
crossorigin: 'anonymous',
},
],
},
},
});
24 changes: 24 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "example-nuxt-web-dynamic-app-base-url",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"start": "npm run dev"
},
"dependencies": {
"@auth0/auth0-nuxt": "*",
"nuxt": "^3.16.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@nuxt/devtools": "^3.2.3",
"@nuxt/eslint-config": "^1.15.2",
"eslint": "^9.20.1",
"typescript": "~5.9.3"
}
}
19 changes: 19 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
const { value: user } = await useUser();
const host = useRequestURL().host;
</script>

<template>
<div>
<h1>Welcome to the homepage</h1>
<p v-if="user">
You are logged in as <strong>{{ user?.name }}</strong> on
<strong>{{ host }}</strong
>.
</p>
<p v-else>
You are not logged in. Logging in from <strong>{{ host }}</strong> will
redirect you back to this same origin.
</p>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<template>This is a private page.</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<template>This is a public page.</template>
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default defineEventHandler(async (event) => {
const url = getRequestURL(event);

if (url.pathname === '/private') {
const auth0Client = useAuth0(event);
const session = await auth0Client.getSession();
if (!session) {
return sendRedirect(event, '/auth/login?returnTo=' + url.pathname);
}
}
})
4 changes: 4 additions & 0 deletions examples/example-nuxt-web-dynamic-app-base-url/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
26 changes: 26 additions & 0 deletions packages/auth0-nuxt/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [Configuration](#configuration)
- [Basic configuration](#basic-configuration)
- [Dynamic Application Base URL](#dynamic-application-base-url)
- [Configuring the mountes routes](#configuring-the-mountes-routes)
- [Configuring Stateful Sessions](#configuring-stateful-sessions)
- [Customizing State and Transaction Identifiers](#customizing-state-and-transaction-identifiers)
Expand Down Expand Up @@ -63,6 +64,31 @@ NUXT_AUTH0_APP_BASE_URL=http://localhost:3000
NUXT_AUTH0_SESSION_SECRET=<YOUR_LONG_RANDOM_SECRET>
```

### Dynamic Application Base URL

`appBaseUrl` supports serving multiple origins from a single Auth0 application. It can be configured in three ways:

- **Static** — a single URL string (the default). Use this when your app is served from one origin:

```
NUXT_AUTH0_APP_BASE_URL=https://app.example.com
```

- **Allow-list** — a comma-separated list of origins. On each request the SDK matches the incoming origin against the list and uses the matching entry for the callback `redirect_uri`, the post-login redirect, and logout. This is the recommended approach for production multi-origin setups:

```
NUXT_AUTH0_APP_BASE_URL=https://app1.example.com,https://app2.example.com
```

- **Dynamic** — omit `appBaseUrl` entirely. The base URL is inferred from each request's host, honoring the `x-forwarded-host` / `x-forwarded-proto` headers set by proxies and CDNs, and falling back to the `Host` header.

Regardless of the mode, every origin must be registered in your Auth0 application's **Allowed Callback URLs** and **Allowed Logout URLs** — this remains the primary safeguard.

> [!IMPORTANT]
> In dynamic and allow-list modes, secure session cookies are enforced when `NODE_ENV=production`. Explicitly setting `sessionConfiguration.cookie.secure = false` in that mode throws an `InvalidConfigurationError`.

See the [`example-nuxt-web-dynamic-app-base-url`](../../examples/example-nuxt-web-dynamic-app-base-url) example for a runnable two-host setup.

### Configuring the mountes routes
The SDK for Nuxt Web Applications mounts 4 main routes:

Expand Down
2 changes: 1 addition & 1 deletion packages/auth0-nuxt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The `SESSION_SECRET` is the key used to encrypt the session cookie. You can gene
openssl rand -hex 64
```

The `APP_BASE_URL` is the URL that your application is running on. When developing locally, this is most commonly http://localhost:3000.
The `APP_BASE_URL` is the URL that your application is running on. When developing locally, this is most commonly http://localhost:3000. It may also be a comma-separated allow-list of origins, or omitted to infer the origin per request — see [Dynamic Application Base URL](https://github.com/auth0/auth0-nuxt/blob/main/packages/auth0-nuxt/EXAMPLES.md#dynamic-application-base-url) for serving multiple origins from a single Auth0 application.


> [!IMPORTANT]
Expand Down
1 change: 1 addition & 0 deletions packages/auth0-nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { RouteConfig } from './types';

export * from './types';
export type { SessionConfiguration, SessionCookieOptions, StateData } from '@auth0/auth0-server-js';
export { InvalidConfigurationError } from '@auth0/auth0-server-js';

/**
* Module options for the Auth0 Nuxt module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ describe('callback.get handler', () => {
node: {
req: {
url: 'foo',
headers: { host: 'localhost:3000' },
socket: {},
},
},
} as unknown as H3Event;
Expand Down Expand Up @@ -77,4 +79,19 @@ describe('callback.get handler', () => {

expect(sendRedirectMock).toHaveBeenCalledWith(mockEvent, 'http://localhost:3000');
});

it('resolves the base url dynamically from the request host', async () => {
const dynamicEvent = {
context: { auth0ClientOptions: { appBaseUrl: undefined } },
node: { req: { url: 'foo', headers: { host: 'app2.localhost:3000' }, socket: {} } },
} as unknown as H3Event;
mockAuth0Client.completeInteractiveLogin.mockResolvedValue({ appState: undefined });

await callbackHandler(dynamicEvent);

expect(mockAuth0Client.completeInteractiveLogin).toHaveBeenCalledWith(
new URL('foo', 'http://app2.localhost:3000')
);
expect(sendRedirectMock).toHaveBeenCalledWith(dynamicEvent, 'http://app2.localhost:3000');
});
});
Loading
Loading