Horizon

Test Login with Amazon locally

Test Login with Amazon on localhost by registering a stable HTTPS Allowed Return URL from a Horizon tunnel.

Test "Login with Amazon" on your laptop, with HTTPS URLs Amazon accepts.

Auth.js doesn't list an Amazon provider, so this page uses Amazon's authorization code flow directly in a Next.js App Router app.

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.
  • An Amazon developer account with access to the Login with Amazon console
  • A Next.js App Router app

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 URLs in Amazon

Create a security profile, then add your tunnel as a website.

  1. In the Login with Amazon developer console, select Create a New Security Profile.
  2. Enter a Name and Description.
  3. Enter a Consent Privacy Notice URL, then select Save.
  4. Open Web Settings for the security profile and select Edit.
  5. Set Allowed Return URLs to https://my-app.hrzn.run/api/amazon/callback. This is the redirect for the flow on this page.
  6. If you also use the JavaScript SDK popup flow, set Allowed Origins to https://my-app.hrzn.run.
  7. Select Save, then copy the Client ID and Client Secret.

Amazon requires Allowed Origins to use HTTPS, and it requires an HTTPS redirect URI. A tunnel URL is already HTTPS.

Add the sign-in routes

Store the credentials.

.env.local
AMAZON_CLIENT_ID=replace-with-your-client-id
AMAZON_CLIENT_SECRET=replace-with-your-client-secret
APP_URL=https://my-app.hrzn.run

The sign-in route sends the browser to Amazon. It builds the redirect_uri from APP_URL, not from the request. Horizon sets the Host header to localhost:3000, so a URI built from the request would point at localhost.

app/api/amazon/login/route.ts
import { randomUUID } from "node:crypto";
import { NextResponse } from "next/server";

export async function GET() {
  const state = randomUUID();
  const authorizeUrl = new URL("https://www.amazon.com/ap/oa");
  authorizeUrl.searchParams.set("client_id", process.env.AMAZON_CLIENT_ID!);
  authorizeUrl.searchParams.set("scope", "profile");
  authorizeUrl.searchParams.set("response_type", "code");
  authorizeUrl.searchParams.set(
    "redirect_uri",
    `${process.env.APP_URL}/api/amazon/callback`,
  );
  authorizeUrl.searchParams.set("state", state);

  const response = NextResponse.redirect(authorizeUrl);
  response.cookies.set("amazon_state", state, { httpOnly: true, secure: true });
  return response;
}

The callback route checks state, trades the code for an access token, and reads the profile. Amazon requires the same redirect_uri in the token request.

app/api/amazon/callback/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const code = request.nextUrl.searchParams.get("code");
  const state = request.nextUrl.searchParams.get("state");
  const savedState = request.cookies.get("amazon_state")?.value;

  if (!code || !state || state !== savedState) {
    return new NextResponse("Invalid sign-in response", { status: 400 });
  }

  const tokenResponse = await fetch("https://api.amazon.com/auth/o2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: `${process.env.APP_URL}/api/amazon/callback`,
      client_id: process.env.AMAZON_CLIENT_ID!,
      client_secret: process.env.AMAZON_CLIENT_SECRET!,
    }),
  });
  const { access_token } = await tokenResponse.json();

  const profileResponse = await fetch("https://api.amazon.com/user/profile", {
    headers: { Authorization: `Bearer ${access_token}` },
  });
  const profile = await profileResponse.json();

  return NextResponse.json(profile);
}

The token endpoint above is the North America one. Amazon lists https://api.amazon.co.uk/auth/o2/token for the European Union and https://api.amazon.co.jp/auth/o2/token for the Far East.

Start the app on port 3000.

npm run dev

Sign in

Open https://my-app.hrzn.run/api/amazon/login in your browser. On the first visit, select Continue to site on the Before you continue page. Amazon then shows its sign-in and consent screens.

Check it works

After you approve, your browser lands on https://my-app.hrzn.run/api/amazon/callback and shows your profile as JSON. Amazon documents user_id, name and email in the profile response.

Your Horizon terminal prints a line for the callback:

Output
  GET     200  /api/amazon/callback

Troubleshooting

Amazon refuses the redirect URI

  • Check that Allowed Return URLs holds https://my-app.hrzn.run/api/amazon/callback, character for character.
  • Check that APP_URL is the public HTTPS URL and that you restarted npm run dev after editing .env.local.
  • Check that the token request sends the same redirect_uri as the authorization request.

The callback returns Invalid sign-in response

The state value didn't match the cookie, or Amazon sent no code. Start again from /api/amazon/login in the same browser. If the user denied access, Amazon sends an access_denied error instead of a code.

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 URLs stay valid. -s needs a subdomain you reserved, see Pricing.

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