The identifier-first sign-in flow
Type an email address, get sent to the right identity provider. Two endpoints do this, they behave very differently, and one of them should not be used.
Identifier-first sign-in is the pattern where a sign-in page asks for an email address before it asks for anything else, decides from the domain which identity provider that person belongs to, and sends them there — so somebody at a federated company never sees a password box at all, and somebody at a company without federation does.
It is a small piece of plumbing with an outsized security surface, because the endpoint answering the question is by definition unauthenticated: it has to work for somebody who has not signed in yet.
Two endpoints answer this question
| `GET /api/v1/federation/discover` | `POST /api/v1/auth/discovery` | |
|---|---|---|
| Matches on | A verified federated domain, proven by a DNS record you published. | A comma-separated free-text list on any provider record. No ownership check of any kind. |
| Search scope | Domains claimed and verified by one organisation. | Every organisation on the platform, first match wins, in unspecified order. |
| Rate limit | Its own bucket, 30 requests a minute per address. | Shares a 10-per-minute bucket with sign-in, refresh, invitation acceptance and MFA verification. |
| Discloses | The provider your own domain routes to. | Another tenant's organisation identifier, slug, provider name and provider type. |
How the working one behaves
GET /api/v1/federation/[email protected]
--- federated ---
{
"federated": true,
"provider_slug": "entra",
"display_name": "Contoso Entra ID",
"provider_type": "entra_id",
"protocol": "oidc",
"organization_id": "…",
"domain_verified": true,
"domain_verified_at": "2026-08-14T09:02:11Z",
"start_url": "https://…/api/v1/federation/{orgID}/entra/start"
}
--- everything else ---
{ "federated": false }Send the person to start_url when federated is true, and show a password box when it is false. That is the whole integration.
Note that the response reports the verification state of the domain it matched, so a client that wants to be defensive can check domain_verified as well as federated. Both will be true together; the field is there so that a future state cannot be misread.
What neither endpoint reveals
Neither one tells you whether an account exists. Both answer on the domain alone. Asking about [email protected] returns exactly what asking about a real colleague returns, because the question being answered is where does this domain sign in, not is this person here.
That is the property that makes identifier-first safe to expose at all, and it is worth verifying rather than assuming if you build your own sign-in page: a page that shows no such user after the discovery step has reintroduced the enumeration the endpoint carefully avoids.
The sign-in path behind it holds the same line. A password sign-in against an unknown address is refused with the same message and the same timing as one against a real account with the wrong password, and the failure is recorded as an unattributed audit row rather than attributed to a tenant that would have to be guessed at. Reading the audit log covers what that means for your visibility.
The rate limit, and what it really is
Thirty requests a minute per source address for the federation endpoint. Two honest caveats about that number:
- It is held in one process's memory, so a deployment running several API replicas multiplies the effective ceiling by the replica count. Read it as a rate you can rely on being allowed, not as a ceiling that will contain abuse.
- It is keyed on the connection's own address, never on a forwarded-for header. That is correct — a header-keyed bucket is a bucket an attacker chooses the key for, which is no bucket at all — and it means the limit applies per real client rather than per claimed one.
Setting it up
Register the identity provider
Identity → Federated providers. See Single sign-on with OIDC for the wizard and the redirect URI you have to paste into your provider.
Claim the email domain and prove it
Identity → Domain routing, then publish the TXT record and check it. Until
Routes loginsreadsYes, discovery answersfederated: falsefor everybody in that domain. Federated domains and domain verification is the detail.Add subdomains separately if your people use them
A claim on
yourfirm.comdoes not covermail.yourfirm.com. Each is its own claim with its own record.Test with a real address, and with a made-up one
The real address should return
federated: trueand a start URL that works. The made-up one at the same domain should return exactly the same thing — if it does not, something in front of our endpoint is answering differently for accounts that exist, and that is an enumeration hole you have added.
One thing to arrange before you switch it on
If it has already gone wrong, A locked-out tenant covers what can and cannot be recovered from the outside.