Deploying a Separate Auth API
Skip this page unless your sign-in uses an external identity provider, such as Microsoft Entra or Google. Most projects don’t, so check whether it applies to you first.
Does this apply to you
Section titled “Does this apply to you”Being a separate service doesn’t, on its own, mean the auth API needs a public address. What matters is whether the browser talks to the auth API directly, and that depends on your sign-in flow.
| Your sign-in | What the browser does | Does the auth API need its own public address? |
|---|---|---|
| ”Sign in with Microsoft” or “Sign in with Google”: a full-page jump to an external login page, then back | Goes directly to the auth API, and the identity provider sends it back to the auth API | Yes. Follow this page. |
| A username and password form inside your app | Posts to a path on your app, which forwards it to the auth API on the server. The browser only ever talks to your app. | No. Deploy the auth API as a private backend, like your other APIs. |
If you’re not sure, watch what clicking “sign in” does. A full-page jump to a different host means you need this page. A form submit or background request that stays on your app’s domain means you don’t. In that case, the auth API sits behind your app’s reverse proxy like any other backend, reached through a rewrite in next.config.ts, and nothing extra is needed.
A third, less common set-up has the browser call the auth host directly from your app’s pages, rather than through a path on your app. That also needs the auth host to be public, and configured to accept requests from your app’s origin (CORS). Routing the call through a path on your app is the cleaner design, because the auth API stays private and this page doesn’t apply.
Why the auth API is the exception
Section titled “Why the auth API is the exception”The rest of this page covers sign-in through an external identity provider. Here, signing in sends the browser to the auth API’s public address. The identity provider then sends the browser back to the auth API’s callback. Both are browser navigations.
Your other backends stay private, because the browser never calls them directly. The auth API is different: it needs its own public hostname, DNS record, certificate and reverse proxy, the same as your app. It’s the same set-up as your app’s site, repeated for a second hostname.
One timing quirk is worth recognising. Right after the identity provider sends the browser back, the browser treats the first page load on your app as coming from another site. It withholds a SameSite=Strict cookie on that one request, so the first session check can fail even when everything else is right.
The usual fix is a one-time refresh, done by the app on its first load, which starts a fresh same-site visit carrying the cookie. Without it, the symptom is a sign-in that bounces once and then works when you reload the page.
Four things people miss
Section titled “Four things people miss”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, such as both being under example.com. Ports don’t affect this, but the domain does. See Cookie domains.
2. DNS: a new hostname needs a new record. Add a DNS record for the auth hostname, such as auth.example.com, pointing at the server, as you did for your app. A subdomain with no DNS record shows in the browser as “This site can’t be reached” or DNS_PROBE_FINISHED_NXDOMAIN.
3. Certificate: a wildcard only covers one level. A *.example.com certificate matches exactly one level of subdomain:
| Auth hostname | Covered by *.example.com? |
|---|---|
auth.example.com | Yes |
auth.cash.example.com | No, it’s two levels deep |
For a hostname two levels deep, either get a certificate that names it, such as a *.cash.example.com wildcard, or pick a single-level name your existing wildcard covers. For example, use auth-cash.example.com instead of auth.cash.example.com.
4. The auth backend must accept the forwarded hostname. The reverse proxy passes each request to the auth backend on a private address. Many backends keep a host allow-list, the hostnames they’ll answer for, and reject anything else with a 400 “invalid hostname” error.
This isn’t 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 the allow-list. That may be the public name or 127.0.0.1, depending on whether your proxy keeps the original host, so check what the backend actually receives.
Windows and IIS
Section titled “Windows and IIS”Create a second IIS site for the auth hostname, using the same steps as Set up IIS as the reverse proxy. Point its rewrite at the auth backend’s port, such as http://127.0.0.1:10010. Note that it’s http, not https.
With two HTTPS sites sharing port 443, both bindings must have Require Server Name Indication ticked, each with its own hostname and certificate. Otherwise only one certificate can serve port 443, and the other hostname is rejected with a 400 “invalid hostname” error. A binding left with a blank hostname catches everything and causes the same error.
Turn off “Reverse rewrite host in response headers”. This option in ARR’s Server Proxy Settings is on by default. It rewrites the host in every redirect the backend returns to match the incoming host.
For sign-in through an identity provider, that breaks the flow. The redirect out to the identity provider, such as login.microsoftonline.com, and the redirect back to your app both get rewritten to the auth host, so sign-in goes to the wrong place. With preserveHostHeader on, which is part of the reverse proxy set-up, the backend already builds the correct addresses. Untick the option. It’s a server-level setting, so it applies to every site on the server.
Linux and Docker
Section titled “Linux and Docker”The approach is the same, in different syntax:
- Linux (nginx): add a second
serverblock for the auth hostname that usesproxy_passto reach the auth port. - Docker (Caddy): add a second site entry to the
Caddyfile.
Check it end to end
Section titled “Check it end to end”# 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 address:curl.exe -v https://auth.example.com/v1/healthBoth should succeed. Then test the full sign-in:
- Click sign in and authenticate.
- Check you land back in the app, signed in.
- Refresh the page and check you’re still signed in.
- Sign out and check it works.
If your identity provider keeps a list of allowed redirect addresses, register your auth hostname’s callback address there too.