Skip to main content

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:

ComponentDescription
userIdKeycloak's internal UUID for the user
spEntityIdThe SAML entity ID of the service provider (e.g. https://yourapp.example.com/saml/metadata)
|A literal pipe character used as a separator
pepperA 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

PropertyExplanation
DeterministicThe 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 reversibleGiven only a sector-ID, you cannot recover the userId or any other user attribute. HMAC is a one-way function.
Not correlatableGiven two sector-IDs from different SPs, you cannot determine if they belong to the same user (without the pepper).
URL-safe24 characters from the base64url alphabet — safe to use in URLs, query strings, and JSON without encoding.
Fixed lengthAlways 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

Never change the pepper after users exist

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):

  1. Notify all registered SPs well in advance.
  2. Provide SPs with a migration window — both old and new sector-IDs should be accepted.
  3. Update the pepper.
  4. 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).