Skip to main content

Identity providers

Each organization brings its own providers. TagShape supports two.

ProviderEnum value
Microsoft Entra IDEntra
GoogleGoogle

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.

POST /organizations/identityproviders/check
{
"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.