How do I add a redirect URI automatically when a customer adds a domain?
Short answer
Call your identity provider’s management API from the code that marks a customer’s domain as verified and active, and add that domain’s callback URL there. Run the call from a job queue so it retries, make it safe to repeat, and remove the URL when the customer disconnects the domain. With loginos you register the host with one POST, or let loginos ask your server.
A customer connects app.customer.com in your dashboard. Your app checks their DNS, gets a certificate, and the site loads. Then sign-in fails with a redirect URI error, because your identity provider only sends people back to callback URLs it knows. Someone on your team adds the URL by hand, and next week it happens again.
The fix is to make the callback URL part of the domain’s lifecycle in code. The step that makes a domain live registers it with your identity provider, and the step that retires it removes it.
When should your app add the redirect URI?
Add the redirect URI when the customer’s domain becomes active, and not before. A custom domain usually moves through a few states in your database:
| State | What has happened | Redirect URI |
|---|---|---|
| Pending | The customer typed the domain | Not registered |
| Verified | DNS shows the customer controls it | Not yet |
| Active | Your certificate is live and you serve the site | Add it |
| Removed | The customer disconnected it or left, or DNS stopped pointing at you | Remove it |
Registering a pending domain would let anyone put any name on your allow list by typing it into a form. Registering only active domains means every URL on the list belongs to a customer you checked.
Put both calls next to the code that changes the state, or in handlers for events like domain.activated and domain.removed. Then no path through your app can activate a domain without registering it.
How do you make the API call reliable?
A call to your identity provider’s API can fail like any network request, and a missed call leaves a customer whose users can’t sign in. Four habits prevent that.
Make it idempotent. An idempotent operation has the same result however often you repeat it. Adding a URL that is already on the list should change nothing, and removing one that is already gone should count as done. If your provider’s API isn’t built that way, read the current list first and skip the call when there is nothing to do.
Retry from a queue. Save the state change and a “sync this domain” job in the same database transaction, and let a worker make the call. When the call fails, the job runs again later with backoff, and waits at least the Retry-After time when a 429 answer has one. Network errors, rate limits and 5xx errors usually pass on their own. A 400 or 401 won’t, so after a few attempts the job should alert a person.
Serialize changes to one list. Some APIs take the whole list of redirect URIs in one field. Two workers that each read the list, add a URL and write it back can erase each other’s change. Run those updates one at a time, for example from a queue with a single worker.
Reconcile on a schedule. Once a day, compare your active domains with the provider’s list, then add what is missing and remove what is left over. That catches anything a bug or an outage let through.
How do other identity providers handle new redirect URIs?
Auth0, Microsoft Entra ID and Amazon Cognito all keep redirect URIs as a list on the application, and your server can change it through a management API. They differ in how long that list may be.
| Provider | Where the list lives | How you change it | Limit |
|---|---|---|---|
| Auth0 | The application’s callbacks | PATCH /api/v2/clients/{id} in the Management API | 100 callback URLs per application |
| Microsoft Entra ID | The application’s web.redirectUris | PATCH /applications/{id} in Microsoft Graph | 256 redirect URIs, or 100 if the app also signs in personal Microsoft accounts |
| Amazon Cognito | The app client’s CallbackURLs | UpdateUserPoolClient | 100 per app client |
Auth0 says its limit generally can’t be raised, and Microsoft says its maximum can’t be raised for security reasons. Cognito’s update call resets every setting you leave out to its default, so read the current settings with DescribeUserPoolClient and send them back with your change.
Caps like these mean that a product with a few hundred custom domains has to spread customers over several applications, or put a central login domain in front of them. Authentication on customers’ custom domains compares those designs, and wildcard redirect URIs explains why a pattern is no substitute.
How loginos does it
loginos registers hosts, not URLs. The callback path is the same on every domain, so app.customer.com is all it needs, and a platform has no limit on domains.
Register domains from your server
POST /api/v1/domains adds one host, {"host": …}, or up to 1,000, {"hosts": [...]}. Hosts that are already there are fine, so your queue can repeat the call safely. DELETE /api/v1/domains/{host} removes one, and sign-ins from that domain stop at once. The Management API allows 600 requests a minute per platform and answers 429 with Retry-After beyond that.
const LOGINOS = "https://loginos.io";
const auth = "Basic " + Buffer.from(`${process.env.LOGINOS_APP_KEY}:${process.env.LOGINOS_APP_SECRET}`).toString("base64");
// Your job queue runs these and retries with backoff when they throw.
export async function addDomain(host) {
const res = await fetch(`${LOGINOS}/api/v1/domains`, {
method: "POST",
headers: { authorization: auth, "content-type": "application/json" },
body: JSON.stringify({ host }),
});
if (!res.ok) throw new Error(`loginos answered ${res.status}`); // a host already there is fine
}
export async function removeDomain(host) {
const res = await fetch(`${LOGINOS}/api/v1/domains/${encodeURIComponent(host)}`, {
method: "DELETE",
headers: { authorization: auth },
});
if (!res.ok && res.status !== 404) throw new Error(`loginos answered ${res.status}`); // 404: already gone
}To backfill the domains you already have, send them as {"hosts": [...]} in batches of 1,000. The answer lists what was added, what was existing and what was invalid, so you can fix the invalid ones. See Domains: from your server and the Management API.
Or let loginos ask your server
Automatic domains skip the API calls. The first time a sign-in comes from a host that isn’t one of your domains, loginos asks an endpoint on your server, and adds the host only if your server says yes.
Your domains
- lumen.app
- *.lumen.app
loginos posts {"host": "app.customer.com", "client_id": "<your app key>"}. Answer from your own records, within 3 seconds:
// app/api/loginos/domain-check/route.ts
import { db } from "@/lib/db";
export async function POST(req: Request) {
const { host } = await req.json();
// Verified and active in your own records. Nothing else counts.
const domain = await db.customDomain.findUnique({ where: { host } });
return Response.json({ host, allow: domain?.status === "active" });
}Only a 200 with {"host": <the same host>, "allow": true} adds the host, marked as automatic. Anything else stops the sign-in: "allow": false, another host, another status, or no answer in time.
The endpoint must never:
- Say yes to everything. Your app key is public, so a look-alike site can send people to loginos with it. Before the feature turns on, loginos asks about a
.invalidhost, and an endpoint that says yes stays off. - Decide from the domain’s name or DNS. Anyone can register a name that looks like yours and point it at your servers today and elsewhere tomorrow. The request’s
Hostheader proves nothing either.
Removal takes care of itself. loginos asks again when an automatic domain is used a day after the last yes, and a clear no removes it. If you only delete an automatic domain, the next sign-in there adds it again, so answer no for domains you retire. loginos sends at most 120 checks a minute per platform and never adds wildcards, IP addresses or localhost. See automatic domains.
Frequently asked questions
What if my provider limits how many redirect URIs I can add?
You have three options. Spread customers over several applications. Move them to subdomains of one parent domain, if your provider offers a placeholder or wildcard for those. Or put a central login domain in front and hand sessions on to each custom domain. Each adds work, so check the limit before you reach it.
Do www and the bare domain need separate redirect URIs?
Yes. www.customer.com and customer.com are different hosts, and exact matching treats their callback URLs as different. Register both if the customer serves both, or redirect one to the other before sign-in starts.
Can the browser add the redirect URI?
No. Management APIs need your client secret or an admin token, which must stay on your server. Make the call from your backend or a worker, triggered by the domain’s state change.
How do I test the lifecycle before customers use it?
Use a separate test client with its own credentials, and run the whole lifecycle on a test domain: connect, verify, sign in, disconnect, then check that sign-in stops. In loginos, you can register localhost:3001 as a domain while you develop.
One account for every domain you run
loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.
Keep reading
- Customer domainsHow 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.7 min read
- Customer domainsCan I use wildcard redirect URIs with OAuth?Usually not, and you shouldn’t rely on them. RFC 9700, the OAuth 2.0 Security Best Current Practice, requires authorization servers to match redirect URIs exactly, because loose patterns have let attackers steal authorization codes. Google rejects wildcards, while Auth0 and Okta allow narrow ones with warnings. Register exact URIs through an API instead, or use a provider that verifies wildcard ownership or asks your server.7 min read
- Customer domainsHow do I add login to a multi-tenant app where each tenant has its own domain?Resolve the tenant from the request’s Host header, send people to one identity provider to sign in, and check after the callback that the person is a member of that tenant. Keep one user pool for everyone, and store memberships and per-tenant roles in your own database, keyed by the provider’s stable user id. With loginos, every tenant domain goes under one platform.7 min read
- Customer domainsAuth0 multiple custom domains is too expensive, what are the alternatives?The main alternatives are one shared login domain for all your brands, one Auth0 tenant per brand, or a self-hosted server such as Keycloak, Zitadel or Authentik, where every domain is yours. Auth0 offers Multiple Custom Domains only on Enterprise. If your real need is many app domains sharing one user pool, loginos fits, but its sign-in page always stays on loginos.io.5 min read