# How do I handle authentication on customers’ custom domains in my SaaS?

> Let each custom domain run its own sign-in: it sends people to your identity provider and gets them back on its own callback, where your app sets that domain’s session cookie. The provider must accept every custom domain as a redirect target, so register a domain once your records mark it verified and active, and remove it when the customer leaves. loginos does this with one API call.

Your customers want your product on their own address: `app.customer.com` instead of `customer.yoursaas.com`. They point the name at you in DNS, you serve a TLS certificate for it, and the pages load. Then sign-in breaks. Your session cookie doesn’t reach the new domain, and your identity provider refuses to send anyone back to an address it doesn’t know.

The fix has two parts. Each custom domain runs its own sign-in and keeps its own session, and your identity provider’s list of allowed domains follows your own records, in code.

## What changes when a customer brings their own domain?

Three things change at once: DNS, TLS and cookies.

- **DNS.** The customer adds a CNAME record, a DNS entry that makes one name an alias of another: `app.customer.com CNAME customers.yoursaas.com`. Requests then reach you with `Host: app.customer.com`, and your app looks up which customer that is.
- **TLS.** Every custom domain needs a certificate. Caddy’s on-demand TLS and Cloudflare for SaaS can issue them for domains you don’t know in advance.
- **Cookies.** Browsers send a cookie only to the domain that set it, and to its subdomains when the cookie has a `Domain` attribute. The session cookie of `yoursaas.com` never reaches `app.customer.com`, so each custom domain needs its own session.

The cookie rule settles most of the design. If the session must live on `app.customer.com`, the sign-in has to end there too.

> [!TIP]
> Name the session cookie with the [`__Host-` prefix](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#cookie_prefixes). It requires `Secure`, `Path=/` and no `Domain`, so the cookie only goes to the exact host that set it, even when some customers use subdomains of your own domain.

## Should people sign in on the custom domain or on a central login domain?

Both designs use an identity provider. They differ in where the provider sends people back.

| | Sign-in on each custom domain | One central login domain |
| --- | --- | --- |
| Callback URLs at the provider | One per custom domain | One, like `login.yoursaas.com/callback` |
| Session on the custom domain | Set by its own callback | Needs a hand-off from the login domain |
| Code you write | Register and remove domains | One-time hand-off tokens and target checks |
| Main risk | A stale domain left on the list | The hand-off becomes an open redirect |

**Sign-in on each custom domain** is the standard OpenID Connect flow, repeated per domain. `app.customer.com/login` redirects to the provider, the provider sends the browser back to `app.customer.com/callback` with a one-time code, and your server exchanges the code and sets the session cookie there. Someone already signed in to the provider comes straight back, without a form.

**A central login domain** keeps one callback URL. After the sign-in, it creates a short-lived token and redirects to the custom domain, which trades the token for a session. You are then building part of an identity provider yourself: tokens that work once, expire in seconds and are bound to one domain. The target must come from your records too, or the login domain becomes an open redirect, a URL that forwards people to any address an attacker puts in it.

Most teams should sign in on each custom domain. The extra work is keeping the provider’s list current, which takes little code.

## Why must every custom domain be an allowed redirect target?

The redirect URI, or callback URL, is the address where the identity provider sends the browser back with the code. Providers only use addresses you registered, and they compare them exactly: OpenID Connect Core 1.0 and the OAuth security best practice, RFC 9700, both require it.

The rule protects you. Your client ID is public, so anyone can start a sign-in for your app. If the provider accepted any return address, an attacker could send your users through a real sign-in and collect their codes on a domain of their own.

So each custom domain needs its callback URL on the list, and the callback has to run on that domain, the only place that can set its session cookie. A wildcard doesn’t help: `https://*.yoursaas.com/callback` covers your own subdomains at best, never `app.customer.com`. [Can I use wildcard redirect URIs with OAuth?](https://loginos.io/answers/wildcard-redirect-uri-oauth.md) explains the risks.

## How do you keep the provider’s list in step with your customers?

Tie the provider’s list to the lifecycle of a custom domain in your own database:

1. The customer enters `app.customer.com` in your dashboard. You store it as **pending**.
2. You check that they control it, for example that the CNAME points at you or that a TXT record holds a token you gave them.
3. Your TLS layer gets a certificate, and you mark the domain **active**.
4. In the same step, your server registers the domain with the identity provider through its management API.
5. When the customer removes the domain or cancels, or the CNAME stops pointing at you, you mark it inactive and remove it from the provider.

Run steps 4 and 5 from a job queue that retries, and make the “add” call safe to repeat. [How do I add a redirect URI automatically when a customer adds a domain?](https://loginos.io/answers/add-redirect-uri-automatically.md) has the code.

The hands-off alternative is a provider that asks your server about an unknown domain the first time someone signs in there. Your server answers from its records, the way Caddy’s on-demand TLS asks your `ask` endpoint before it requests a certificate.

## Which custom domains should be allowed?

Only domains your own records say are verified and active right now, however they reach the list.

- **Not what someone typed.** Register a domain after the ownership check, not when the form is submitted.
- **Not what DNS says.** A CNAME that points at you today can point elsewhere tomorrow. DNS shows where a name goes, not whose it is.
- **Not the request.** The `Host` header and a provider’s question come from outside. Look the domain up in your records.
- **Not forever.** If an expired domain stays on the list and someone else registers it, they control a valid return address for your sign-ins. Remove domains as soon as they stop being active.

Keep the usual defenses as well. Exchange codes on your server with your client secret, and use PKCE (Proof Key for Code Exchange). PKCE ties each code to a secret known only to whoever started the sign-in, so a code that reaches the wrong place is worth little on its own.

## How loginos does it

loginos is an OpenID Connect provider for products on many domains. Your product is a **platform**, with no limit on domains, and all of them share one user pool. The callback path is the same on every domain, `/api/auth/login/single/callback` by default, and loginos only sends people back to `https://<one of your domains><callback path>`. So for each customer you register a host, not a URL.

When your records mark a domain active, register it from your server. Hosts that are already there are fine, so a retry is harmless:

```bash title="curl"
curl -X POST https://loginos.io/api/v1/domains \
  -u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET" \
  -H "content-type: application/json" \
  -d '{"host": "app.customer.com"}'

# When the customer disconnects the domain or leaves:
curl -X DELETE https://loginos.io/api/v1/domains/app.customer.com \
  -u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET"
```

`{"hosts": [...]}` adds up to 1,000 at once, and sign-ins from a deleted domain stop at once. See [Domains: from your server](https://loginos.io/docs/domains.md#from-your-server).

For the hands-off option, turn on [automatic domains](https://loginos.io/docs/domains.md#automatic-domains). The first time a sign-in comes from a host that isn’t on your list, loginos posts `{"host", "client_id"}` to an endpoint on your server. Only a `200` with `{"host": <the same host>, "allow": true}` within 3 seconds adds the host. Anything else stops the sign-in.

```js title="Express"
app.post("/loginos/domain-check", express.json(), async (req, res) => {
  const host = String(req.body.host ?? "");
  // Your own records: a customer connected it, you verified it, and it is active.
  const domain = await CustomDomain.findOne({ host, status: "active" });
  res.json({ host, allow: Boolean(domain) });
});
```

Before the feature turns on, loginos asks about a `.invalid` host, and your endpoint has to say no. An automatic domain is checked again when it is used a day after the last yes, and a clear no removes it.

Know the limits before you choose loginos:

- **Sign-in happens on loginos.io**, under your platform’s name and logo, never on a customer’s domain.
- **One account per person.** Someone in two of your customers’ workspaces has one loginos account, and your app decides which workspaces they may open. See [multi-tenant login with a domain per tenant](https://loginos.io/answers/multi-tenant-login-custom-domains.md).
- **No SAML** or enterprise connections, so customers can’t bring their own identity provider.

The [quickstart](https://loginos.io/docs/quickstart.md) has the redirect route and the callback for Next.js, Express and PHP.

## Frequently asked questions

### Can one session cookie work on all my customers’ domains?

No. Browsers send a cookie only to the domain that set it, and `app.customer.com` and `yoursaas.com` are unrelated domains. Each needs its own session. People still sign in only once: the second domain sends them to the identity provider, which recognizes them and sends them straight back.

### Do I need a separate OAuth client for each customer domain?

Usually not. One client can hold many redirect URIs, which keeps one set of credentials and one user pool. Some providers cap the number of redirect URIs per client, which pushes larger products toward several clients, so check the cap early.

### What should happen when a customer’s domain expires?

Remove it from the provider’s list as soon as your records stop treating it as active. A lapsed domain can be registered by someone else, and any address still on the list can receive sign-in codes. Re-checking custom domains on a schedule catches the ones customers forget to remove.

### Can my customers have the login page on their own domain?

Only with a provider that offers custom login domains, such as Auth0 with its Multiple Custom Domains feature. [The alternatives to Auth0’s multiple custom domains](https://loginos.io/answers/auth0-multiple-custom-domains-alternatives.md) compares the options. loginos doesn’t: sign-in always happens on loginos.io.

---

Updated 2026-10-14. Web page: https://loginos.io/answers/saas-custom-domain-authentication
