External Authentication
Signing in with Microsoft (Entra ID) via home-realm discovery
Orion can hand authentication to Microsoft Entra ID. A user types their email on the login page; if the domain matches a configured provider, they are offered that provider and sign in through Microsoft instead of with an Orion password.
There are three things to configure, and they are separate on purpose:
- Providers — the directories Orion will trust. Defined in environment variables, in the database through the admin UI, or both.
- Discovery — how the login page decides that a given email address should use a given provider.
- Policy — what external sign-in is permitted to do: create accounts, link to existing ones, and whether providers may be configured in the database at all. Set server-wide by a platform admin, and tightened per organisation.
Providers in environment variables
The first way to define a provider is an environment variable group, using the pattern
ORION_EXTERNAL_PROVIDER__<ID>__<PROPERTY>. <ID> is
any name you choose; it groups the properties and identifies the provider.
ORION_EXTERNAL_PROVIDER__AAD__TYPE=aad ORION_EXTERNAL_PROVIDER__AAD__DISPLAY_NAME=Microsoft ORION_EXTERNAL_PROVIDER__AAD__CLIENT_ID=<app registration client id> ORION_EXTERNAL_PROVIDER__AAD__TENANT_ID=<directory (tenant) id> ORION_EXTERNAL_PROVIDER__AAD__SCOPE=api://<client id>/Orion.Federation.Login ORION_EXTERNAL_PROVIDER__AAD__AUDIENCE=api://<client id> # optional ORION_EXTERNAL_PROVIDER__AAD__DOMAINS=acme.com,*.acme.co.uk
| Property | Meaning |
|---|---|
TYPE | aad — the only type implemented. Anything else is listed but never initialises an MSAL client. |
DISPLAY_NAME | Button label on the discovery page. |
CLIENT_ID | Application (client) ID of the Entra app registration. |
TENANT_ID | Directory (tenant) ID. Becomes the MSAL authority https://login.microsoftonline.com/<tenant>. |
SCOPE | The custom API scope you expose, in its full request form, e.g. api://<client id>/Orion.Federation.Login. The browser requests this form; Orion validates the token's scp claim against the bare last segment (Orion.Federation.Login), which is what Entra actually puts in the token. You only ever set the full form. |
AUDIENCE | Optional. Expected aud of the token. Defaults to api://<CLIENT_ID> — the identifier "Expose an API" gives you — so set it only if you changed the Application ID URI. |
DOMAINS | Email domains routed to this provider. Comma or space separated. *.example.com matches subdomains only — it does not match example.com itself, so list the apex domain too if you need it. |
Multiple providers can be configured by using different IDs
(…__AAD__…, …__CONTOSO__…). Configuration is read once at
startup, so changes require a restart.
An optional DISCOVERY_MODE (domain, identity or
both) overrides the policy default for that one provider. Leave it unset and
the provider follows whatever the policy says.
Providers in the database
The second way is the admin UI, which stores providers in the database — no restart, and
no access to the deployment's configuration needed. An organisation admin manages their
own at /external-auth/org/<org>; a platform admin manages providers
that apply to every organisation at /external-auth/platform/settings.
The two sources are resolved together and behave identically at sign-in. They differ in who controls them, and that difference is the point:
| Environment | Database | |
|---|---|---|
| Controlled by | whoever deploys the server | whoever holds an admin session |
| Changed by | redeploy / restart | the UI, immediately |
| Scope | every organisation | one organisation, or every one |
| Can an org admin remove it? | no | yes, if it is theirs |
ORION_EXTERNAL_PROVIDERS_DB=false makes the
server ignore database providers entirely — only environment providers are honoured, and
no policy setting can override it. Use it where trusting a directory must require a
deployment, not an admin session. ORION_EXTERNAL_PROVIDERS_UI=false is the
milder version: existing database providers keep working, they just become read-only.
The same two switches exist as policy settings, so a platform admin can disable database
providers without a redeploy; the environment variable simply cannot be overridden from
inside the application.
Discovery: domain or identity
Discovery answers one question — the user typed an email address, which provider (if any) should they be offered?
| Mode | Also offered to |
|---|---|
| Anyone at a listed email domain (default) | addresses whose domain is in the provider's DOMAINS list. This is how a first-time user — who by definition has no link yet — reaches the provider at all. |
| Only accounts already linked | nobody. Domains are ignored entirely. |
The two settings answer genuinely different questions. Domain matching asks does this
address look like it belongs to that directory — necessary for a first sign-in, but
it offers the provider to every address at a domain you may share with others, and it
cannot serve a contractor on a gmail.com address who is a guest in your
tenant. Restricting to linked accounts is precise, at the cost of needing every user to
be linked first — by an administrator, or by the user from their own account page.
A link is matched on either address: the one the directory asserts (its
upn, recorded on the link) or the Orion account's own email. Those are
frequently different, and only matching the first made correctly linked accounts
invisible to discovery.
Turning off Offer on the sign-in page hides a provider from everyone except the accounts already linked to it — useful while a directory is being rolled out. Marking it inactive withdraws it from everyone, linked or not, because an inactive provider cannot validate a sign-in and offering it would be a dead end.
Discovery is decided on the server, at
GET /federation/login/discover?email=&appId=. It has to be: the browser
cannot look up an identity link. A side effect is that the login page no longer publishes
every configured tenant and domain list to anonymous visitors.
Policy: creating and linking accounts
A validated token proves an identity at a directory. Policy decides what that entitles
the holder to. Set the server-wide policy at
/external-auth/platform/settings (platform org admins) and per-organisation
policy at /external-auth/org/<org>.
Creating accounts
| Setting | Behaviour |
|---|---|
| Never | the sign-in is refused unless an Orion account already exists. |
| In the provider's organisation (default) | the user is created in the organisation that owns the provider. |
| Create an organisation | the user is created and an organisation named after their email domain, with them as its administrator. |
Role for created users defaults to standard user. Setting it to Administrator means anyone the directory admits becomes an administrator, so combine it with the last provisioning mode only when the directory's membership is exactly the set of people you intend to hand organisations to.
Linking to an existing account
When a validated identity matches an Orion account by email but has no link yet:
| Setting | Behaviour |
|---|---|
| Never | refused. Links exist only where an admin or the user created one. |
| User-initiated only (default on new installs) | refused at the login page. The user signs in to Orion normally and links from Sign-in Methods on their account page — so holding the directory identity is never on its own enough to take over an Orion account. |
| Automatic | linked on the first successful external sign-in, trusting the provider's address. Sound where the directory owns that email domain; not otherwise. |
Once a link exists it is what authenticates the user, matched on the provider's immutable subject rather than the address — so a directory reassigning an address does not hand the new holder the old account.
On the Entra side
- Register an application in Entra ID.
- Add a Single-page application redirect URI of
https://login.shanecraven.com/federation/login/aadcallback— MSAL runs in the browser, so it must be an SPA platform registration, not Web. - Expose an API scope (e.g.
Orion.Federation.Login) and use its full identifier asSCOPE. - Grant consent for the users or groups who should be able to sign in.
Both sign-in portals
Orion has two portals, and external sign-in works in both. Which one your users see
depends on ORION_AUTH_MODE — a fresh install defaults to OAuth.
| Portal | Sign-in page | External sign-in posts to |
|---|---|---|
| OAuth (default) | /oauth/{org}/authorize → the sign-in page |
POST /oauth/{org}/login/external |
| Legacy | /Federation/Login |
the legacy login API, via the federationAad cookie |
Both validate the token and resolve it to a user through the same service, so providers, discovery, provisioning and linking behave identically whichever portal a user arrives at. On the OAuth page the provider button appears as soon as the typed address matches; the password field is hidden entirely when policy has disabled password sign-in.
The flow (legacy portal)
▶ domain matched against every provider's DOMAINS
▶ matching providers offered (plus "Sign in with password")
Browser ──▶ MSAL acquires a token — silently if the user already has
a Microsoft session, otherwise via a popup
▶ token stored in the
federationAad cookieBrowser ──▶ back to /Federation/Login
Server ──▶ presents the token to its own legacy login API as
AadAccessToken▶ validated against the configured providers, exchanged for a session key
▶
OrionFederationKey cookie set — the user is signed in
/federation/login/aadcallback exists purely as the MSAL redirect target; MSAL
completes the exchange in the browser, so the action itself returns 200 and does
no work.
The trust decision lives in the legacy login endpoint
(POST /legacy/api/Authentication/User/Login, AuthenticationController).
It takes the base64 FederationCombinedToken from the Authorization: bearer
header, pulls out the aad token, and validates it — signature, issuer, audience
and scp — against Microsoft's JWKS for each configured provider's tenant. The
upn claim of the first provider that validates becomes the user's email, and the
password check is bypassed. With no provider configured, no Entra token is ever
accepted.
Endpoints behind the login page:
GET /federation/login/discover?email=&appId=— the providers that apply to one address, by whichever discovery mode each provider uses.Result: truemeans none applied, so the user signs in locally;AllowPasswordreflects the policy.GET /federation/login/providers?appId=— providers configured for an organisation, unfiltered. Domain lists are deliberately not returned.GET /federation/login/islocal?id=<email>— retained alias for/discover;idis the email address.
Behaviour worth knowing
- Discovery is domain-based, not enforced. The password option remains unless
ORION_REGISTRATION_REQUIRE_EXTERNALis set — matching a domain offers Microsoft, it does not compel it. - No provider configured ⇒ no discovery page. With an empty provider list the login page falls back to password entry, which is why an install can look like it has no external authentication at all.
- The app registration is the real boundary.
DOMAINSonly decides which button is offered — it is not an access control. Whoever you consent to the Entra app can present a valid token, so restrict the app to the users or groups you intend, and use the provisioning and linking policy to decide what a valid token is worth. - Sign-ins refused by policy say so. A user whose token is genuine but whose account cannot be created or linked is returned to the login page with an explanation, not bounced silently — and the server log records the outcome and reason.
- MSAL caches in
localStorage, so a returning user is usually signed in silently./federation/cleartears that down along with the cookies.
federationAad cookie is written by client-side JavaScript, so it is
not HttpOnly — unavoidable, since MSAL must set it. Treat it as a short-lived bearer token:
it is exchanged for an Orion session immediately and is not a long-term credential.
If Microsoft sign-in does not appear
- Check
https://login.shanecraven.com/federation/login/discover?email=someone@yourdomain.com. An emptyProviderslist means discovery, not Entra, is what refused. - Is a provider configured at all? Environment variables must be named exactly
ORION_EXTERNAL_PROVIDER__<ID>__<PROPERTY>(double underscores); database providers appear under/external-auth/org/<org>. - Is external sign-in enabled by policy, and are database providers allowed? Check
ORION_EXTERNAL_PROVIDERS_DBand the server policy at/external-auth/platform/settings. - Using linked-identity discovery? Nothing is offered until a link exists — the user has to sign in normally once and link from Sign-in Methods.
- Does the email domain match
DOMAINS? Matching is on the part after@, lower-cased. - Is
CLIENT_IDset? Providers without one are listed but no MSAL client is created for them, so the button does nothing. - Redirect URI mismatch in Entra — it must be the SPA platform and exactly
https://login.shanecraven.com/federation/login/aadcallback. - Configuration is read at startup — restart after changing any of these.
- Sign-in bounces back to the login page after Microsoft succeeds ⇒ the browser got a
token but the server rejected it. Check the server log for
Entra token validation failed for provider …: it is almost alwaysTENANT_ID, or anAUDIENCEthat does not match the app registration's Application ID URI.