Product documentation
Pairwise subject identifiers (pseudonymous per-application sub)
Configure an OAuth client to receive a per-sector pseudonymous sub (OIDC Core 8.1 pairwise) so the same person is uncorrelatable across unrelated applications, and understand why switching an existing client re-keys its users.
Pairwise subject identifiers
The sub claim identifies the signed-in user to your application. By default it is a
public identifier: every application in your tenant sees the same sub for a given
person, so two applications could compare notes and discover they are talking to the same
human.
A pairwise application receives a per-sector pseudonymous sub instead
(OpenID Connect Core 1.0, §8.1).
The same person gets a different, unlinkable sub in each unrelated sector, so two
applications in different sectors cannot correlate their users by subject. Thoryn's internal
join key is unchanged — the person is still one user across your workspace — only the
outward sub is projected.
This is a per-client setting on the standard application-management surface. The storage and the token-mint projection already exist in the hub; this guide is how a tenant admin turns it on for a client.
When to use it
- You operate several unrelated relying parties and do not want them able to correlate a
shared user by
sub. - A privacy or regulatory requirement calls for pseudonymous, non-correlatable subject identifiers per application.
If you want one stable identifier across your apps (e.g. a shared account system keyed on
sub), keep the default public — that is the correct choice and needs no configuration.
Sectors
A sector is the scope within which the pairwise sub is stable:
- Two clients in the same sector see the same
subfor one person. - Two clients in different sectors see different, unlinkable subjects.
A client's sector is:
- the host of its registered
sectorIdentifierUri, when set; otherwise - the single host of its
redirectUris.
A pairwise client whose redirect URIs span more than one host (or which has no
redirect URI with a host) has no single well-defined sector, so it must register a
sectorIdentifierUri. This is enforced at registration (a 400), not deferred to a
failed login.
When you register a
sectorIdentifierUri, Thoryn fetches it and verifies its contents per OIDC Core §8.1: the URL must return a JSON array ofredirect_urivalues, and every redirect URI you registered must appear in it. The fetch runs through the platform SSRF guard (OutboundUrlGuard) — a URL resolving to a private, loopback, link-local, or cloud-metadata address is rejected with no request made — and the check fails closed: if the document cannot be retrieved, is not a JSON array, or omits one of your redirect URIs, registration is rejected with400 invalid_sector_identifier. Host the JSON array at a stablehttps://URL the sector controls (Thoryn caches it briefly, TTL 300s).
For end users
A tenant administrator (or a machine client holding tenant:applications.write) can:
- Register a pairwise application by setting
subjectType: "pairwise"(plus asectorIdentifierUriwhen required). - Read an application to see its current
subjectTypeand resolvedsectorIdentifier. - Switch an existing application to (or between) pairwise — an identity-affecting change that requires explicit acknowledgement (see the warning below).
Register a new pairwise application
curl -sS -X POST https://api.stg.thoryn.org/api/v1/applications \
-H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Reporting Portal",
"redirectUris": ["https://reporting.example.com/callback"],
"grantTypes": ["authorization_code", "refresh_token"],
"subjectType": "pairwise"
}'Because this client has a single redirect host, the sector is
reporting.example.com automatically — no sectorIdentifierUri needed. The response echoes
the resolved setting:
{
"clientId": "app-3f9c1a2b7d4e",
"clientSecret": "shown-once-…",
"subjectType": "pairwise",
"sectorIdentifier": "reporting.example.com"
}When a client's redirect URIs span multiple hosts, register the sector explicitly:
curl -sS -X POST https://api.stg.thoryn.org/api/v1/applications \
-H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Multi-host App",
"redirectUris": [
"https://eu.example.com/callback",
"https://us.example.com/callback"
],
"grantTypes": ["authorization_code"],
"subjectType": "pairwise",
"sectorIdentifierUri": "https://apps.example.com/sector.json"
}'Every client in that same sector (apps.example.com) sees the same sub for one person;
a client in any other sector sees a different one.
Switch an existing application — this re-keys its users
Warning — identity-affecting. Changing an existing client's
subjectType, or moving a pairwise client to a different sector, changes thesubevery one of its users receives on their next sign-in. An application that storessubas its account key will no longer recognise its existing users unless it can re-link them. Only do this on a client whose users you can migrate.
To make Thoryn perform the change you must set acknowledgeSubjectChange: true. Without it,
the request is refused with 409 subject_change_not_acknowledged and nothing changes:
curl -sS -X PATCH https://api.stg.thoryn.org/api/v1/applications/app-3f9c1a2b7d4e \
-H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subjectType": "pairwise",
"sectorIdentifierUri": "https://apps.example.com/sector.json",
"acknowledgeSubjectChange": true
}'Re-sending the value a client already has (for example, patching only its display name while
passing the unchanged subjectType) is a no-op and needs no acknowledgement.
Endpoint reference
| Method | Path | Description |
|---|---|---|
POST | /api/v1/applications | Register a client; accepts subjectType + sectorIdentifierUri. |
GET | /api/v1/applications/{clientId} | Read a client; returns subjectType + sectorIdentifier. |
PATCH | /api/v1/applications/{clientId} | Change a client; an identity-affecting subject change needs acknowledgeSubjectChange: true. |
Request fields
| Field | Where | Description |
|---|---|---|
subjectType | create + patch | public (default) or pairwise. |
sectorIdentifierUri | create + patch | https:// URL whose host is the sector. Required for a pairwise client whose redirect URIs do not share one host. When supplied, it is fetched and verified per OIDC Core §8.1 (must return a JSON array of redirect_uri values that includes every registered redirect URI), guarded against SSRF and failing closed. |
acknowledgeSubjectChange | patch only | Set true to confirm a sub-changing edit on an existing client. Defaults to false. |
Response fields
| Field | Description |
|---|---|
subjectType | The client's current subject identifier type. |
sectorIdentifier | The registered sector, as a bare host. null when the sector is derived from the single redirect-URI host. |
Troubleshooting
Status / errorCode | Cause | Fix |
|---|---|---|
400 invalid_subject_type | subjectType was neither public nor pairwise. | Send one of the two allowed values. |
400 invalid_sector_identifier | Non-https / host-less / malformed sectorIdentifierUri; a sector on a public client; a pairwise client with multiple redirect hosts and no sector; or (OIDC Core §8.1 content check) the sector document could not be fetched, was not a JSON array, omitted one of your redirect URIs, or resolved to a blocked (private/loopback/metadata) address. | Supply a valid https:// sector URL, reachable over the public internet, hosting a JSON array of redirect_uri values that includes every redirect URI you registered. |
409 subject_change_not_acknowledged | A PATCH would change the sub of an existing client's users, without acknowledgeSubjectChange: true. | Confirm you can migrate the client's users, then re-send with acknowledgeSubjectChange: true. |
404 not_found | The client does not exist for your tenant (cross-tenant access looks identical to non-existent). | Use a client your tenant owns. |
Security notes
- Public clients are unchanged.
subjectTypedefaults topublic; a public client'ssubis exactly what it was before this feature existed. There is no silent migration. - The sector document is fetched and verified — through the SSRF guard. Registering a
sectorIdentifierUrion a pairwise client makes product-api dereference it (OIDC Core §8.1) to confirm every registeredredirect_uriis listed. That outbound fetch targets a tenant-supplied URL, so it goes throughOutboundUrlGuardbefore dispatch: URLs resolving to private, loopback, link-local, or cloud-metadata addresses are rejected with no request made, on every fetch (including cache re-validation). The check fails closed — an unreachable, malformed, or mismatched document blocks the registration rather than provisioning an unverified pairwise client. See ADR2026-08-03-sector-identifier-uri-verification.md. - The pseudonym is not an authenticator. A pairwise
subis a stable, one-way, per-sector pseudonym derived with a server-held key; token integrity is still carried by the token signature. See ADR2026-07-20-pairwise-subject-projection.mdfor the derivation and key custody. - Cross-tenant is
404, never403. Consistent with every other application endpoint.