Sector-ID
What it is and why it exists
When a user authenticates through ClickID, the service provider receives a NameID — a stable identifier for that user. The naive approach would be to use the user's internal database ID or email address as the NameID. This would work, but it creates a privacy problem: every SP that receives the same user ID can correlate users across services.
The Sector-ID is ClickID's solution. Each SP receives a different identifier for the same user. Two SPs cannot determine whether they are serving the same person by comparing NameID values — even if they collude.
This mirrors the privacy model used by the Dutch DigiD BSN-derived sector-specific pseudonyms, but implemented on open infrastructure with an auditable algorithm.
The algorithm
The sector-ID is computed by the custom Sector-ID Mapper SPI (a Java extension to Keycloak):
sectorId = BASE64URL(HMAC-SHA256(userId + "|" + spEntityId, pepper))[:24]
Breaking this down:
| Component | Description |
|---|---|
userId | Keycloak's internal UUID for the user |
spEntityId | The SAML entity ID of the service provider (e.g. https://yourapp.example.com/saml/metadata) |
| | A literal pipe character used as a separator |
pepper | A server-side secret (never sent to any client); see below |
HMAC-SHA256(...) | HMAC with SHA-256 as the hash function, producing 32 bytes |
BASE64URL(...) | Standard base64url encoding (RFC 4648 §5) — URL-safe, no padding |
[:24] | Take the first 24 characters of the base64url-encoded output |
The result is a 24-character, URL-safe string. Example: aB3kXmQ9nL7rPwT2vYcZ0s4u
A worked example (pseudocode)
import hmac, hashlib, base64
user_id = "f7a3b912-4c1e-4d9a-8b3c-2e5f0a1d6c8b"
sp_entity = "https://yourapp.example.com/saml/metadata"
pepper = b"your-server-side-secret-here"
message = (user_id + "|" + sp_entity).encode("utf-8")
digest = hmac.new(pepper, message, hashlib.sha256).digest()
sector_id = base64.urlsafe_b64encode(digest).decode("ascii")[:24]
print(sector_id) # e.g. "aB3kXmQ9nL7rPwT2vYcZ0s4u"
Properties
| Property | Explanation |
|---|---|
| Deterministic | The same (userId, spEntityId, pepper) always produces the same sector-ID. Stable across sessions, across time, across cluster nodes. |
| Unique per (user, SP) | Different spEntityId values produce different outputs, even for the same user. |
| Not reversible | Given only a sector-ID, you cannot recover the userId or any other user attribute. HMAC is a one-way function. |
| Not correlatable | Given two sector-IDs from different SPs, you cannot determine if they belong to the same user (without the pepper). |
| URL-safe | 24 characters from the base64url alphabet — safe to use in URLs, query strings, and JSON without encoding. |
| Fixed length | Always exactly 24 characters. Suitable for fixed-length database columns. |
The pepper
The pepper is a symmetric secret key used as the HMAC key. It is:
- Server-side only — never sent to any client or SP
- Identical across all cluster nodes — must be shared via environment variable or Kubernetes secret
- The same for all users and all SPs within a single realm
- Realm-specific in practice — the sandbox and live realms each have their own pepper (by convention; the Sector-ID Mapper reads from a single env var, and each realm deployment should use a different value)
Configuring the pepper
The pepper is passed to the Sector-ID Mapper SPI via the environment variable SECTOR_ID_PEPPER (mapped to KC_SPI_SECTOR_ID_PEPPER in the Keycloak SPI configuration):
# In .env or your container environment
SECTOR_ID_PEPPER=your-random-32-byte-base64-value
Generate a secure pepper with:
openssl rand -base64 32
In Keycloak's mapper configuration (in the realm JSON), the value is referenced as:
${env.KC_SPI_SECTOR_ID_PEPPER}
Keycloak resolves this at runtime from the environment variable.
Pepper rotation warning
If you change the SECTOR_ID_PEPPER after users have authenticated, every user's sector-ID changes. From every SP's perspective, every user appears to be a new, unknown person on their next login.
This will break all SP user-account associations. You will need to coordinate with every SP to re-link their user records.
There is no automated migration path. Treat the pepper as permanent once users have logged in.
If you absolutely must rotate the pepper (e.g. key compromise):
- Notify all registered SPs well in advance.
- Provide SPs with a migration window — both old and new sector-IDs should be accepted.
- Update the pepper.
- Provide SPs with a way to correlate old and new sector-IDs (this requires a one-time secure export; consult the ClickID admin documentation).
How multiple cluster nodes share the pepper
In a Kubernetes deployment, the pepper is stored in a Kubernetes Secret and mounted as an environment variable in all Keycloak pods:
# In the Helm values or your own manifest
keycloak:
sectorIdPepper: "your-secret-here" # stored in a K8s Secret
The Helm chart creates the Secret and injects it via envFrom. All replicas share the same pepper, ensuring consistent sector-ID derivation regardless of which pod handles the SAML request.
Verify consistency across nodes by checking that the same user gets the same sector-ID across multiple logins (even if different pods handled each request).