loginos

How do I add a redirect URI automatically when a customer adds a domain?

loginos teamUpdated 6 min read

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:

StateWhat has happenedRedirect URI
PendingThe customer typed the domainNot registered
VerifiedDNS shows the customer controls itNot yet
ActiveYour certificate is live and you serve the siteAdd it
RemovedThe customer disconnected it or left, or DNS stopped pointing at youRemove 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.

ProviderWhere the list livesHow you change itLimit
Auth0The application’s callbacksPATCH /api/v2/clients/{id} in the Management API100 callback URLs per application
Microsoft Entra IDThe application’s web.redirectUrisPATCH /applications/{id} in Microsoft Graph256 redirect URIs, or 100 if the app also signs in personal Microsoft accounts
Amazon CognitoThe app client’s CallbackURLsUpdateUserPoolClient100 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.

shop.newbrand.exampleSign in
Your server

Your domains

  • lumen.app
  • *.lumen.app
1/6Someone signs in on shop.newbrand.example, a domain that isn’t on your list yet.

loginos posts {"host": "app.customer.com", "client_id": "<your app key>"}. Answer from your own records, within 3 seconds:

Next.js
// 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 .invalid host, 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 Host header 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

All answers