Sign-in with a corporate account (OIDC)
Applies to: SimpleTwo 0.9.x · Checked: 20.09.2026
The coordinator is an OpenID Connect relying party: a person signs in with the same account they use everywhere else, and SimpleTwo creates their account on first sign-in and keeps the profile current from the provider's data.
Verified with Entra ID (Azure AD), ADFS, Keycloak, Okta and Google. ID-token signatures are checked against the provider's JWKS using RS256, which is what those providers use by default.
The coordinator has no direct SAML. A SAML-only provider connects through ADFS or Entra ID: to SimpleTwo they look like ordinary OIDC, and SAML stays between them and your provider. That is why the console calls the section "Single sign-on (OIDC)".
1. What to create at the provider
One web application registration with this redirect URL:
https://<coordinator address>/admin/api/sso/callback
The redirect URL is derived from the coordinator's public address, so you only change it when a domain or a proxy of your own sits in front.
Default scopes are openid email profile groups. Groups are what maps to roles; if your provider
delivers them under a different scope or claim name (Entra ID is the usual case), correct
Scopes and the claim name.
2. Settings in the console
Settings → Single sign-on (OIDC):
| Field | What it is | Default |
|---|---|---|
| Issuer | the issuer URL; configuration is read from <issuer>/.well-known/openid-configuration | — |
| Client ID / Client secret | the application's credentials at the provider | — |
| Redirect URL | the return address | <public address>/admin/api/sso/callback |
| Scopes | space-separated | openid email profile groups |
| Email claim | the claim carrying the address, which is also the account name | email |
| Name claim | the display name | name |
| Groups claim | the list of groups | groups |
| Group → role | mapping provider groups to SimpleTwo roles | empty |
| Default role | the role for anybody who matched no rule | empty |
Somebody whose groups match no rule does not get in: the journal records sso.no_role with
the list of groups they actually carried. That is a convenient way to build the mapping — try
signing in, look at what arrived, and fill the table. A filled "Default role" opens the door to
the whole organisation, so set it deliberately.
The flow is authorization code with state, nonce and PKCE; the ID token is checked for
signature, issuer, audience and expiry. None of that is configurable — those are the conditions
under which a token is accepted at all.
3. Profile: what comes from the directory
Besides the address, name and groups, the coordinator can carry arbitrary profile attributes — job title, department, phone, city, manager. Each field is given a source in Groups / roles → profile:
- from a claim — the value arrives from the provider and refreshes on every sign-in; the person does not edit it;
- own — the person fills it in themselves.
A field whose name or claim means "date of birth" is recognised automatically and treated specially: the year is visible only to its owner, colleagues get the day and the month. Redaction happens on the server rather than in the interface, and an unreadable value is dropped entirely rather than passed through.
The manager is better carried as a user_dn / manager_dn pair than as an address — how to set
that up in ADFS, and why, is in the ADFS article.
4. The organisation's domains
The client finds the installation by the domain of a work email: a person types
ivan@example.com, and the app works out which coordinator to talk to. Domains are listed in the
same settings, and a readiness check shows what the client sees.
A domain served by another installation does not belong here: talking between installations is federation, a separate mechanism.
5. What a person sees
- In a browser (admin console, web client) — the ordinary hop to the provider and back.
- In the app — the person types their work email and password in the app itself. The coordinator checks them with the provider (password grant) and, if the provider confirms, creates or refreshes the account. No password is stored or compared locally: a directory account has none here.
SimpleTwo does not rewrite the address. ADFS checks it against userPrincipalName by default, so
an employee with the UPN ivan@corp.local and the address ivan@example.com gets "invalid
credentials" while the password is right. The fix is on the ADFS side — Alternate Login ID,
see the ADFS article.
6. When sign-in fails
Look at the audit journal in the console, at events prefixed sso.:
| Event | Means | What to do |
|---|---|---|
sso.password_reject | the provider did not accept the credentials | check which identifier the provider actually matches (UPN or ALI) |
sso.no_role | sign-in worked, the groups produced no role; the groups that arrived are logged beside it | add the group to Group → role, or set Default role |
Two failures that look like a SimpleTwo fault and are fixed at the provider:
- No groups arrive at all. Check that the groups claim is included in the token: ADFS only puts a claim into an OIDC token when it has a registered short name, and Entra ID needs groups added to the token configuration explicitly.
- A different signing algorithm. RS256 is accepted. A provider signing with ES256 is not supported yet — it shows up as a signature check failure in the journal.
Who runs the sign-in
Applies to: SimpleTwo 0.9.285 and later.
The SSO card of the coordinator console has a Who runs the sign-in block:
- Coordinator — as described above: its own sign-in form and its own broker to the company provider.
- Directory — the browser sign-in is directory's sign-in page: the second factor, its own sign-in through the company provider. The coordinator becomes one of directory's applications and still issues the platform's tokens itself, so no other service notices the mode.
Switching to identity is possible only once the directory host has a public name and the
coordinator has reached it. The line under the switch says why not. On the switch the coordinator
registers itself on directory as the application platform and keeps its secret. In this mode no
account is created at sign-in: the person must already be in directory. Password sign-in from the
mobile and desktop clients works in both modes; directory checks the password. There is no
fallback password sign-in through the company provider in identity mode: directory checks the
password itself, through the LDAP directory too.
How integration secrets are stored
Applies to: directory newer than 0.27.1.
The secrets directory keeps for its integrations — the client secret at the company provider, the SCIM and SIEM target tokens, the SIEM client key, webhook secrets, the LDAP read account's password, the SimpleOne connection's password or token — and directory's own signing keys are stored encrypted in its database. Directory gets the key from the coordinator the same way chat and mail do and holds it in memory only; it is neither in the database nor on the host's disk. The key exists as soon as an encryption key has been created in the coordinator's console — the same one messages are sealed with; directory gets a separate key derived from it.
The current state is shown on the console's Overview, on the Directory setup card, and in /healthz
(secrets_sealed):
- integration secrets sealed — all is well;
- not sealed: no encryption key — the installation has no encryption key or the coordinator is unreachable; secrets are stored as entered, as before this version;
- sealed, but the key is not held — directory has not received the key after a restart. New secrets are refused until the key arrives: directory will not write plaintext beside ciphertext.
Secrets saved earlier in plaintext are encrypted by themselves as soon as the key arrives.
The second factor and the address books
Applies to: directory newer than 0.28.0.
The same key seals every person's second-factor (TOTP) secret and the content of the address
books — personal ones and the organisation's shared book: a contact's address or number, label
and note. Recovery codes and app passwords were always stored as hashes only, and passkeys as
their public half only; they need no sealing. People notice nothing: signing in with a code,
the books, sync, and vCard and CSV import and export work as before. Secrets and contacts saved
earlier, deleted contacts included, are sealed when the key first arrives; phones do not
download the books again because of it. /healthz lists what is sealed in secrets.covers.
Recovery from the key sheet
With no coordinator, but a dump of the directory database and the printed recovery key sheet, check that the sheet opens it:
identity recover --key "the key from the sheet" --key-id kek-1 --dsn "identity database connection string"
This is a dry run: the database is not changed. The command prints a report per kind of data — how many values are sealed, how many opened, how many are stored in plaintext — and exits with 0 only if everything opened. If the coordinator was rebuilt with a new master key and a new sheet was printed, re-seal the database under it:
identity recover --key "the old sheet" --key-id kek-1 --dsn "…" \
--reseal-key "the new sheet" --reseal-key-id kek-1
Give the generation printed on the new sheet, even when it has the same name as the old one. The re-seal is recorded in the security event log; after it only the new sheet opens the database, and directory gets the key that opens it from the new coordinator. If the re-seal was interrupted, run the same command again — it finishes the rest.
Next
- ADFS: signing in by mail address and the manager from the directory
- Guests, contractors and family members — access for people who are not in the directory
- Federation between installations