Horizon

Test Auth.js (NextAuth v5) login behind a tunnel

Run Auth.js sign-in on localhost behind a Horizon tunnel, with AUTH_TRUST_HOST, AUTH_URL and the right callback path.

Sign in with Auth.js on your laptop while the provider redirects to a stable HTTPS URL.

Before you begin

  • Node.js 18 or later
  • A Horizon account and the CLI (see Getting started)
  • A reserved subdomain for -s. Reserve one on the Subdomains page.
  • A Next.js App Router app with Auth.js installed (npm install next-auth@beta)
  • An OAuth app at a provider. This page uses GitHub. For other providers, see Test OAuth redirect URLs locally.

Set up Auth.js

Auth.js builds its callback URL from the request headers. Behind a tunnel, Horizon sets Host to localhost:3000 and sends the public host in X-Forwarded-Host. Set AUTH_TRUST_HOST so Auth.js trusts X-Forwarded-Host. Auth.js documents this for apps behind a reverse proxy.

Generate the secret.

npx auth secret

Add the environment variables.

.env.local
AUTH_SECRET=replace-with-the-generated-secret
AUTH_TRUST_HOST=true
AUTH_GITHUB_ID=replace-with-your-client-id
AUTH_GITHUB_SECRET=replace-with-your-client-secret

Configure Auth.js with a provider.

auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
});

You can set trustHost: true in this config instead of the environment variable. Pick one.

Expose the route handlers. The path decides your callback URL.

app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";

export const { GET, POST } = handlers;

Add a sign-in button.

app/page.tsx
import { signIn } from "@/auth";

export default function Home() {
  return (
    <form
      action={async () => {
        "use server";
        await signIn("github");
      }}
    >
      <button type="submit">Sign in with GitHub</button>
    </form>
  );
}

Start the app on port 3000.

npm run dev

Start a tunnel

Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so the redirect URI you register would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.

hrzn tunnel http://localhost:3000 -s my-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Keep this terminal open.

Register the callback URL

Auth.js serves the callback at /api/auth/callback/<provider id> in Next.js. For GitHub the redirect URI is:

https://my-app.hrzn.run/api/auth/callback/github

Register it in the provider. The GitHub guide shows where. For GitLab and Twitch, see GitLab and Twitch.

Sign in

Open https://my-app.hrzn.run in your browser. On the first visit, select Continue to site on the Before you continue page. Then select Sign in with GitHub.

Check it works

The provider shows its consent screen. After you approve, your browser lands back on https://my-app.hrzn.run.

Your Horizon terminal prints a line for the callback. The status is a 3xx, because Auth.js redirects after it sets the session.

Output
  GET     302  /api/auth/callback/github

Setting AUTH_URL instead

Auth.js v5 infers the host from the request headers and says AUTH_URL is mostly unnecessary. If you prefer to pin the public URL, set it in .env.local and restart the dev server.

.env.local
AUTH_URL=https://my-app.hrzn.run

Auth.js documents AUTH_URL for apps with a different base path. For a tunnel, AUTH_TRUST_HOST=true is the documented fix. Use it first.

Troubleshooting

Auth.js throws an UntrustedHost error

Auth.js requires trustHost to be true because it relies on the request's host header. Set AUTH_TRUST_HOST=true in .env.local, or trustHost: true in the NextAuth config. Restart npm run dev.

The provider says the redirect URI doesn't match

  • Compare the URI registered at the provider with https://my-app.hrzn.run/api/auth/callback/<provider id>. The scheme, host and path must match.
  • Check that the subdomain in the provider matches the one you pass to -s.
  • Check that AUTH_TRUST_HOST=true is set and that you restarted the dev server. Without it, the callback can resolve to localhost.

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the registered URI stays valid. -s needs a subdomain you reserved, see Pricing.

The browser shows a Before you continue page

Horizon shows this page once every 7 days per IP address. Select Continue to site.

Nothing reaches your app

  • Check that the Horizon terminal is still running. If its last line is Connection lost. Reconnecting…, wait for Reconnected.
  • Check that the app listens on port 3000, the port in your tunnel command.

Next steps

On this page