Horizon

Test Sign in with Apple locally

Test Sign in with Apple on localhost with a public HTTPS Return URL from a Horizon tunnel and Auth.js, since Apple rejects localhost.

Test Sign in with Apple on your laptop. Apple needs a public HTTPS domain and Return URL, and a Horizon tunnel provides both.

Auth.js's Apple docs state it plainly: "Apple does not support localhost/http URLs. You can only use a live URL with HTTPS." A reserved Horizon subdomain is a live HTTPS URL that stays the same across restarts.

Before you begin

  • 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 Apple Developer account with access to Certificates, Identifiers & Profiles
  • An App ID to use as the primary App ID for your website

Start a tunnel

Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so the domain and Return URL you register with Apple 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

Your public URL is https://my-app.hrzn.run. Keep this terminal open.

Create the Services ID in Apple

  1. In Certificates, Identifiers & Profiles, select Identifiers in the sidebar, then select the add button (+).
  2. Select Services ID, then select Continue.
  3. Enter a Description and a unique Identifier. Register the Services ID. You use the identifier as the client ID.
  4. Select the Services ID from the list. Select Sign in with Apple, then select Configure.
  5. In the modal, select your App ID from the list of primary App IDs.
  6. In the Website URLs section, set Domains and Subdomains to my-app.hrzn.run. Leave out https://.
  7. Set Return URLs to https://my-app.hrzn.run/api/auth/callback/apple.
  8. Select Done, then Continue, then review and select Save.

Apple says you don't need to upload a file to your server to register domains and subdomains.

Configure Auth.js

Auth.js builds the redirect URI from the request. Horizon sets Host to localhost:3000 and sends the public host in X-Forwarded-Host. AUTH_TRUST_HOST=true tells Auth.js to trust X-Forwarded-Host. AUTH_URL pins the public URL. Auth.js says AUTH_URL is mostly unnecessary in v5, so treat it as a second guard.

Apple requires the client secret to be a JWT (a signed token). Auth.js provides a command that asks for your Apple details, generates the secret, and adds AUTH_APPLE_ID and AUTH_APPLE_SECRET to your .env file.

npx auth add apple

Generate the Auth.js secret.

npx auth secret

Add the remaining environment variables to the same file.

.env.local
AUTH_SECRET=replace-with-the-generated-secret
AUTH_URL=https://my-app.hrzn.run
AUTH_TRUST_HOST=true
AUTH_APPLE_ID=replace-with-your-services-id-identifier
AUTH_APPLE_SECRET=replace-with-the-generated-jwt

Configure the provider.

auth.ts
import NextAuth from "next-auth";
import Apple from "next-auth/providers/apple";

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

Expose the route handlers. This path makes the callback /api/auth/callback/apple.

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("apple");
      }}
    >
      <button type="submit">Sign in with Apple</button>
    </form>
  );
}

Start the app on port 3000.

npm run dev

Sign in through the public URL

Open https://my-app.hrzn.run in your browser. On the first visit, select Continue to site on the Before you continue page. Horizon shows this page once every 7 days per IP address. Then select Sign in with Apple.

Don't start from http://localhost:3000. Apple can't send the user back there.

Check it works

Apple shows its sign-in page. After you approve, your browser lands back on https://my-app.hrzn.run.

Your Horizon terminal prints a line ending in the callback path.

Output
  /api/auth/callback/apple

Apple returns the user's email and name only the first time the user consents to the app, according to Auth.js. A second sign-in test returns no name.

Troubleshooting

Apple rejects localhost or an http URL

Apple doesn't accept localhost or http in Domains and Subdomains or Return URLs, and a 127.0.0.1 address fails later in the flow. Use my-app.hrzn.run and the https:// Return URL from step 2.

Apple returns an error after you approve

  • Compare Return URLs with https://my-app.hrzn.run/api/auth/callback/apple. Check the scheme, host and path.
  • Check that Domains and Subdomains holds my-app.hrzn.run.
  • Check that AUTH_APPLE_ID is the Services ID identifier, not the App ID.
  • Check that you restarted npm run dev after editing .env.local.

The redirect URI in the request contains localhost

Your app built the redirect URI from the Host header, which Horizon sets to localhost:3000. Set AUTH_TRUST_HOST=true and AUTH_URL=https://my-app.hrzn.run in .env.local. Restart npm run dev.

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 and restart npm run dev.

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Apple still holds the old domain and Return URL. Restart with -s my-app. -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