Skip to content

Volume ITechnology

11.25 Enterprise Identity & SSO Platform ​

Enterprise Identity and Single Sign-On (SSO) platforms are the gatekeepers of modern corporate networks. They solve a simple problem: how do you let employees use hundreds of SaaS applications without creating hundreds of passwords, while maintaining centralized control over who has access to what?

The Core Identity Problem ​

Imagine an enterprise with 10,000 employees. Every day, they use Salesforce, Slack, AWS, and internal tools. If every tool manages its own credentials:

  1. Security Risk: Employees reuse passwords or write them down.
  2. Onboarding/Offboarding Nightmare: IT has to manually create or revoke 100 accounts when an employee joins or leaves.
  3. Compliance Failure: No central audit trail of who accessed what and when.

An Identity Provider (IdP) centralizes this. Applications (Service Providers, or SPs) delegate authentication to the IdP.

Requirements ​

The platform here is an identity provider sold as a service: 2,000 customer organisations and 10 million users, signing in to the SaaS applications their employers have connected.

Functional. Sign users in with a password, a passkey and multi-factor authentication. Act as the identity provider for SAML and OIDC applications. Receive joiners, movers and leavers from each customer's HR system, and push accounts out to applications with SCIM. Manage sessions, including "sign this person out everywhere". Let each organisation set policy (who must use MFA, from which networks). Record everything for audit.

Non-functional, with numbers.

  • 99.99% availability, which is about 53 minutes of downtime a year (365.25 days × 24 × 60 × 0.0001). When the identity provider is down, nobody at 2,000 companies can open any application.
  • Sign-in completes within 500 ms at p95, not counting the time the person spends typing.
  • A leaver loses access everywhere within 5 minutes of HR marking them terminated.
  • Audit records are kept for at least a year and exported to each customer on request.

Out of scope today: the admin console, and customer-specific risk-scoring models.

The clarifying questions, and what each answer changes ​

"Is the identity provider the source of truth for people, or is HR?" Usually HR. Then the IdP must not let admins create employees by hand without an HR record, or the two drift and leavers keep accounts.

"Which applications support SCIM, and which only accept a SAML login?" This is the question that decides whether "remove access within 5 minutes" is even possible. For an application without SCIM, disabling the user in the IdP stops new sign-ins, but an application session that is already open keeps working until it expires, sometimes for days. Those applications need short session lifetimes negotiated, or they are listed as a known gap.

"How long do sessions last, and must revocation be instant?" Long sessions mean fewer sign-ins and less load. Instant revocation means every application checks back with the IdP, which means more load. The answer sets both the session-store size and the token lifetime.

"Must customer data stay in a particular region?" If yes, organisations are placed in regional cells, and a user's session never leaves their region.

"Do we store passwords at all, or pass sign-in through to the customer's own directory?" Federating to the customer's Active Directory removes our password storage but makes their directory part of our availability.

Estimation ​

The 9 a.m. rush. The largest time-zone block holds about 40% of users, 4 million people. If 60% of them sign in within the same 30 minutes, that is 2.4 million sign-ins in 1,800 seconds: about 1,300 sign-ins a second. What that forces: see the next line; this rate is cheap for everything except one component.

Password hashing is the bottleneck, deliberately. A password hash such as Argon2id is tuned to cost about 100 ms of CPU, so that an attacker with a stolen database cannot test billions of guesses. At 1,300 sign-ins a second that is 1,300 × 0.1 = 130 CPU cores busy doing nothing but hashing. What that forces: the authentication engine is sized and autoscaled on CPU, scaled up before each region's morning, and protected by per-account rate limits so an attacker's guessing cannot consume that capacity. It is also a quiet argument for passkeys, where the server checks a signature in well under a millisecond instead of computing a deliberately slow hash.

Signing in to applications. Each user opens about 8 applications a day: 80 million SSO sign-ins a day ÷ 100,000 seconds = 800 a second, perhaps 4,000 a second in the morning peak. Each one signs a SAML assertion or an ID token, which costs well under a millisecond of CPU with an RSA-2048 or ECDSA key. What that forces: signing is cheap next to password hashing; the real constraint is the session lookup that comes with every one of those 4,000 requests.

Session storage. 10 million users × about 1.5 live sessions × about 1 KB = 15 GB per copy. What that forces: sessions fit in memory in a small Redis cluster per region, replicated across regions so a regional failure does not sign everyone out.

Leavers. About 15% of 10 million users leave each year: 1.5 million, about 4,100 a day. Each has about 20 assigned applications: 82,000 SCIM calls a day, roughly one a second. What that forces: throughput is trivial. The difficulty is reliability against 20 different third-party APIs, each sometimes down, within a 5-minute promise. That makes it a durable queue per application with retries and an alarm, not a fast path.

API ​

The identity provider speaks protocols that other people defined, so most of its API is not ours to design.

http
GET /.well-known/openid-configuration      → issuer, endpoints, supported algorithms
GET /oauth2/jwks                           → the public keys applications use to check our tokens

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fcrm.example%2Fcallback&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

→ 200 OK
{ "access_token": "eyJ…", "id_token": "eyJ…", "token_type": "Bearer",
  "expires_in": 3600, "refresh_token": "8xLOxBtZp8" }

→ 400 Bad Request
{ "error": "invalid_grant", "error_description": "Authorization code has expired" }
http
GET /saml/app_7f2/sso?SAMLRequest=fZJdT4MwFIb…&RelayState=%2Fdashboard
→ after sign-in, an auto-submitting form that posts SAMLResponse to the application's ACS URL

Our own administrative and lifecycle API:

http
POST /scim/v2/Users                         (called by the HR connector)
{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "priya.sen@kk.example", "externalId": "HR-20931", "active": true }
→ 201 Created  { "id": "u_19c2", … }

PATCH /scim/v2/Users/u_19c2
{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [ { "op": "replace", "path": "active", "value": false } ] }
→ 200 OK                                    (deprovisioning starts here)

DELETE /admin/users/u_19c2/sessions
→ 202 Accepted  { "revokedSessions": 3, "applicationsNotified": 12 }

Protocol endpoints use the protocol's error format, not ours. OAuth defines {"error": "invalid_grant"} and every OAuth client library expects exactly that shape. Wrapping it in a house error envelope would break every integration. The platform's own envelope (code, message, requestId) is for the admin API only.

The token request carries a code_verifier. That is PKCE: the application proves it is the same one that started the sign-in, which stops a stolen authorisation code from being exchanged by someone else (8.4.3).

Deprovisioning is a PATCH to active: false, not a delete. Deleting the user loses the audit trail and the link to their HR record; deactivating keeps both and triggers the same downstream removal.

Data model ​

orgs
  id             UUID PRIMARY KEY
  cell, region   TEXT NOT NULL            -- where this organisation's data lives
  policy         JSONB NOT NULL           -- MFA rules, allowed networks, session length

users
  org_id, id     UUID                     -- PRIMARY KEY (org_id, id)
  username       TEXT NOT NULL            -- UNIQUE (org_id, username)
  external_id    TEXT                     -- the HR system's id
  status         TEXT NOT NULL            -- active | suspended | deprovisioned

credentials
  user_id        UUID, type TEXT          -- password | passkey | totp
  secret         BYTEA                    -- Argon2id hash, or a passkey's public key

apps
  org_id, id     UUID
  protocol       TEXT                     -- saml | oidc
  client_id, redirect_uris, acs_url, entity_id
  scim_base_url  TEXT NULL                -- NULL means "no SCIM: deprovisioning is login-only"

assignments      org_id, user_id, app_id  -- PRIMARY KEY (org_id, user_id, app_id)

signing_keys
  id, algorithm, public_jwk
  private_ref    TEXT                     -- a handle in the KMS; the key never leaves it
  status         TEXT                     -- next | active | retired

provisioning_jobs
  id, org_id, app_id, user_id, op         -- create | update | deactivate
  attempts INT, next_attempt_at TIMESTAMPTZ, status TEXT

sessions (Redis)
  session:{sid}        → user, org, auth_time, methods used ("pwd", "mfa"), expires_at
  user:{id}:sessions   → set of sids, for "sign out everywhere"

Access patterns:

QueryFrequencyReturns
Find a user by organisation and username1,300/s peakone row
Look up a session by id4,000/s peakone key
All sessions of one userrare (logout, leaver)a few keys
An application's configuration by client id4,000/s peak (cached)one row
Due provisioning jobsevery few secondsup to hundreds

Partition by org_id, grouped into cells. Every query belongs to one organisation, so an organisation and everything it owns live in one cell, and a bad deploy or overload in one cell touches only its organisations.

signing_keys.status = next exists so keys can be rotated without an outage. The next key is published in the JWKS days before it is used, so every application has already cached it when tokens start being signed with it.

apps.scim_base_url being NULL is recorded on purpose, so the 5-minute leaver promise can be reported honestly per application.

Federated Authentication Protocols ​

The two heavyweights in enterprise identity are SAML 2.0 (Security Assertion Markup Language) and OIDC (OpenID Connect).

SAML 2.0: The Enterprise Heavyweight ​

SAML is an XML-based protocol. It is older, robust, and the standard for enterprise SSO.

How SAML Works (SP-Initiated Flow)

BrowserService Provider (SP)Identity Provider (IdP)Attempts to access resourceRedirects to IdP with SAML AuthnRequestFollows redirect, lands on login pagePrompts for credentials + MFASubmits credentialsValidates user, generates SAML AssertionReturns HTML form with SAMLResponse & ACS URLBrowser auto-submits form (POST) to SPValidates IdP signature, extracts identityGrants access (Sets session cookie)
SAML 2.0 SP-Initiated Authentication Flow

The SAML Assertion: The core of SAML is the assertion—a signed XML document containing the user's identity (NameID) and attributes (Roles, Department). The SP trusts the assertion because it is cryptographically signed by the IdP's private key.

OIDC: The Modern Alternative ​

OIDC sits on top of OAuth 2.0. Instead of XML, it uses JSON Web Tokens (JWTs). It is lighter and preferred for modern web and mobile apps.

BrowserRelying Party (App)Identity ProviderAccess appRedirect to IdP with client_id, scopes (openid)Login & ConsentRedirect back to SP with Auth CodeDelivers codeBack-channel POST code + client_secretReturns ID Token (JWT) & Access TokenValidates ID TokenGrants access
OIDC Authorization Code Flow

Architecture of an Identity Platform ​

To build an Identity Platform, we need several interacting components.

User BrowserAPI Gateway / WAFProtocol HandlersSAML / OIDCAuthentication EngineSession ManagerCore Directory / GraphPolicy EngineMFA, Risk, RBACAudit & Analytics Log
Architecture of an Identity Platform
  1. Protocol Handlers: Translate incoming SAML AuthnRequests or OIDC Auth requests into a standard internal authentication context.
  2. Authentication Engine: Orchestrates the login flow (password, WebAuthn/Passkeys).
  3. Policy Engine: Determines if the user should be allowed in based on context (IP, device posture, time of day) and dictates if MFA is required.
  4. Session Manager: Tracks active sessions, handles token revocation, and global sign-out.
  5. Core Directory: The source of truth for users, groups, and attributes.
  6. Audit: High-throughput immutable logging for compliance (SOC2, HIPAA).

Deep Dive: Directory Sync and Provisioning (SCIM) ​

Authentication is only half the battle. If a user is deleted from the IdP, they cannot log in, but their account still exists in the SP (taking up a license).

SCIM (System for Cross-domain Identity Management) is an HTTP-based protocol for automating user provisioning.

When HR marks an employee as terminated in Workday (the HRIS), the system propagates the change:

HR SystemWorkdayIdentity PlatformApp 1 (Salesforce)App 2 (Slack)App 3 (AWS IAM)Webhook/PollSCIM POST /UsersSCIM PUT /Users/123SCIM DELETE /Users/456
SCIM Provisioning Pipeline

Building the SCIM Provisioning Pipeline ​

Here is a simplified TypeScript implementation of a provisioning dispatcher that handles asynchronous updates to downstream systems.

typescript
// types.ts
export interface UserContext {
  id: string;
  email: string;
  status: 'ACTIVE' | 'SUSPENDED';
  groups: string[];
}

export interface AppIntegration {
  appId: string;
  type: 'SCIM' | 'GRAPH_API';
  credentials: { baseUrl: string; token: string };
  attributeMapping: Record<string, string>; // e.g. { "email": "urn:ietf:params:scim:schemas:core:2.0:User:userName" }
}

// scim-client.ts
import axios from 'axios';

export class ScimClient {
  constructor(private baseUrl: string, private token: string) {}

  async provisionUser(user: UserContext, mapping: Record<string, string>): Promise<void> {
    // Transform internal user to SCIM payload
    const scimPayload: any = {
      schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
      active: user.status === 'ACTIVE'
    };

    for (const [internalAttr, scimAttr] of Object.entries(mapping)) {
      if (internalAttr === 'email') {
        scimPayload.userName = user.email; // SCIM often uses userName as the unique identifier
        scimPayload.emails = [{ primary: true, value: user.email }];
      }
      // ... handle other attributes
    }

    try {
      await axios.post(`${this.baseUrl}/Users`, scimPayload, {
        headers: {
          'Authorization': `Bearer ${this.token}`,
          'Content-Type': 'application/scim+json'
        }
      });
    } catch (error) {
      if (error.response?.status === 409) {
        // Conflict - user already exists. We should fall back to PUT/PATCH
        await this.updateUser(user, mapping);
      } else {
        throw new Error(`Provisioning failed: ${error.message}`);
      }
    }
  }

  async updateUser(user: UserContext, mapping: Record<string, string>): Promise<void> {
    // SCIM lookup by username, then PUT or PATCH
    // ... implementation omitted for brevity
  }
}

// provisioning-dispatcher.ts
export class ProvisioningDispatcher {
  async handleUserStatusChange(user: UserContext, assignedApps: AppIntegration[]) {
    // Fan-out updates to all downstream apps concurrently
    const promises = assignedApps.map(async (app) => {
      if (app.type === 'SCIM') {
        const client = new ScimClient(app.credentials.baseUrl, app.credentials.token);
        try {
          await client.provisionUser(user, app.attributeMapping);
          // Log success to audit
          await this.logAudit('PROVISION_SUCCESS', user.id, app.appId);
        } catch (err) {
          // Push to dead-letter queue for retry
          await this.logAudit('PROVISION_FAILED', user.id, app.appId, err.message);
          await this.enqueueRetry(user, app);
        }
      }
    });

    await Promise.allSettled(promises);
  }

  private async logAudit(action: string, userId: string, targetId: string, error?: string) {
    // Write to Kafka or Kinesis for immutable audit logging
    console.log(`AUDIT: ${action} | User: ${userId} | Target: ${targetId} | Err: ${error}`);
  }

  private async enqueueRetry(user: UserContext, app: AppIntegration) {
    // SQS or similar queue for delayed retry
  }
}

Session Management and Scaling ​

Identity systems are extremely read-heavy. Validating sessions and checking policies happen on almost every request in a zero-trust architecture.

Global Session Store ​

When an IdP authenticates a user, it issues a session cookie for the IdP domain. This allows seamless SSO into subsequent applications.

The session store must be globally distributed, highly available, and low latency. Redis (e.g., Redis Enterprise or ElastiCache with Global Datastore) is the industry standard.

BrowserIdP US EastIdP EU WestRedis US RegionRedis EU RegionIdP Session CookieIdP Session CookieAsync Replication
Global Session Store Replication

High Availability and Disaster Recovery ​

If the IdP goes down, the entire company stops working. Availability targets are typically 99.99% or higher.

  • Active-Active Deployments: Run across multiple geographic regions simultaneously.
  • Cell-based Architecture: Isolate tenants into "cells." If an update breaks a cell, it only impacts a subset of customers.
  • Circuit Breakers: If the Policy Engine (which might call out to a third-party risk API) slows down, fail open or fail closed depending on the security posture, but do not let it cascade and crash the Auth Engine.

Scale and failure ​

What breaksBlast radiusHow you find outWhat keeps it runningRecovery
A region goes downits organisations, if not replicatedhealth checks; error rate by regionactive-active cells, replicated sessionstraffic moves; sessions survive
Session store losteveryone signed out at oncesession-miss rate jumpsreplication; capacity for a re-login stormthe 130-core hashing budget is what absorbs it
Signing key compromisedevery token it signedKMS alerts; security reportthe next key already publishedswitch to it; retire the old key; revoke sessions
An application's SCIM API downleavers keep that appprovisioning job age past 5 minutesdurable per-app queue with retriesjobs drain when it returns; alert security meanwhile
HR sends a mass termination by mistakethousands locked outdeprovision rate far above normala brake: hold if over ~1% of an org in an houra human releases or discards the batch
Clock skew between serversSAML assertions rejected"not yet valid" error ratea short allowed skew windowfix time sync

The mass-termination brake is the one people forget. An HR import bug that marks every employee as terminated is a real event, and an identity provider that obeys instantly turns one bad file into a company-wide outage. Deprovisioning should be fast for one person and deliberately slow for ten thousand.

What the interviewer will push on ​

  1. Clock Skew in SAML: "How do you handle a SAML assertion that the SP rejects because it says it was issued in the future?"
    • Answer: Time sync is critical. Implement a NotBefore and NotOnOrAfter window with a grace period (e.g., +/- 3 minutes) when generating or validating assertions to account for clock skew between the IdP and SP servers.
  2. IdP-Initiated vs SP-Initiated SAML: "Which is more secure and why?"
    • Answer: SP-initiated is generally more secure. In IdP-initiated flows, the SP receives an unsolicited SAML Response. This is vulnerable to CSRF-like attacks (stolen assertions being replayed) because the SP didn't generate a request ID (InResponseTo) to map the response back to a specific browser session state.
  3. SCIM Failure Handling: "Workday sends a webhook to terminate an employee. The SCIM call to AWS IAM fails. What happens?"
    • Answer: The system must not drop the event. It should be written to a persistent message queue (like Kafka or SQS) before acknowledging the webhook. If the SCIM call fails, it goes into a retry loop with exponential backoff. The audit log must record the failure so security teams can intervene manually if needed.
  4. Handling Massive Spikes (The 9 AM Login Rush): "How does the system handle everyone logging in at the exact same time?"
    • Answer: Cache heavily. Read paths (user lookups, app configurations) should hit read replicas or distributed caches (Memcached/Redis). The password hashing algorithm (like Argon2 or bcrypt) is CPU intensive by design; auto-scaling groups for the Auth Engine must scale on CPU utilization well ahead of the spike.

Recall

  • SAML 2.0: XML-based, signed assertions, heavy rely on back-and-forth browser redirects.
  • OIDC: JSON Web Tokens (JWT), uses OAuth 2.0 underlying mechanisms, lighter weight.
  • SCIM: JSON REST API standard for provisioning/deprovisioning users and groups across systems.
  • Session Store: Must be distributed and ultra-fast (Redis). A failure here logs everyone out.
  • Resiliency: Identity platforms require cell-based, active-active architectures due to their status as a single point of failure for all corporate tools.

Quiz ​

  1. Why is a standard like SCIM necessary alongside SAML/OIDC?
  2. In a SAML SP-Initiated flow, how does the SP know the SAML assertion is actually from the trusted IdP?
  3. Why is an IdP-Initiated SAML flow susceptible to CSRF attacks?
  4. When handling a 9 AM login spike, which component of the IdP architecture is most likely to bottleneck on CPU, and why?
Answers 1. SAML/OIDC handle *authentication* (who are you) at the moment of login. SCIM handles *lifecycle management* (provisioning/deprovisioning). Without SCIM, you would have to manually create accounts in the SP before the user could SSO, and manually delete them when they leave to free up licenses. 2. The IdP signs the SAML Assertion (or the entire SAML Response) using its private key. The SP validates this signature using the IdP's public key, which was exchanged during initial configuration (often via metadata URL). 3. Because the SP receives an unsolicited assertion without having initiated a request, it cannot correlate the login to a specific, active browser session state using an `InResponseTo` ID. An attacker could potentially steal an assertion and post it to the SP. 4. The Authentication Engine, specifically the password verification step. Modern password hashes (bcrypt, Argon2) are intentionally designed to be slow and CPU-intensive to thwart brute-force attacks.