Identity and SSO
The mental model behind how dtedge knows who you are, which orgs you belong to, and what you can do in each. Read this before setting up SSO; the configuration pages assume you know the shape.
For setup walkthroughs see Configure global SSO and Configure per-org SSO.
Three ways to authenticate
| Method | When you'd use it | Stored where |
|---|---|---|
| Local password + 2FA | Default for the first super-admin, dev clusters, break-glass | bcrypt hash in dtedge DB |
| OIDC SSO | Enterprise installs where the IdP is the identity authority | dtedge links the IdP identity to a user row via (provider, subject) |
| API key | Non-interactive callers (CI, scripts, custom dashboards) | sha256 hash in dtedge DB |
The three resolve to the same user record under the hood — SSO doesn't create a parallel user table; it links into the existing one. An SSO user can also have an API key. A local user can SSO-link later.
Two SSO surfaces
dtedge supports two distinct SSO configurations, not mutually exclusive:
Per-org SSO
Each organization brings its own IdP that authenticates its
own members. Configured by the org admin at Organizations → <org> → SSO. Each org's IdP is independent — Acme using Auth0,
BorTech using Keycloak, Foo Co. using Azure AD all coexist.
Per-org SSO works on slug-first login: the user picks "Sign in
with SSO" on the login page and types their org slug (acme).
The dtedge backend looks up which IdP that slug maps to, redirects
the user, and verifies the returned token against that org's
configured client.
Global SSO
One IdP for the whole install. Configured by a super-admin at
Admin → Settings → Global SSO. Used for:
- Single-tenant deployments where one customer owns everything and wants one IdP across every org.
- Super-admin break-glass when per-org SSO is broken.
- Cross-org membership driven by IdP groups (see global org-role assignments).
A global SSO user logs in by picking "Sign in with SSO" on the login page without typing a slug — global has no realm.
Which one fires?
User clicks "Sign in with SSO"
│
▼
Did they type an org slug?
│
├── No → Global SSO (if configured)
│
└── Yes → Does that org have per-org SSO?
├── Yes → Per-org SSO
└── No → Fall through to global (if configured)
OR error "no SSO for this realm"
Per-org SSO takes precedence for the slug it covers. Global SSO is the fallback.
Identity linking
When you SSO in for the first time, dtedge looks for a (provider, subject) pair in the user_identities table:
- Match: link found → resolve the user, you're in.
- No match by
(provider, subject), but email matches an existing user: dtedge creates the identity link to that user. Your existing local-account roles + memberships stay. - No match by either: dtedge creates a fresh user — but only if auto-provision is on for this SSO config. Otherwise you see a "not provisioned" page; an admin needs to invite you first.
The match key is (provider, subject) not email because:
- IdP
subclaims are stable across email changes - The same user might exist at different IdPs with the same email (we want different linked users, not collision)
Group → role reconciliation
When the IdP ships a groups claim and the SSO config has a
group→role mapping (per-org SSO) or org-role assignments
(global SSO), dtedge reconciles roles on every login:
User's groups from IdP : [engineers, finance-viewers]
Mapping rows : engineers → editor
finance-viewers → viewer
sales → viewer
What user should have : editor, viewer (in org X for per-org;
in org X and finance for global)
What user has in Casbin : editor, sales-viewer (stale!)
Diff:
add : viewer
revoke : sales-viewer (mapping says they're not in `sales` any more)
keep : editor (still in `engineers`, still maps)
Two important guarantees:
-
Only roles in the mapping universe are touched. If you manually granted "admin" outside the mapping, it stays. The reconcile only removes roles that were in the mapping universe but aren't in
should-have. -
Absent groups claim is treated as "no signal". If the IdP silently stops shipping the
groupsclaim (broken Action, misconfigured scope), reconcile is skipped entirely. Without this safety net every user would lose every mapping-driven role on every login until the operator noticed. TheHasGroupsClaimflag in the backend's verify step is what enforces this.
Super-admin isolation
The super-admin flag is never touched by SSO group mapping.
It's set manually via Admin → Users → Toggle super-admin. A
super-admin who falls out of every IdP group keeps the
platform-wide flag.
This is intentional: the highest-privilege flag in the system shouldn't be assignable from outside the system. An IdP misconfig can't accidentally grant super-admin; an IdP compromise can't escalate.
Require SSO
Both SSO surfaces have a Require SSO toggle:
- Per-org Require SSO: members of this org can't use local
password. If they only belong to this org, their
/loginwith email+password returnsSSO_REQUIRED. - Global Require SSO: install-wide. Every non-super-admin user must SSO in. Super-admins keep local as break-glass — forcing them to SSO would let an IdP outage take the whole platform admin path down.
The login gate looks at:
Is the user a super-admin? → local always works
Does global require_sso = true? → local refused
Do all the user's orgs require SSO? → local refused
Otherwise → local works
Session lifetime
Once you log in (local or SSO), you get a JWT with iat (issued
at). The JWT lives until either:
expis reached (default 24h, configurable)- A session invalidation event stamps
users.sessions_invalidated_atlater than your token'siat
Session-invalidation events:
| Event | Triggered by |
|---|---|
| You change your password | /auth/password |
| Admin resets your password | Admin → Users → Set password |
| Admin disables your account | Admin → Users → Toggle active off |
| You're toggled to/from super-admin | Admin → Users → Toggle super-admin |
| Admin resets your 2FA | Admin → Users → Reset 2FA |
| You disable your own 2FA | Settings → 2FA → Disable |
| You log out | /auth/logout |
| Your IdP back-channel logout fires | IdP → POST /auth/sso/backchannel-logout |
After any of these, your tokens are rejected on the next request.