Skip to content

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 sub for one person.
  • Two clients in different sectors see different, unlinkable subjects.

A client's sector is:

  1. the host of its registered sectorIdentifierUri, when set; otherwise
  2. 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 of redirect_uri values, 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 with 400 invalid_sector_identifier. Host the JSON array at a stable https:// 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:

  1. Register a pairwise application by setting subjectType: "pairwise" (plus a sectorIdentifierUri when required).
  2. Read an application to see its current subjectType and resolved sectorIdentifier.
  3. 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 the sub every one of its users receives on their next sign-in. An application that stores sub as 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

MethodPathDescription
POST/api/v1/applicationsRegister 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

FieldWhereDescription
subjectTypecreate + patchpublic (default) or pairwise.
sectorIdentifierUricreate + patchhttps:// 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.
acknowledgeSubjectChangepatch onlySet true to confirm a sub-changing edit on an existing client. Defaults to false.

Response fields

FieldDescription
subjectTypeThe client's current subject identifier type.
sectorIdentifierThe registered sector, as a bare host. null when the sector is derived from the single redirect-URI host.

Troubleshooting

Status / errorCodeCauseFix
400 invalid_subject_typesubjectType was neither public nor pairwise.Send one of the two allowed values.
400 invalid_sector_identifierNon-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_acknowledgedA 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_foundThe 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. subjectType defaults to public; a public client's sub is 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 sectorIdentifierUri on a pairwise client makes product-api dereference it (OIDC Core §8.1) to confirm every registered redirect_uri is listed. That outbound fetch targets a tenant-supplied URL, so it goes through OutboundUrlGuard before 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 ADR 2026-08-03-sector-identifier-uri-verification.md.
  • The pseudonym is not an authenticator. A pairwise sub is a stable, one-way, per-sector pseudonym derived with a server-held key; token integrity is still carried by the token signature. See ADR 2026-07-20-pairwise-subject-projection.md for the derivation and key custody.
  • Cross-tenant is 404, never 403. Consistent with every other application endpoint.