External Authentication

Signing in with Microsoft (Entra ID) via home-realm discovery

Back to Documentation

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.

This is identity federation — who the user is. It is unrelated to External Claim Providers, which supply claim values for tokens Orion has already issued.

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
PropertyMeaning
TYPEaad — the only type implemented. Anything else is listed but never initialises an MSAL client.
DISPLAY_NAMEButton label on the discovery page.
CLIENT_IDApplication (client) ID of the Entra app registration.
TENANT_IDDirectory (tenant) ID. Becomes the MSAL authority https://login.microsoftonline.com/<tenant>.
SCOPEThe 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.
AUDIENCEOptional. 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.
DOMAINSEmail 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:

EnvironmentDatabase
Controlled bywhoever deploys the serverwhoever holds an admin session
Changed byredeploy / restartthe UI, immediately
Scopeevery organisationone organisation, or every one
Can an org admin remove it?noyes, if it is theirs
Turning the database off. 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?

An account already linked to a provider is always offered it. A link is explicit intent by the user or an administrator, so no setting suppresses it. The discovery mode decides who else is offered the provider.
ModeAlso 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 linkednobody. 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.

Linked-identity discovery does reveal, to anyone who can reach the login page, whether a given address is linked to a directory — that is inherent to answering the question at all. Domain matching leaks nothing per-address. If that distinction matters for your deployment, prefer domain matching.

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>.

Organisations can only tighten. The two policies are combined and the stricter wins, so an organisation admin can never grant their organisation something the platform admin withheld. A value you set may be shown back to you reduced — that is the effective policy, not a failed save.

Creating accounts

SettingBehaviour
Neverthe 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 organisationthe 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:

SettingBehaviour
Neverrefused. 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.
Automaticlinked 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

  1. Register an application in Entra ID.
  2. 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.
  3. Expose an API scope (e.g. Orion.Federation.Login) and use its full identifier as SCOPE.
  4. 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.

PortalSign-in pageExternal 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.

A directory proves who someone is, not which organisation they belong to. The OAuth path therefore refuses a validated identity whose Orion account belongs to a different organisation than the one in the authorize URL — otherwise a user of one organisation could obtain a code asserting another.

The flow (legacy portal)

User enters email on /Federation/Login
   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 cookie
Browser ──▶ 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: true means none applied, so the user signs in locally; AllowPassword reflects 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; id is the email address.

Behaviour worth knowing

  • Discovery is domain-based, not enforced. The password option remains unless ORION_REGISTRATION_REQUIRE_EXTERNAL is 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. DOMAINS only 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/clear tears that down along with the cookies.
The 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 empty Providers list 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_DB and 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_ID set? 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 always TENANT_ID, or an AUDIENCE that does not match the app registration's Application ID URI.
Authentication Claim Providers All Documentation