Skip to content

Deploying a Separate Auth API

Skip this page unless your sign-in uses an external identity provider (Microsoft Entra, Google, and the like). Most projects don’t, so check “Does this apply to you?” first.

Having a separate auth service does not by itself mean it needs its own public address. What matters is whether the browser talks to the auth API directly, and that comes down to your sign-in flow.

Your sign-inWhat the browser doesAuth API needs its own public front door?
”Sign in with Microsoft / Google”: a full-page jump to an external login page, then backNavigates directly to the auth API, and the identity provider redirects back to the auth APIYes (this page)
A username + password form inside your appPosts to a same-origin path your app rewrites to the auth API server-side; the browser only ever talks to your appNo (deploy the auth API as a private backend, like your other APIs)

Not sure? Look at what clicking “sign in” actually does. A full-page navigation to a different host (an external identity provider) means you need this page. A form submit or background request that stays on your app’s domain means you don’t: the auth API sits behind your app’s reverse proxy like any other backend, reached through a next.config.ts rewrite, and nothing extra is required.

A third, less common setup has the browser call the auth host cross-origin directly (a fetch(..., { credentials: 'include' }) straight to the auth host rather than a same-origin path). That also needs the auth host to be publicly reachable and CORS-configured for your app’s origin. The cleaner design routes it through the same-origin rewrite so it stays private, in which case this page doesn’t apply either.

The rest of this page is for the external-provider case. There, the sign-in is a full-page browser redirect to the auth API (the address in NEXT_PUBLIC_AUTH_API_BASE_URL), and the provider then redirects the browser back to the auth API’s callback. Both are browser navigations.

So unlike your other backends (which the browser never calls directly, so they stay private), the auth API needs its own public hostname, DNS, certificate, and reverse proxy, the same treatment your app gets. It’s the same recipe as your app’s site, repeated for a second hostname.

One timing quirk you don’t configure but should recognise: right after the external-provider redirect chain, the browser treats the first landing on your app as “cross-site” and withholds a SameSite=Strict cookie on that one request, so the very first session check can fail even when everything above is correct. The workflow handles this in the generated proxy.ts with a one-time meta refresh that starts a fresh same-site navigation carrying the cookie. If a deployment strips or bypasses that proxy, the symptom is a login that bounces once and then works on a manual reload.

1. Hostname: keep it on the same domain. The session cookie only works if the app and the auth API share the same registrable domain (e.g. both under example.com). See Cookie domains. Ports don’t affect this; the domain does.

2. DNS: a new hostname needs a new record. Add a DNS record for the auth hostname (e.g. auth.example.com) pointing at the server, as you did for your app. A subdomain nobody added to DNS shows up in the browser as “this site can’t be reached” / DNS_PROBE_FINISHED_NXDOMAIN.

3. Certificate: a wildcard only covers one level. A *.example.com certificate matches exactly one label:

Auth hostnameCovered by *.example.com?
auth.example.comYes
auth.cash.example.comNo, two levels deep

If your auth hostname is two levels deep, either get a certificate that names it (or a *.cash.example.com wildcard), or pick a single-level name the existing wildcard already covers, e.g. auth-cash.example.com instead of auth.cash.example.com.

4. The auth backend must accept the forwarded hostname. The reverse proxy hands each request to the auth backend on a private address. Many backends keep a host allow-list (a list of hostnames they’ll answer for) and reject anything else with a 400 “invalid hostname”. This is not the same as CORS: CORS controls which websites may call the API from a browser; the host allow-list controls which Host values the API accepts. Add the hostname your proxy forwards to that allow-list. (Whether that’s the public name or 127.0.0.1 depends on whether your proxy preserves the original host; check what the backend actually receives.)

Create a second IIS site for the auth hostname using the same steps as Set up IIS as the reverse proxy, pointing its rewrite at the auth backend’s port (e.g. http://127.0.0.1:10010; note http, not https).

With two HTTPS sites sharing port 443, both bindings must have Require Server Name Indication (SNI) ticked, each with its own hostname and certificate. Otherwise only one certificate can serve port 443 and the other hostname is rejected with 400 “invalid hostname”. A binding left with a blank hostname acts as a catch-all and causes the same error.

Turn OFF “Reverse rewrite host in response headers”. In ARR’s Server Proxy Settings this option is on by default, and it rewrites the host in every redirect (Location) the backend returns to match the incoming host. For an external-provider flow that’s fatal: the redirect out to the identity provider (e.g. login.microsoftonline.com) and the redirect back to your app both get rewritten to the auth host, so sign-in silently goes to the wrong place. With preserveHostHeader on (part of the reverse-proxy setup), the backend already builds correct URLs, so reverse-rewrite can only do harm here. Untick it. It’s a server-level setting, so it applies to every proxied site.

The idea is the same, in different syntax:

  • Linux (nginx): add a second server block for the auth hostname that proxy_passes to the auth port.
  • Docker (Caddy): add a second site entry.
Terminal window
# the auth backend itself, on the server (use your project's health path):
curl.exe -v http://127.0.0.1:10010/v1/health
# through the public front door:
curl.exe -v https://auth.example.com/v1/health

Both should return success. Then test the full sign-in: click sign in, authenticate, land back logged in, refresh to confirm the session sticks, and check logout. If your identity provider keeps a list of allowed redirect URLs, make sure your auth hostname’s callback URL is registered there too.