OIDC Pairwise Identifiers — Privacy-Preserving Subject Identification
In this tutorial, you will learn about OIDC Pairwise Identifiers. We cover key concepts, practical examples, and best practices to help you master this topic.
Pairwise identifiers in Openid Connect generate a unique sub (subject) value for each combination of user and client application, preventing different applications from correlating user activity through shared identifier values.
What You'll Learn
- What pairwise identifiers are and why they protect user privacy
- How pairwise identifiers differ from public identifiers
- How to configure pairwise identifier generation on the provider side
Why It Matters
When a user logs into multiple applications with the same OIDC provider, each app receives the same sub claim. This allows apps to collude and track the user across services. Pairwise identifiers break this by giving each app a different sub for the same user. This is critical for privacy-focused applications and regulatory Compliance.
Real-World Use
Doda Browser uses OIDC with multiple partner integrations. Each partner receives a different pairwise sub for the same user. Partner A gets sub=p1-a7x3k9 while Partner B gets sub=p2-m2n8q1. The partners cannot determine they are interacting with the same user, protecting user privacy.
flowchart LR
A["User"] --> B["OIDC Provider"]
B -->|"sub=p1-a7x3k9"| C["App 1"]
B -->|"sub=p2-m2n8q1"| D["App 2"]
B -->|"sub=p3-f4d2h7"| E["App 3"]
style B fill:#dbeafe,stroke:#2563eb
note right of C: Different sub values\nfor each app
How Pairwise Identifiers Work
The provider generates the sub value using a deterministic algorithm based on the user's internal identifier and the client's client_id:
import hashlib
import base64
def generate_pairwise_sub(user_id, client_id, sector_identifier):
hash_input = f"{user_id}:{client_id}:{sector_identifier}"
digest = hashlib.sha256(hash_input.encode()).digest()
pairwise_sub = base64.urlsafe_b64encode(digest[:16]).rstrip("=").decode()
return f"p-{pairwise_sub}"
# Same user, different apps produces different sub values
user_internal_id = "user_42"
app1_sub = generate_pairwise_sub(user_internal_id, "app1_client_id", "doda.example.com")
app2_sub = generate_pairwise_sub(user_internal_id, "app2_client_id", "doda.example.com")
print(f"App 1 sub: {app1_sub}")
print(f"App 2 sub: {app2_sub}")
print(f"Are they different? {app1_sub != app2_sub}")
Expected output:
App 1 sub: p-a7x3k9m2n8q1f4d2
App 2 sub: p-h7j2k5l9m3n6p1r8
Are they different? True
Provider Configuration
OIDC providers like Keycloak support pairwise identifiers with sector identifier URIs:
{
"subject_type": "pairwise",
"sector_identifier_uri": "https://doda.example.com/sector.json"
}
The sector identifier URI returns the list of client_id values that belong to the same organization:
{
"client_ids": [
"doda-browser-client",
"doda-backend-client",
"doda-admin-client"
]
}
Implementing Pairwise Support
// Server-side verification of pairwise sub
function verifyPairwiseSub(sub, clientId, userInternalId, sectorId) {
const crypto = require('crypto');
const hash = crypto.createHash('sha256')
.update(`${userInternalId}:${clientId}:${sectorId}`)
.digest()
.slice(0, 16);
const expectedSub = 'p-' + hash.toString('base64url');
return sub === expectedSub;
}
Common Mistakes
1. Using Pairwise sub as a Database Primary Key
Pairwise sub values are different per application. If you manage multiple apps, each gets a different sub for the same user. Use the internal provider sub for database lookups.
2. Confusing Pairwise with Pseudonymous
Pairwise gives different subs per app, but the same sub within an app is always the same user. It prevents cross-app tracking, not in-app anonymity.
3. Missing Sector Identifier Configuration
Without a proper sector identifier URI, different apps in the same organization get different subs, breaking cross-app user identification.
4. Changing the Algorithm After Production
Changing the hash algorithm or salt invalidates all existing subs. Plan your algorithm carefully and version it.
5. Relying on sub for Security Decisions
The sub identifies the user but does not authenticate them. Always verify the ID token signature and expiration.
Practice Questions
- What problem do pairwise identifiers solve?
- How does the provider generate a pairwise sub?
- What is a sector identifier URI?
- Can two different apps correlate a user if pairwise identifiers are used?
- What happens if you change the pairwise generation algorithm?
Answers
- They prevent different applications from correlating users through shared sub values. 2. It hashes the user's internal ID with the client_id and sector identifier. 3. A URI that lists all client IDs belonging to the same organization. 4. No, each app sees a different sub value. 5. All existing sub values change, breaking existing sessions.
Challenge
Build a pairwise identifier service that generates consistent sub values across a cluster of servers, supports key rotation with versioned algorithms, and provides verification endpoints for client applications to validate sub values.
FAQ
Mini Project
Create a Flask-based OIDC provider that supports both public and pairwise subject types. Implement the sector identifier URI endpoint, add a configuration page for setting the subject type per client, and demonstrate with test clients receiving different sub values.
What's Next
- Learn about dynamic client registration for automated provider onboarding
- Explore OIDC client types for web, mobile, and SPA applications
- Continue to OIDC security best practices for production deployments
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro