OIDC setup
Orbit has no local passwords — sign-in is always via an external OIDC provider
using the Authorization Code flow with PKCE. OIDC_ISSUER_URL,
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET and OIDC_REDIRECT_URI are always
required (see Configuration).
Any provider that implements OIDC Discovery works — the sections below are worked examples for the common ones, not a list of what is supported.
The redirect URI rule
The redirect URI is always <external-url>/auth/callback, where
<external-url> is the URL a browser reaches Orbit at:
http://localhost:3000/auth/callbackin local developmenthttps://storage.example.com/auth/callbackin production
Whatever you register with your provider must match OIDC_REDIRECT_URI
exactly — scheme, host, port and path. Orbit sends the configured value at
both legs of the flow (authorization and token exchange), so a mismatch fails
the exchange rather than silently logging someone in.
Set OIDC_REDIRECT_URI to the public HTTPS URL, even though the app itself
receives plain HTTP behind the proxy. Orbit deliberately uses the configured
value rather than reconstructing the URL from the request, so it does not
depend on X-Forwarded-Proto being set. The session cookie's Secure flag is
derived from this value too, which is another reason it must be the https://
one.
What Orbit reads from the token
| Claim | Used for | Required |
|---|---|---|
sub | The user's identity — the key their profiles and settings hang off | Yes. Login fails without it |
email | Display in the nav rail | No |
name | Display in the nav rail | No |
Providers disagree about where they put the last two, so Orbit falls back in this order:
- email:
email→preferred_username→upn - name:
name→given_name+family_name→preferred_username→ email
If the ID token carries neither an email nor a name, Orbit makes one call to
the provider's userinfo endpoint and fills the gaps from there. Providers
that put everything in the ID token never incur that request.
preferred_username and upn are not verified email addresses — Entra ID
returns a UPN that merely looks like one. Orbit uses them for display only, and
identity is always sub. Do not treat the email Orbit shows as proof of an
address in another system.
Orbit has no authorization model: any user who completes the flow gets full access, and there is no admin or read-only tier. Restrict who may sign in at the provider — see Security.
Keycloak
- In your realm, Clients → Create client. Client type OpenID Connect, client ID of your choosing.
- Client authentication: On (confidential). Leave Standard flow enabled; the others are not needed.
- Valid redirect URIs: your
<external-url>/auth/callback. - Copy the secret from the Credentials tab.
OIDC_ISSUER_URL=https://id.example.com/realms/my-realm
OIDC_CLIENT_ID=orbit-object-console
OIDC_CLIENT_SECRET=<from the Credentials tab>
OIDC_REDIRECT_URI=https://storage.example.com/auth/callback
The issuer is the realm URL — no /protocol/openid-connect suffix. Keycloak
puts email and name in the ID token, so no userinfo call is made.
To restrict access, add a group or role condition to the client rather than leaving it open to the whole realm.
Microsoft Entra ID (Azure AD)
- App registrations → New registration. Under Redirect URI choose
Web and enter your
<external-url>/auth/callback. - Certificates & secrets → New client secret; copy the Value (not the Secret ID) — it is shown once.
- API permissions: the delegated
openid,profileandemailpermissions under Microsoft Graph are enough. No admin consent is needed for these.
OIDC_ISSUER_URL=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_CLIENT_ID=<application (client) ID>
OIDC_CLIENT_SECRET=<the secret Value>
OIDC_REDIRECT_URI=https://storage.example.com/auth/callback
Three things that trip people up:
- The
/v2.0suffix is required. Without it discovery returns the v1 endpoints, whose ID tokens differ. Use the tenant GUID rather thancommonororganizationsunless you genuinely intend a multi-tenant app — those issuers vary per tenant at runtime and will not match a fixed issuer. emailis often absent from the ID token. Entra ID only emits it when the account has a mail attribute or the optional claim is configured. Orbit handles this: it falls back topreferred_usernameand, failing that, callsuserinfo. You do not need to configure the optional claim, though you may if you want a real mail address rather than a UPN.- Restrict access in the app registration, under Enterprise applications → Properties → Assignment required, plus user/group assignment. Otherwise every account in the tenant can sign in.
Google
- In Google Cloud Console: APIs & Services → Credentials → Create credentials → OAuth client ID, application type Web application.
- Authorized redirect URIs: your
<external-url>/auth/callback. - Configure the OAuth consent screen. For an organisation, set the user type to Internal so only your Workspace accounts can sign in.
OIDC_ISSUER_URL=https://accounts.google.com
OIDC_CLIENT_ID=<client id>.apps.googleusercontent.com
OIDC_CLIENT_SECRET=<client secret>
OIDC_REDIRECT_URI=https://storage.example.com/auth/callback
The issuer has no path component. Google returns email and name in the ID
token, so no userinfo call is made. Google does not offer per-application
access rules beyond the consent screen's user type — if you need finer control,
put Orbit behind a provider that does.
Any other provider
Authentik, Auth0, Okta, Zitadel, Dex, GitLab and anything else implementing OIDC Discovery work the same way. You need four things:
-
The issuer URL — the value where
<issuer>/.well-known/openid-configurationresolves. Check it before anything else:curl -s https://id.example.com/realms/my-realm/.well-known/openid-configuration | headIf that does not return JSON, the issuer URL is wrong and Orbit will fail to start with a discovery error.
-
A confidential client with the Authorization Code flow enabled. Orbit sends PKCE (
S256) as well; providers that require PKCE are fine, and those that ignore it are too. -
The redirect URI registered exactly as above.
-
Scopes, if your provider needs something other than the default
openid profile email— setOIDC_SCOPES(space-separated).openidmust always be included.
OIDC_ISSUER_URL=https://id.example.com
OIDC_CLIENT_ID=orbit-object-console
OIDC_CLIENT_SECRET=<client secret>
OIDC_REDIRECT_URI=https://storage.example.com/auth/callback
OIDC_SCOPES="openid profile email" # only if the default is not right
Troubleshooting
| Symptom | Cause |
|---|---|
| Server exits at startup with a discovery error | OIDC_ISSUER_URL is wrong, unreachable, or (for Entra ID) missing /v2.0. Test the .well-known URL with curl |
invalid_grant / "Incorrect redirect_uri" after signing in | The registered redirect URI does not match OIDC_REDIRECT_URI byte for byte — most often http vs https behind a proxy |
invalid_client at the token step | Wrong client secret, or the client is public rather than confidential |
| Login succeeds but the nav rail shows no email | The provider supplied neither email nor preferred_username nor a userinfo endpoint that returns them. Harmless — identity is sub |
| Login fails with "no sub claim" | The provider returned a token without sub. Check that you are using an OIDC client, not a plain OAuth2 one |
| Everyone in the directory can sign in | Expected — Orbit has no authorization model. Restrict at the provider |
The server logs the reason for a failed exchange, so kubectl logs (or the
container's stdout) is the fastest place to look.