Platrium Docs
ArchitectureAuthentication

OpenID Connect

Sign in with Okta, Auth0, Entra, Authentik or anything else that speaks OIDC, without a single vendor library.

Platrium can hand sign-in over to any OpenID Connect provider. There is no "Auth0 integration" and no "Okta integration": an administrator gives Platrium an issuer URL, a client ID and a client secret, and discovery does the rest. If it speaks OIDC, it works!

The protocol code is engine/internal/auth/protocol/oidc. The generic half (who gets in, and the session) is auth.Manager in engine/internal/auth/manager.go. Admin APIs are IdpAdmin in engine/internal/orchestrator/idpadmin.go and api/graphql/core/idp.graphql.

The Big Idea: Protocols Only Verify

Local passwords, OIDC and (in the enterprise edition) SAML all do the same job in the end: prove who someone is. Everything after that is identical, so it lives in one place.

Local password IdpAuthHandoff OIDC SAML (EE) IdpProvider.Admit auth.Manager Session

A protocol turns its own response (an ID token, a SAML assertion) into an IdpAuthHandoff: a subject, an email, a name. It does not decide whether the person is allowed in. That is the job of Admit and the Manager:

  • IdpProvider.Admit is the one gate every sign-in passes. It refuses a disabled provider, a disabled user, and an email outside the provider's allowed domains.
  • Manager.HandleFederatedLogin finds the user, creates them on first sign-in if the provider allows it, keeps their name and email current, and starts the session.

The OIDC handler never asks "is this provider enabled?" It only knows how to do OIDC.

How A Sign-In Works

GET /api/auth/login?idp=...&return_to=/home discovery (issuer/.well-known/openid-configuration) state, nonce, PKCE verifier, sealed into a cookie 302 to the provider + Set-Cookie platrium_authverify_... the user signs in there 302 back with ?code=...&state=... GET /api/auth/oidc/{idpId}/callback (with the cookie) open the cookie, check it matches the state exchange the code (with the PKCE verifier) verify the ID token, map the claims Admit, find or create the user 302 to return_to, with the session cookie Browser Platrium Identity provider

The pieces worth knowing:

  • Discovery finds every endpoint from the issuer URL, so nothing about a provider is hardcoded.
  • PKCE (S256) is always on, even though we also use a client secret.
  • The ID token is checked for signature, issuer, audience, expiry and the nonce. If the email is not in the token, we ask the userinfo endpoint, but only trust it for the same subject.
  • return_to is cut down to a same-origin path (and at most 512 characters), so a sign-in link can't bounce you to someone else's site.

Where A Sign-In In Progress Lives

Between leaving for the provider and coming back, Platrium has to remember the state, nonce and PKCE verifier. It remembers them in the browser, in a short-lived cookie that only the server can read.

The value is sealed (encrypted and authenticated) with a key derived from PLATRIUM_SECRET_KEY. Each sign-in gets its own cookie, named platrium_authverify_ plus a short hash of its state. It is HttpOnly, SameSite=Lax, good for ten minutes, only sent to the callback path, and removed by the callback whatever happens.

Who Gets An Account

Identity is the pair (provider, subject): the provider's own stable ID for the person (OIDC's sub), never their email.

Platrium never links an external sign-in to an existing user by email. If it did, anyone who could get a provider to claim admin@acme.com would become the admin. A person arriving through Okta with the admin's email address gets their own separate account, as a plain member.

Each external provider carries a provisioning policy, the same for every protocol:

SettingMeaning
jitUsersCreate the account on first sign-in. When off, only people who already have one can sign in.
defaultRoleThe role a created user starts with. Never an administrative role (see below).
allowedEmailDomainsWhen not empty, only these email domains may sign in.

On every sign-in the user's email and display name are refreshed from the provider, which is the source of truth for who they are. Their external_id never changes.

Redirect URIs

Every provider is registered with its own callback address:

{APP_URL}/api/auth/oidc/{idpId}/callback

APP_URL is the public address of the installation (for example https://files.example.com). The provider's ID in the path means a callback says which provider, and therefore which tenant, answered before anything else is looked at. With several tenants and several providers each, a single shared callback would have nothing to go on.

The admin API returns the finished address as a read-only redirectUri, and the settings page shows it with a copy button.

Storing A Provider

A provider is two rows. idp_providers holds what every provider has (name, type, enabled, the provisioning policy). idp_oidc_configs holds the OIDC settings, one row per provider, with typed columns:

ColumnNotes
issuerFixed at creation (see below)
client_id, client_secretThe secret is sealed (enc:v1:...), bound to the provider's ID
scopesRequested in addition to openid
email_claim, name_claim, picture_claimDefault to email, name, picture
require_email_verifiedOn by default
extra_auth_paramsFor providers that need more, such as Auth0's audience

A later SAML provider gets its own table beside this one, so each protocol has typed, required columns instead of a blob of JSON.

The Secret Key

PLATRIUM_SECRET_KEY is required: the engine will not start without it. It must be the same on every instance of a cluster. Different purposes (signing upload receipts, sealing client secrets, sealing sign-in cookies) get their own keys derived from it, so none shares raw key bytes with another.

Change the key and every sealed client secret becomes unreadable: the providers stay, and each needs its secret entered again. Pick one and keep it safe.

The Built-In Provider Is Special

Every tenant gets one LOCAL provider when it is created. It is the fallback that keeps people signed in when SSO is down, so it is read-only and permanent: it cannot be created again, edited, disabled or deleted. That rule is in IdpStore and nowhere else, so every caller gets the same answer.

Managing Providers

Administrators (anyone holding idp.manage, which is the super administrator today) use the Identity providers page in the admin console, backed by these GraphQL fields:

FieldDoes
identityProviders, identityProvider(id)List and read, with user counts. Never returns a secret.
createIdentityProviderCreates the provider and its config together. config is a @oneOf input: exactly one protocol.
updateIdentityProviderThe settings every type shares
updateOidcConfigOIDC connection settings. Leave clientSecret out to keep the current one.
setIdentityProviderEnabledDisabling refuses new sign-ins; accounts and files stay
deleteIdentityProviderOnly with no users. Otherwise disable it.
testOidcDiscoveryChecks an issuer URL before anything is saved

The settings every provider shares are one form, whatever the type. Only the connection settings differ, and the web app takes them from a small registry (connections/index.ts), so adding SAML later is one new entry.

Trying It With Auth0

Auth0 is a handy way to try this without running your own provider.

Start the engine with the two required settings. APP_URL is where you open Platrium in the browser, and the key can be any long random string:

export APP_URL=http://localhost:3000
export PLATRIUM_SECRET_KEY="$(openssl rand -base64 32)"

Create an application in Auth0. Choose Regular Web Application. Note its Domain, Client ID and Client Secret. Leave the callback URL empty for now.

Add the provider in Platrium. Sign in as the super administrator, open Admin console, Identity providers, Add provider. Use https://<your-tenant>.us.auth0.com as the issuer, press Test, then enter the client ID and secret.

Copy the redirect URL. After you add the provider, Platrium shows its callback address. Paste it into the Auth0 application under Allowed Callback URLs, and save.

Sign out and try it. The login page now has a "Login with ..." button. A first sign-in creates an account as a member, if you left account creation on.

Browsers share cookies across ports on localhost. If you run two engines on different ports, they overwrite each other's login cookie, which looks like random sign-outs. Stick to one engine at a time.

When Something Goes Wrong

A failed sign-in sends the person back to the login page with ?error=<code>, and the page shows a plain sentence. The detail goes to the engine log, never to the browser.

CodeMeans
sso_deniedThe person or the provider declined
sso_expiredThe sign-in is stale, already used, or this browser didn't start it
sso_no_accountNo account, and the provider doesn't create them (or the email domain isn't allowed)
sso_disabledThe account or the provider is disabled
sso_failedAnything else

For sso_expired, look for the log line starting with oidc callback. It says exactly why: no cookie, a cookie from another sign-in, the wrong provider, or too slow.

Not Yet

  • Logout. Signing out of Platrium does not sign you out of the provider, and a provider's logout notice does not end Platrium sessions. (sessions_valid_after is already there for the second.)
  • Disabling doesn't sign anyone out. It stops new sign-ins; existing sessions run until they end.
  • Groups and group-based roles. See above.
  • SSRF protection. The server fetches the issuer URL an administrator types in. That is fine while only the cluster operator sets issuers, but before multi-tenancy ships, a tenant's own administrator must not be able to point it at internal addresses. It needs a guard that refuses private ranges (by resolved IP), switchable off for self-hosted installs that really do use an internal provider.

On this page