Command Palette
Search for a command to run...

Enterprise SSO (IDP mode)

A deployment can run in IDP mode: users onboard through your organization's OpenID Connect identity provider instead of the per-device password flow. This is a deliberate, opt-in change to the threat model - read Key custody and the threat-model notes before enabling it.

How it differs from standalone

StandaloneIDP mode
OnboardingPer-device password setupSSO via your IdP (auth-code + PKCE)
Vault unlock keyArgon2id(unlock password)HKDF(factor1 ‖ K2)
Where the second factor livesn/afactor1 in the IdP (or a passphrase); K2 on the server
Password signup / login routesShownHidden, the client routes to SSO
RevocationPer-deviceIdP deprovision + identity-device revoke

Enabling it

IDP mode is env-only, there is no admin UI and no editable config table, so the server is the single source of truth clients read.

Set the mode + OIDC config at boot

OPENSECRET_AUTH_MODE=idp \
OPENSECRET_OIDC_ISSUER=https://login.example.com/realms/acme \
OPENSECRET_OIDC_CLIENT_ID=open-secret \
OPENSECRET_OIDC_PROVIDER_KIND=generic \
OPENSECRET_OIDC_SCOPES="openid email profile" \
./bin/open-secret-server -server-url=https://vault.example.com

On first boot the server discovers the issuer's OIDC configuration (a network call to <issuer>/.well-known/openid-configuration) and fails loud if the issuer is unreachable or required params are missing, a misconfigured node never accepts logins it can't validate.

Register the client at your IdP

You register this app, in your own identity provider / tenant - open-secret ships no IdP application of its own, and there is no vendor-owned or multi-tenant registration involved. Each self-hosted deployment owns its own single-tenant app.

Register a public client (PKCE, no client secret), in Entra this is the "Single-page application" platform, and allow these redirect URIs:

  • Web app: https://vault.example.com/sso/callback
  • Browser extension: https://<extension-id>.chromiumapp.org/sso

Release the email scope (the account is keyed on it). That bare app is all the generic provider needs; the attribute providers add one delegated permission (see Providers).

Restart to reload

OIDC config is read at boot. Restart the server after changing any OPENSECRET_OIDC_* value.

How the server trusts tokens (no secret or cert to install)

This is the part people expect to be fiddly and isn't. The client is a public PKCE client, so:

  • There is no client secret to put on the open-secret server (or anywhere). PKCE, the code_challenge/code_verifier pair the client generates per sign-in, replaces the secret. OPENSECRET_OIDC_CLIENT_ID is the only client identifier the server holds, and it's public.
  • There is no signing certificate or key to copy in, either. The server validates each ID token's signature against the IdP's public keys, which it fetches automatically from the issuer's JWKS endpoint (found via <issuer>/.well-known/openid-configuration at boot, then cached and rotated on its own). You configure the issuer URL; the server retrieves and refreshes the keys itself.

So the entire server-side trust setup is: set OPENSECRET_OIDC_ISSUER + OPENSECRET_OIDC_CLIENT_ID, make sure the server can reach the issuer over HTTPS, and register the redirect URIs at the IdP. Nothing is uploaded to the server.

Which clients can sign in

IDP mode also gives you a real gate on which client can complete a login, and both halves of it are controlled by you (the IdP admin + the server config), never by the user:

  1. Redirect-URI allow-list (at the IdP). SSO only completes if the client redirects to a URI you registered for the client id. A user who stands up their own copy of the web UI on some other origin gets a different redirect URI, which the IdP rejects (redirect_uri_mismatch).
  2. Audience binding (at the server). The backend only accepts an ID token whose audience equals OPENSECRET_OIDC_CLIENT_ID. So a user can't sidestep gate 1 by registering their own IdP app with their own redirect URI: tokens from a different client id are refused.

Together these mean a rogue client cannot onboard via SSO. Two honest limits: it gates the login, not a user who has already unlocked on a legitimate client and then scripts the API with keys they hold (E2EE makes that irreducible); and it is IDP-mode only (standalone mode has no SSO, so a self-built client there just needs the backend URL and the user's password). This is why a locked-down deployment pairs SSO with managed devices that run only the policy-managed extension.

Environment reference

Env varRequiredWhat it does
OPENSECRET_AUTH_MODEyes (idp)Switches the deployment to IDP mode. Immutable after first boot.
OPENSECRET_OIDC_ISSUERyesOIDC issuer URL. Discovered at boot; bound as the ID-token issuer.
OPENSECRET_OIDC_CLIENT_IDyesThe public client id; also the ID-token audience.
OPENSECRET_OIDC_GROUPS_CLAIMno (groups)Claim carrying the user's groups; captured for a future group-sharing model.
OPENSECRET_OIDC_PROVIDER_KINDno (generic)entra, authentik, or generic. Selects how the client obtains factor1.
OPENSECRET_OIDC_SCOPESno (openid email profile)Space-separated scopes the client requests (include the IdP-API scope for attribute providers).

The entra and authentik providers read a few more provider-specific vars (OPENSECRET_OIDC_ENTRA_*, OPENSECRET_OIDC_AUTHENTIK_*), listed in Providers below. The generic provider needs none.

The ID token's signature is validated against the issuer's JWKS (RSA, RSA-PSS, and ECDSA families; symmetric HS* is rejected, it would require sharing a secret with the IdP).

Providers

The provider_kind chooses where factor1, the non-server half of the vault key, comes from.

Generic OIDC (passphrase)

Works with any OIDC provider. The IdP only proves identity; it does not store a key. Instead the user sets an unlock passphrase that derives factor1 = Argon2id(passphrase), entered on each sign-in.

Microsoft Entra ID

factor1 is a random 32-byte key stored client-side in a Microsoft Graph open extension on the user object. Add to the env:

OPENSECRET_OIDC_PROVIDER_KIND=entra
OPENSECRET_OIDC_SCOPES="openid email profile User.ReadWrite"
OPENSECRET_OIDC_ENTRA_EXTENSION_NAME=com.opensecret.k1
# OPENSECRET_OIDC_ENTRA_GRAPH_BASE defaults to https://graph.microsoft.com/v1.0

Grant the app registration delegated User.ReadWrite (one-time admin consent) so the client can read/write its own extension. The key is written once, atomically (create-if-absent), and read on every later unlock, the user never types a passphrase.

Authentik

factor1 is stored in the signed-in user's Authentik attributes:

OPENSECRET_OIDC_PROVIDER_KIND=authentik
OPENSECRET_OIDC_AUTHENTIK_BASE_URL=https://auth.example.com
# OPENSECRET_OIDC_AUTHENTIK_ATTRIBUTE defaults to opensecret_k1

Configure the OAuth scope so the token can reach the Authentik core users API.

Key custody

IDP mode relaxes one clause of the standalone invariant, "the server holds no cross-device secret", and binds the relaxation tightly:

  • The vault key is HKDF-SHA256(factor1 ‖ K2). Both 32-byte halves are concatenated as independent HKDF input, so neither half-holder can steer the derived key without the other's bytes.
  • K2 is server-held (one per identity). factor1 lives in the IdP (attribute providers) or is derived from the user's passphrase, and never transits the open-secret backend.
  • The backend escrows K2 plus the wrapped identity keypair blob. A stolen server database alone is K2 + ciphertext: insufficient to decrypt without factor1.
  • "The server never sees plaintext" is preserved in every provider.

The vault keypair is identity-centric: one escrowed keypair per user, surfaced as a single "identity device", so the entry-signing and sharing pipeline is byte-identical to standalone. The first device to sign in provisions and escrows the identity; later devices recover it after SSO.

Recovery and revocation

  • Revocation is IdP-driven. Deprovisioning the user at the IdP (or revoking the identity device) locks the account out, ExchangeSSOToken then fails and the per-request device check rejects a lingering token.
  • Operator backup still works and is mode-agnostic. Because IDP custody has no user-held backup file, an IDP deployment should enable the operator backup key so a user who loses IdP access is still recoverable.
  • Per-physical-device cryptographic revocation is not available in IDP mode (one escrowed keypair per user); revocation is identity-level.