Single sign-on (OpenID Connect)
OpenID Connect, JIT provisioning, group-to-role
Edit this pageOn this page
OpenID Connect with the authorization-code flow and PKCE, just-in-time provisioning, and group-to-role mapping. Configure under Settings → SSO.
Which providers work
Any provider that speaks OpenID Connect. There is no list of supported
vendors in the code and no per-vendor branch: you give Loomscope an issuer
URL, and it reads that provider's /.well-known/openid-configuration to
learn where the authorization, token, JWKS and userinfo endpoints are. A
provider that moves an endpoint keeps working, and there is no way to save a
configuration where four URLs are right and the fifth has a typo.
That covers Entra ID, Okta, Google Workspace, Keycloak, Authentik, Zitadel, Auth0, Ping, GitLab, Dex and anything else conformant — but the point is that none of them are named anywhere in the implementation. Only Keycloak is exercised in CI, which is a statement about our test rig, not about compatibility.
The sign-in button reads "Sign in with " plus whatever you typed in Name, so it says what your organisation calls its directory.
What is not supported
- SAML is not implemented. A provider that only speaks SAML cannot federate with Loomscope today.
- LDAP / Active Directory bind is not implemented either. For AD, use Entra ID's OIDC endpoint; for a bare LDAP directory, most identity providers can front it and speak OIDC to us.
For provisioning and deprovisioning at scale, SCIM 2.0 works alongside any of the above.
Configuring a provider
| Field | What it is |
|---|---|
| Name | What the sign-in button says |
| Issuer | The provider's issuer URL. Discovery is fetched from it |
| Client ID | From the application you registered with the provider |
| Client secret | Stored encrypted, like every other secret |
| Scopes | openid profile email plus whatever carries groups |
| Groups claim | The claim holding group names — often groups or roles |
| Group → role | A mapping from group name to Loomscope role |
| Default role | What a user with no mapped group gets on first sign-in |
The redirect URI to register with your provider is:
https://loomscope.example.com/api/sso/callbackIt must match exactly, including scheme and port. BETTER_AUTH_URL has to
match the browser address for the same reason — the redirect URI is derived
from it rather than from the incoming request, so that a forwarded Host
header cannot move where the authorization code is delivered.
Settings → SSO shows you the exact URI to register. If BETTER_AUTH_URL
is unset it shows nothing and says so, rather than printing a localhost
address you would register and then spend an afternoon debugging as
redirect_uri mismatch.
How the flow works
- A user picks the provider on the sign-in page, which hits
/api/sso/start/{providerId}. - Loomscope generates a PKCE verifier and an HMAC-signed state carrying the provider and the return path, and redirects to the provider.
- The provider authenticates and calls back to
/api/sso/callback. - The state signature and the PKCE verifier are checked, the code is
exchanged, and the
id_tokenclaims are verified — issuer, audience, expiry and nonce. - The user is provisioned or matched, roles are applied, and a session is created.
The state cookie is scoped to /api/sso and is single-use. A callback
without a matching state is refused rather than treated as a fresh login.
Just-in-time provisioning
A user who authenticates successfully and has no membership gets one, at the default role. That is what makes SSO worth having: nobody has to be invited first.
Two guards on it:
- Email must be verified by the provider. An unverified email claim is refused, because otherwise anybody who can register at your provider with an address they do not own gets an account.
- A deprovisioned membership cannot sign back in. A member deactivated over SCIM is refused at the callback even with a completely valid directory login. Deprovisioning that only stops the next invitation is not deprovisioning.
Group-to-role mapping
Map a provider group to a Loomscope role. At each sign-in, the user's groups are read from the configured claim and the mapping applied.
In several mapped groups, the strongest role wins. The order a directory lists groups in is not something an operator can see or control, so a first-match rule would make the answer arbitrary.
No mapped group means no change. An existing user whose groups map to nothing keeps the role they have; only a brand-new user gets the default role. A directory reorganising its groups must not silently strip a role an operator set by hand.
Tested against
The flow is exercised end to end in CI against Keycloak, using a real authorization-code round trip rather than a mocked provider. Entra ID, Okta and Google Workspace all speak the same flow; the fields that differ between them are the issuer, the scopes and the name of the groups claim.
Entra ID
The groups claim carries object IDs by default, not names. Either configure the application to emit group names, or map the object IDs — the mapping takes whatever strings the claim contains.
Okta
Add a groups claim to the ID token with a filter matching the groups you
want emitted; Okta does not include them by default.
Troubleshooting
Sign-in redirects somewhere wrong. BETTER_AUTH_URL does not match the
browser address. It must include scheme and port.
The callback returns "invalid state". The state cookie did not arrive. Usually a proxy stripping cookies, or a redirect URI on a different host than the one that started the flow.
A user is provisioned with the wrong role. Check the groups claim name, and check what the provider actually emits — the claim frequently contains identifiers rather than names.
A deactivated user can still sign in. They were deactivated in the provider but not in Loomscope. OIDC only tells you about people at the moment they authenticate; that gap is exactly what SCIM closes.
Local accounts alongside SSO
Enabling SSO does not disable local sign-in. An owner should keep one local account with a strong password as a break-glass path — an identity provider outage otherwise locks everybody out of the tool they would use to investigate it.