Identity providers
Each organization brings its own providers. TagShape supports two.
| Provider | Enum value |
|---|---|
| Microsoft Entra ID | Entra |
Google |
The value is stored and sent by name, not by number, so both a database column and a JSON payload read for themselves. The converter is attached to the type rather than left to each service's JSON options — being sent by name is part of what the type is, and not a decision three services get to make separately. Numbers are still accepted on the way in, so anything written before this still binds.
Configuring a provider
A provider record on an organization holds the client ID, the client secret, and — for Entra — the tenant ID.
Configure them through the management UI's organization page, or via PUT /organizations/{id}.
The login page then offers whichever providers the organization has configured, fetched by the anonymous
GET /organizations/{id}/signin along with the organization's name, logo and custom CSS.
Testing credentials before saving
POST /organizations/identityproviders/check puts a set of credentials to the provider they belong to without
storing anything, so an administrator finds out whether they work at configuration time rather than at somebody
else's login.
{
"provider": "Entra",
"clientId": "…",
"clientSecret": "…",
"tenantId": "…"
}
Two things about this endpoint are deliberate:
It is not under /organizations/{id}. Credentials are checked before they are saved, and an organization
still being created has no ID to check them under.
It runs server-side rather than from the browser. Entra's token endpoint allows the CORS preflight and then
omits Access-Control-Allow-Origin from the response, so a browser never receives the reply. Google would allow
it, but is asked from the server too — one way of doing it, and a client secret that never travels from a browser
to a third party.
Permissions: OrganizationRead and either OrganizationUpdate or OrganizationCreate. The attribute can
only express an and, so the either/or is settled inside the handler, which returns its own 403. See
API permissions.
How an identity maps to an account
An account is found by (provider, providerUserId) — what the provider calls the person, which does not change
when their name or address does.
When that finds nothing, the email address is tried. A match means the same person is signing in with a second provider (or with an account created before identifiers were recorded), and the new identity is attached to the account they already have rather than a second account being created.
That is the only thing an email address is still used for.
Automatic account creation
When neither lookup finds anyone, the organization's account patterns
decide. A pattern matching the address creates the account; no match refuses the login with a
403 Forbidden from POST /generatejwt, which the auth UI renders as an access-denied page rather than an error.
Profile photos
Where the provider supplies one, it is normalised and stored against the account, and compared by hash so an unchanged photo is not rewritten on every login. A photo that cannot be read is passed over — a bad picture is not a reason to refuse someone a session.
Storage caveat
Client secrets are held on the organization record as given. They are returned by GET /organizations/{id} to
holders of OrganizationRead, and by the anonymous GET /organizations/{id}/signin. Encrypting them at rest is a
known outstanding item; their present protection is that the account API is not reachable from outside the
cluster.