Identity, Authentication, and Access Management Questions
Designing and operating identity and access control systems. Covers authentication protocols and standards (OAuth, SAML, OIDC, MFA), authorization models (RBAC, ABAC), identity lifecycle and privilege management, IAM architecture and automation, and access control across cloud and on-premises environments. The 'who can do what' control plane, distinct from cryptographic key management.
Describe a secure password storage scheme for a large-scale web application that will hold millions of user accounts. Explain in detail how you would store passwords to protect against offline cracking and database leaks. Cover algorithm selection (e.g., Argon2, bcrypt), per-user salts, optional peppers, cost parameters, migration strategy for legacy hashes, and operational practices (rate-limiting login attempts, monitoring, user experience trade-offs).
Sample Answer
Direct answer
For a large-scale application, passwords should never be stored in any reversible or fast-to-compute form; they should be hashed with a modern, deliberately slow password-hashing algorithm, Argon2id (the current OWASP-recommended default) or bcrypt as a widely-supported alternative, combined with a unique, randomly generated salt per user so identical passwords never produce identical stored values, and tuned cost parameters that make each individual guess expensive enough to make large-scale offline cracking impractical even after a full database leak. A pepper (a secret value held outside the database, in application configuration or a secrets manager) adds a second layer that survives a database-only leak. The remaining, equally important half of the design is operational: a migration path that can move users off a weaker legacy hash without forcing a mass password reset, rate limiting on login attempts, monitoring for credential-stuffing patterns, and being honest about the user experience cost of a slow, secure hash.
Structured elaboration
Algorithm selection. Argon2id is the current recommended default: it is a memory-hard function, meaning it deliberately requires a configurable amount of RAM per hash computation, not just CPU time, which specifically defeats the massively parallel cracking hardware (GPUs, ASICs) that made older, memory-light algorithms crackable at enormous scale. bcrypt is an older but still acceptable and extremely widely deployed alternative, CPU-hard but not memory-hard, and remains a reasonable choice where Argon2 tooling is not yet available in a given language ecosystem. Both are deliberately slow by design, which is the entire point: a login check that takes 100 to 300 milliseconds is imperceptible to a real user logging in once, but the same cost multiplied across billions of guess attempts makes offline brute-forcing of a leaked hash database computationally expensive rather than trivial. Fast general-purpose hashes (MD5, SHA-256 used alone) must never be used for password storage, since their entire design goal, being fast, is the opposite of what password storage needs.
Per-user salts. A salt is a random value generated uniquely per user and stored alongside the hash (it does not need to be secret). Its purpose is specifically to defeat precomputed lookup tables (rainbow tables) and to ensure that two users with the same password produce completely different stored hashes, so a leak does not reveal "these accounts share a password" as a free signal, and an attacker cannot precompute a single table of common-password hashes that works against every account in the database at once. Modern password-hashing libraries (Argon2 and bcrypt implementations) generate and store the salt automatically as part of the resulting hash string, so this is rarely something an implementation has to build by hand.
Optional peppers. Unlike a salt, a pepper is a secret value, typically a single application-wide secret (or one of a small rotating set) held outside the database entirely, in application configuration, an environment variable, or a secrets manager. Its purpose is to add protection specifically against a database-only leak: an attacker who exfiltrates the password table alone (via a SQL injection vulnerability, an exposed backup, a misconfigured replica) still lacks the pepper, and cannot verify or crack the hashes without it, whereas an attacker who compromises the application server itself (and can therefore read the pepper too) is not helped by it. A pepper is a defense-in-depth layer on top of a properly salted hash, not a replacement for one.
Cost parameters. Both Argon2 and bcrypt expose tunable cost parameters (Argon2: memory size, iteration count, and parallelism; bcrypt: a work factor controlling the number of internal rounds) that should be set as high as the application's actual login-latency budget and server capacity allow, not left at a library default that may be outdated. The right approach is periodic re-benchmarking against current hardware (as attacker hardware and defender server hardware both improve over time, a cost parameter set once at launch quietly becomes weaker relative to modern cracking capability) and choosing the highest cost setting that keeps login latency within an acceptable, typically sub-second, user-facing budget.
Migration strategy for legacy hashes. This is the senior wrinkle in an otherwise well-understood staple: an application that currently stores passwords with a weaker legacy scheme (an old bcrypt work factor, or worse, an unsalted fast hash from years ago) cannot simply re-hash every stored password at once, since the plaintext is, by design, never available after the original hash was stored. The standard approach is rehash-on-login: the first time a user successfully authenticates after the migration begins, verify their password against the legacy hash as before, and if it succeeds, immediately re-hash the now-known-correct plaintext with the new algorithm and cost parameters and replace the stored value, all inside that single authenticated request. Users who log in regularly migrate transparently and gradually; users who never log in again keep their weaker hash indefinitely, which is an acceptable, bounded residual risk (their credential was never actively re-exposed, it simply was not upgraded), rather than the disruptive alternative of forcing every user to reset their password immediately, which causes real support load and account-abandonment.
Operational practices. Password hashing strength alone does not address every risk: rate limiting on login attempts (per-account and per-source-IP, with backoff and lockout thresholds) is necessary because a strong hash slows offline cracking of a leaked database but does nothing to stop a live, online guessing attack against the login endpoint itself, which needs its own defense. Monitoring should track failed-login-attempt patterns (spikes against a single account, or the same password tried across many accounts, a strong credential-stuffing signal) and alert distinctly from ordinary user error. The user-experience trade-off of a deliberately slow hash is real but small at the individual-request scale (a few hundred milliseconds added to a login that happens infrequently per user) and should be weighed against server capacity at peak login volume (a cost parameter tuned for security alone, without regard to concurrent login volume during, say, a Monday-morning traffic spike, can itself become an availability problem, which is why benchmarking cost parameters against realistic peak load, not just security ideal, matters).
Worked example
The rehash-on-login migration pattern, shown structurally (illustrative, not an executed benchmark):
def verify_and_migrate(user, submitted_password):
if user.hash_algorithm == "legacy_sha256_unsalted":
if legacy_verify(submitted_password, user.stored_hash):
# Correct password now known in plaintext for this one request only;
# immediately re-hash with the current algorithm and discard the plaintext.
user.stored_hash = argon2id_hash(submitted_password)
user.hash_algorithm = "argon2id"
save(user)
return True
return False
elif user.hash_algorithm == "argon2id":
return argon2id_verify(submitted_password, user.stored_hash)
Trace this against a concrete population: assume 1,000,000 accounts, of which 600,000 log in at least once during the three months after migration begins. Each of those 600,000 accounts transparently upgrades to Argon2id the moment they authenticate, with no visible change to the user beyond the same login flow. The remaining 400,000 dormant accounts keep the legacy hash until they next log in (which may be never, if the account has been abandoned) or until a separate, explicit decision is made to force a reset for accounts that remain on the legacy scheme past some deadline. This is a deliberate, named trade-off: gradual, transparent migration for active users versus an unresolved tail of dormant accounts, rather than a single disruptive mass reset that would affect all 1,000,000 accounts' users regardless of whether their individual risk actually changed.
Trade-offs and pitfalls
- Choosing a fast general-purpose hash (unsalted or salted MD5/SHA-256) for password storage is the single most common and most damaging mistake; the entire design goal of a password hash is to be slow and memory-hard, the opposite of what those functions were built for.
- A pepper is not a substitute for proper salting and a slow algorithm; it defends against a narrower threat (database-only leak without application-server compromise) and should be treated as an additional layer, not the primary control.
- Cost parameters set once and never revisited quietly weaken over time as attacker hardware improves; periodic re-benchmarking against current hardware should be a scheduled operational task, not a one-time launch decision.
- A forced mass password reset after discovering a legacy-hash problem causes real, measurable user friction and support load, and the rehash-on-login pattern exists specifically to avoid that cost while still making steady, real progress on migrating the actual risk.
- Hashing strength does not substitute for rate limiting and monitoring; a perfectly chosen Argon2id configuration does nothing to stop a slow, patient, low-volume online guessing attack against the live login endpoint, which is a distinct threat requiring its own defense.
Design a prevention and detection pipeline to avoid hard-coded credentials in source code repositories. Include pre-commit hooks, static analysis rules, CI secret scanning, automated creation and rotation of secrets when leaks are detected, and developer education. Mention specific tools and how you'd integrate the pipeline with GitHub/GitLab.
Sample Answer
Direct answer
Prevent hard-coded credentials with layered defenses at every stage a commit can pass through: a fast local pre-commit hook that catches most leaks before they ever leave the developer's machine, a server-side scan on every push and pull request that catches what the local hook missed or was bypassed, and an automated response (rotate the leaked credential, not just delete the line of code) for the rare leak that gets through both. Developer education matters because the hooks and scanners are the safety net, not the primary control; the primary control is developers reaching for a secrets manager reference instead of a literal string in the first place.
Structured elaboration
Layer 1: pre-commit hooks (local, fastest, first line of defense). A pre-commit hook installed via the pre-commit framework runs a secret-detection tool such as Gitleaks or detect-secrets (originally built at Yelp) against the staged diff before git commit completes. This layer is deliberately fast (only the staged changes, not the full history) and runs entirely on the developer's machine, so it catches a leak at the earliest and cheapest possible point, seconds after it was typed, before it is even committed locally. Its weakness is that it is opt-in per developer unless enforced by tooling: a developer can skip it with --no-verify, so it cannot be the only layer.
Layer 2: static analysis rules and CI secret scanning (server-side, cannot be bypassed by an individual developer). Every push and pull request triggers a server-side scan that runs regardless of whether the local hook ran. Two complementary mechanisms belong here:
- Native platform scanning: GitHub's secret scanning detects known secret formats (cloud provider keys, common API token patterns) automatically once enabled on a repository or organization, and GitHub's push protection goes a step further by blocking the push itself in real time when a recognized secret pattern is detected, before it ever lands in the remote's history at all. GitLab's equivalent is Secret Detection, run as a CI/CD job (via GitLab's built-in
Secret-Detectiontemplate) against each merge request's diff. - Custom static analysis: because native scanners only recognize well-known secret formats, a supplementary CI job running Gitleaks or TruffleHog with a custom rule set (regexes tuned to the organization's own internal token formats, such as an internal service's API key prefix) catches what the native scanner does not know to look for. This is the "static analysis rules" the question asks for: a maintained, versioned rule set specific to the organization, not just the generic patterns a vendor ships by default.
- Both are wired as a required CI status check, so a pull request or merge request cannot merge until the scan passes, which is what makes this layer effective even against a developer who bypassed the local hook.
Layer 3: automated response when a leak is detected. Detection alone is insufficient if the response is "someone deletes the line and force-pushes"; the credential itself is still valid and anyone who cloned the repository before the fix (including in CI logs, forks, or cached pipelines) still has it. The response has to be:
- Immediately revoke or rotate the actual credential at its issuing system (the cloud provider's Identity and Access Management (IAM) console or API, the database, the third-party service), automated wherever the issuing service exposes an API for it, because deleting the leaked line from the repository does nothing to the credential's validity.
- Purge the credential from git history (using a tool such as
git filter-repoor the BFG Repo-Cleaner) as a hygiene follow-up, done after rotation, never as a substitute for it, since a force-pushed history rewrite does not retroactively invalidate a secret anyone already copied. - Open an incident ticket and notify the owning team and security, with the detection tool's finding (file, line, commit, timestamp) attached so the rotation can be scoped precisely to the actual exposed value.
- For public repositories specifically, some cloud providers partner directly with GitHub's secret scanning to receive an automatic notification (and in some cases auto-revoke) when their own token format is detected in a public repo, which is a genuine defense-in-depth layer beyond what the organization's own pipeline controls.
Layer 4: developer education. The hooks and scanners above are a safety net for when the primary practice fails; the primary practice is developers never reaching for a literal credential string to begin with. Onboarding should cover concretely what counts as a secret (not just passwords and API keys, but connection strings, private key files, and webhook signing secrets), the sanctioned alternative (reference a secrets manager or environment variable injected at runtime, never a hard-coded value), and a walkthrough of exactly what happens when the pipeline catches something, so a false sense of "the scanner will catch it anyway" doesn't erode care at the point of typing the code.
Worked example
flowchart TD
Dev["Developer writes code"] --> Pre["Pre-commit hook: Gitleaks / detect-secrets on staged diff"]
Pre -- "clean" --> Push["git push"]
Pre -- "secret found" --> Fix1["Blocked locally, developer fixes before commit"]
Push --> Native["Native scan: GitHub secret scanning + push protection, or GitLab Secret Detection"]
Native -- "known secret pattern" --> Block["Push/merge blocked in real time"]
Native -- "clean" --> CI["CI job: Gitleaks/TruffleHog with custom org rule set"]
CI -- "required status check fails" --> Block2["Merge request blocked until fixed"]
CI -- "passes" --> Merge["Merge allowed"]
Block --> Response["Automated response: rotate credential, purge history, open incident"]
Block2 --> Response
For example, a developer accidentally commits a database connection string containing a plaintext password into a feature branch and opens a pull request on GitHub. The local pre-commit hook was skipped (the developer used --no-verify to save time). GitHub's push protection does not recognize this particular connection-string format (it is not one of the well-known formats it ships detectors for), so the push succeeds, but the required CI status check running Gitleaks with the organization's custom regex for its own connection-string format flags it and fails the check, blocking the merge. The automated response pipeline opens an incident, rotates the database password immediately via the database's own credential-rotation API, and only after rotation does a follow-up task rewrite the branch's history to remove the string, because the branch had not yet merged to a shared history other clones depended on.
Trade-offs and pitfalls
- Relying on a single layer is the most common mistake. A team that only enables native platform scanning will miss any secret format the vendor does not recognize (many internal or third-party token formats), which is exactly why a maintained custom rule set is a required layer, not a nice-to-have.
- History purges without rotation give a false sense of remediation. Removing a line from git history and force-pushing does not invalidate the credential; anyone who already cloned, forked, or has it cached in a CI log still has a working secret. Rotation has to happen first and is the only step that actually closes the exposure.
- Local hooks are trivially bypassable (
--no-verify) and cannot be the sole control, which is exactly why the required server-side CI status check exists: it cannot be skipped by an individual developer's local git configuration. - False positives erode trust in the pipeline. An overly broad custom regex that flags test fixtures or example config files as leaks trains developers to treat scanner failures as noise to work around rather than signal to act on; rule sets need an allowlist mechanism (per-file or per-pattern) for genuine false positives, reviewed periodically rather than left to accumulate silently.
- Public-repository auto-revocation partnerships (where a cloud provider is notified directly by the hosting platform) are a real defense-in-depth layer but are provider-and-platform-specific, so the organization's own rotation automation still has to be the primary mechanism, not a fallback assumption that a third party will always catch it.
Design single sign-on (SSO) and single logout (SLO) across multiple web applications and multiple identity providers (SAML and OIDC). Explain front-channel vs back-channel logout mechanisms, how you'd correlate sessions across apps, and how to handle IdP unavailability or failure modes without leaving orphaned sessions.
Sample Answer
Direct answer
Correlate every application's local session to the identity assertion that created it via a single shared identifier, a Security Assertion Markup Language (SAML) NameID plus SessionIndex, or an OpenID Connect (OIDC) sid (session ID) claim, so a single logout event issued by either protocol's identity provider (IdP) can be mapped back to every application session it created, regardless of which protocol established it. Logout must propagate through BOTH front-channel (browser-mediated redirects or iframes, reaching sessions the user's current browser can still touch) and back-channel (server-to-server calls, reaching sessions a redirect chain won't, background jobs, other devices, closed tabs) mechanisms, and every session must carry an absolute maximum time-to-live (TTL) as a backstop, so an IdP outage during logout can orphan a session for, at most, a bounded window, never indefinitely.
Structured elaboration
Session correlation across apps
- The correlating identifier is protocol-specific: SAML federates identity via a NameID plus a SessionIndex, a per-IdP-session identifier included in the original assertion; OIDC federates it via the ID token's
sidclaim. Each application, on establishing its local session from either assertion type, must persist that correlating identifier alongside its own local session record. - A central session registry, keyed by the correlating identifier rather than any one app's own session id, maps "this IdP session" to "every local app session it created", exactly what a logout event needs to fan out to: the IdP issues one logout event naming one correlating identifier, and the registry resolves it to N app sessions to terminate.
Front-channel vs back-channel logout
- Front-channel logout: the IdP, or a gateway, redirects, or loads via a hidden iframe, the user's current browser through each app's own logout endpoint in sequence, SAML's
LogoutRequest/LogoutResponsevia the HTTP-Redirect or HTTP-POST binding, or OIDC's front-channel logout via each relying party's registered logout URI loaded in an iframe. This reaches any app whose session lives in the SAME browser session actively completing the logout. - Back-channel logout: the IdP calls each app's logout endpoint directly, server-to-server, independent of the user's browser, SAML's back-channel
LogoutRequestvia the SOAP binding, or OIDC's back-channel logout via a signed logout token posted to each relying party's back-channel logout endpoint. This is the ONLY mechanism that reaches sessions the browser-redirect chain cannot: a session on a different device, a session in a background tab the user never revisits, or a mobile app's session with no browser redirect surface at all. - A front-channel-only implementation is a common, incomplete-by-construction shipped bug: it looks correct in the common case, one browser, one active tab, and silently leaves every OTHER session, another device, a background tab, an app the browser navigated away from mid-chain, live and orphaned.
sequenceDiagram
participant U as User
participant App1 as App A
participant App2 as App B
participant IdP as Identity Provider
U->>App1: Click logout
App1->>IdP: Front-channel logout request
IdP->>App2: Back-channel logout token (server to server)
App2-->>IdP: Session invalidated ack
IdP-->>App1: Front-channel logout response
App1-->>U: Redirect to logged-out page
Handling IdP unavailability without orphaned sessions
- A back-channel logout call to an app that's temporarily unreachable must be retried, not fired-and-forgotten; queue it with backoff and track delivery per correlating-identifier-and-app pair, so a transient failure doesn't silently leave that one app's session alive indefinitely.
- Every session, independent of whether logout propagation ever successfully reaches it, must carry its own absolute maximum lifetime, a hard session TTL requiring re-authentication once elapsed regardless of activity. This is the backstop bounding the worst case: even a completely failed logout fan-out, every back-channel call permanently failing, leaves an orphaned session alive for at most that TTL, never forever.
- Run a periodic reconciliation job comparing the session registry's "logout events issued" log against "sessions actually confirmed terminated", and re-attempt delivery for any gap, so a retry queue that silently exhausted its attempts doesn't go unnoticed indefinitely.
- If the IdP ITSELF is unavailable, not just one app's back-channel endpoint, new logins for the affected identity fail clearly, but this does not, by itself, terminate EXISTING sessions; existing sessions rely on the TTL backstop above, not an emergency broad logout the down IdP couldn't issue anyway.
Worked example
A concrete failure trace: App B's back-channel logout endpoint is unreachable at the moment logout fires.
- The user logs out via App A. The IdP's front-channel redirect updates App A's own session immediately.
- The IdP's back-channel call to App B times out.
- A retry queue schedules redelivery with exponential backoff, attempts at 1, 2, and 4 seconds after the first failure.
- Cumulative retry window:
1 + 2 + 4 = 7 seconds. If all three attempts fail, the periodic reconciliation job, running on its own separate schedule, detects the "issued but not confirmed" gap and re-queues delivery. - Worst case, if reconciliation also cannot reach App B (a prolonged outage), App B's session survives only until its own absolute session TTL, for example 8 hours, elapses. That TTL, not the retry logic, is the actual upper bound on how long the orphaned session can live, and it holds regardless of how badly the redelivery path fails.
Trade-offs and pitfalls
- Correlating on a SAML SessionIndex or OIDC
sidonly works if every app actually persists it at session-creation time; a legacy app that stores only its OWN session id and discards the IdP's correlating identifier cannot be reached by any logout fan-out at all, no protocol-level fix helps if the correlating id was thrown away at login. - Back-channel logout requires each app to expose a network endpoint reachable by the IdP, not hidden behind the user's own browser session, a real infrastructure requirement, firewall rules, service discovery, that front-channel-only deployments never had to solve. Teams sometimes skip back-channel specifically because standing this up is real work, which is exactly how the front-channel-only gap above ships.
- A hard absolute session TTL as the ultimate backstop is a genuine trade-off against convenience, even a perfectly legitimate, continuously active session eventually forces re-authentication. The alternative, no absolute TTL at all, means a failed logout fan-out has no bound whatsoever, an orphaned session that lives forever, strictly worse for a control whose entire purpose is bounding exposure.
- Mixing SAML and OIDC IdPs on the same platform means the correlation layer has to normalize two structurally different logout wire formats, XML-based SAML requests and responses versus JSON/JWT-based OIDC logout tokens, into one internal event shape. Under-testing this normalization layer itself, rather than each protocol's logout independently, is where cross-protocol SSO/SLO bugs actually tend to live.
Design an authentication and authorization architecture for a web application serving 2 million monthly users. Specify identity provider selection (OIDC/OAuth2 choices), token format and lifetime, session management, refresh token rotation, and how microservices enforce authorization at scale.
Sample Answer
Direct answer
For a web application serving 2 million monthly users, use OpenID Connect (OIDC, an identity layer built on OAuth 2.0) through a managed or hardened identity provider, issue short-lived JSON Web Token (JWT) access tokens alongside a rotating refresh token, and have each microservice enforce authorization by validating that JWT locally against a cached signing key rather than calling a central service on every single request, reserving centralized checks for the rarer decisions that genuinely need to reflect a change faster than a token's lifetime allows.
Structured elaboration
Identity provider selection. Choose OpenID Connect over bare OAuth 2.0 for the authentication layer itself: OAuth 2.0 alone is a delegation and authorization framework with no standard notion of "who is this user," while OIDC adds the ID token and a standard set of claims specifically for authentication on top of it. Prefer a managed identity provider over building one from scratch unless identity is genuinely the product's core differentiator, since correctly implementing token issuance, signing-key rotation, and account-recovery flows at this scale is a substantial, security-critical undertaking a managed provider has already hardened across many customers. At 2 million monthly users, plan for federation from day one too, both social login (existing users signing in with an account they already have) and, if any enterprise customers are in scope, Security Assertion Markup Language (SAML) or OIDC federation of their own; retrofitting federation onto an already-live user base is materially harder than including it as launch scope.
Token format and lifetime. Use JWTs for access tokens specifically because they're self-contained: any microservice can verify a request locally, checking the signature against a cached public key plus the expiry and claims, without a network call back to the identity provider. The alternative, opaque tokens validated through the identity provider's introspection endpoint on every call, adds a network hop and a central dependency to every single authorized request, a real cost at this volume. Keep access-token lifetime short, on the order of minutes, precisely because a JWT cannot be revoked mid-flight the way a server-side session lookup can; the short time-to-live is what bounds the exposure window from a leaked token, not a revocation call. Reserve the ID token for establishing session identity at login and refresh time; it is never sent to backend microservices as an authorization credential.
Session management. For a browser client, treat the long-lived, renewable refresh token as the actual session, stored either server-side or in an HttpOnly, Secure, SameSite cookie, never in localStorage, which is readable by any script and therefore by any cross-site scripting (XSS) vulnerability. Short-lived access tokens are then minted on demand from that refresh token, giving the user the experience of a persistent session without needing to log in again every few minutes, while keeping the credential that actually travels with every request short-lived.
Refresh token rotation. Rotate the refresh token on every use: each refresh call returns both a new access token and a new refresh token, and the token just used is immediately invalidated. Pair this with reuse detection: if a refresh token that has already been rotated out is presented again, treat that as a signal of theft and revoke the entire token family descended from that original login, forcing re-authentication. This is the current OAuth 2.0 security best practice, documented in RFC 9700, the Internet Engineering Task Force's OAuth 2.0 security best-current-practice document published in 2025, precisely because a static, long-lived refresh token silently copied by an attacker is otherwise indistinguishable from the legitimate client until the real user and the attacker happen to collide on using the same token.
Microservice authorization enforcement at scale. Each microservice validates the JWT locally, checking signature, expiry, and audience claim against a cached copy of the identity provider's public signing key, refreshed on its own schedule rather than fetched on every request, and then makes its own authorization decision from the token's claims combined with any resource-specific ownership check it needs locally. This avoids a central authorization service becoming a bottleneck or single point of failure across potentially thousands of requests per second system-wide. A small number of genuinely cross-cutting, fast-changing authorization decisions, one that must reflect a change made seconds ago rather than up to a token's lifetime later, can route through a shared policy cache instead, but the common case should resolve from the token alone without leaving the microservice.
flowchart LR
Client["Web client"] -->|OIDC login| IdP["Identity provider"]
IdP -->|ID token + JWT access token + refresh token| Client
Client --> GW["API gateway"]
GW --> SvcA["Microservice A<br/>(validates JWT locally)"]
GW --> SvcB["Microservice B<br/>(validates JWT locally)"]
SvcA --> Key["Cached IdP public key"]
SvcB --> Key
Worked example
At 2 million monthly users, assume roughly 10 percent are concurrently active during a peak hour, a reasonable planning assumption for a consumer web application:
2,000,000×0.10=200,000 concurrently active sessions at peak
If each active session refreshes its access token once every 15 minutes (the chosen access-token lifetime):
200,000/15≈13,333 refresh calls per minute
13,333/60≈222 refresh requests per second at the identity provider, at peak
This is the number worth sizing the identity provider's refresh endpoint capacity against; it is a small fraction of the much larger per-request JWT-validation volume every microservice absorbs locally without ever calling the identity provider, which is exactly the throughput benefit of choosing local validation over introspection in the first place.
Trade-offs and pitfalls
The most self-defeating pitfall is choosing JWTs specifically for local, stateless validation, then adding introspection on every call anyway "to be safe": that reintroduces the exact network hop and central dependency the JWT choice was meant to avoid, at this request volume. A related pitfall is lengthening access-token lifetime to reduce refresh traffic, which directly enlarges the exposure window on a leaked token, since a JWT cannot be revoked mid-flight. Storing tokens in localStorage for developer convenience remains the single most common real-world path from a cross-site-scripting bug to full account takeover. The deeper trade-off: fully local JWT validation scales cleanly, but it means a permission change takes up to one token's lifetime to actually take effect everywhere, which is acceptable for most permission changes and wrong for a handful, such as immediately revoking a compromised account, that genuinely need a faster path; the fix for those few cases is a short-TTL check against a small, targeted revocation list, not routing every request through introspection.
Perform a threat modeling exercise for an enterprise IAM platform. Identify top attack vectors (token theft, account takeover, IdP compromise, provisioning abuse, privileged escalation, lateral movement) and propose concrete mitigations, detection strategies, and compensating controls for each vector.
Sample Answer
Direct answer
A threat model for an enterprise identity and access management (IAM) platform should walk each stage where trust is established or extended, credential issuance, token use, account elevation, and inter-system access, and ask what an attacker gains at each stage and what specific control catches or blocks it. The six vectors named here span three parts of that lifecycle: the integrity of tokens and the identity provider (IdP) that issues them, the moment an identity is created or elevated, and what an attacker does after gaining an initial foothold.
Structured elaboration
This applies STRIDE-style reasoning (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege, the standard threat-categorization lens) directly to the IAM platform rather than teaching the methodology itself. For each vector: what the attacker actually does, the primary preventive mitigation, how you would detect it, and a compensating control that limits damage if the primary mitigation is absent or fails.
| Vector | Attacker action | Mitigation | Detection strategy | Compensating control |
|---|---|---|---|---|
| Token theft | Steals a valid, unexpired token via cross-site scripting (XSS), insecure client storage, or a malicious browser extension | Short token lifetimes; sender-constrained tokens (mutual TLS or DPoP, Demonstrating Proof-of-Possession, so a stolen token cannot be replayed from a different client); store tokens in httpOnly cookies, not scriptable storage | Same token used from two different IP addresses or user agents in a short window; impossible-travel pattern between two token uses | Fast revocation via a token-introspection endpoint or short-lived-token expiry, plus step-up authentication required for sensitive actions even inside an already-authenticated session |
| Account takeover | Gains control of a user's identity via a phished password, phished push-based multi-factor approval, or SIM-swap-based SMS interception | Phishing-resistant authentication (FIDO2/WebAuthn hardware-bound passkeys) preferred over SMS or push-based multi-factor authentication (MFA); MFA required on every account | New-device or new-location login alerting; an unusual action sequence immediately after login, such as a bulk data export or an MFA-method change | Risk-based step-up authentication on sensitive actions regardless of how the session began, and session-level anomaly monitoring able to force mid-session re-authentication |
| IdP compromise | Compromises the identity provider itself: its signing key, its admin console, or a federation trust configuration; the highest blast-radius vector, since it can mint a valid token for any identity | Hardware security module (HSM)-backed signing keys so private key material is never directly exposed even to IdP administrators; the IdP's own admin accounts get the strongest privileged access management (PAM) and MFA treatment of any account in the environment | Monitoring the IdP's own admin audit log for configuration changes (a new federation trust added, a signing key exported or rotated unexpectedly); anomaly detection on token-issuance volume | Short-lived tokens bound the maximum damage window even if a signing key is compromised, paired with a rehearsed emergency key-rollover runbook so the actual rollover takes minutes, not days |
| Provisioning abuse | A malicious or coerced actor abuses the account-creation or entitlement-granting workflow itself, for example through SCIM (System for Cross-domain Identity Management, the standard protocol many IdPs use to auto-provision downstream apps), rather than compromising an existing account | Dual-control approval on any provisioning action granting elevated entitlements, so no single actor can both request and approve; the provisioning system itself is treated as a privileged system | Alerting on provisioning events without a matching change ticket; periodic reconciliation between the HR system of record and actual granted entitlements | Periodic access review and attestation, a named owner actively re-certifying who has access on a fixed cadence, catches an abusively-provisioned account even if the initial detection missed it |
| Privileged escalation | A foothold in a lower-privileged account or system is used to reach a higher-privileged one, via excessive standing permissions or a flaw in authorization logic | Least privilege by default plus just-in-time (JIT) elevation instead of standing privileged access, so there is no permanently-elevated credential sitting around to escalate into | Alerting on the elevation event itself (a JIT request, an addition to a privileged group), correlated against whether the requesting identity's recent behavior looks anomalous | Session recording and brokering through the privileged access management layer, so a successful escalation is fully observed and time-boxed rather than open-ended |
| Lateral movement | Uses one compromised identity's access to reach additional systems, most dangerous when one credential or broadly-trusted identity is valid everywhere | Segmented workload and service identities: short-lived, narrowly-scoped credentials per system rather than one shared service account reused across many systems | Correlating a single identity's access pattern across multiple systems in a short window against its historical baseline | Distinct credentials and scopes per trust boundary mean reaching one system with a stolen identity does not automatically grant reachability to the next |
Worked example
A realistic chained attack shows why treating these six vectors in isolation understates the real risk. An attacker phishes a push-based MFA approval from a standard user (account takeover). From that lower-privileged foothold, they discover a service account with excessive standing permissions, including access to the IdP's admin console, and use it to escalate (privileged escalation, enabled by the absence of just-in-time elevation). With admin access to the IdP, they attempt to add a new federation trust so their own external identity provider is accepted as authoritative (an IdP compromise attempt). Reading this chain against the table above: the account-takeover step should have been caught by new-device login alerting; if it was not, the privileged-escalation step should have been caught by alerting on the elevation event itself, since a standard user reaching admin-console access is a clear deviation from baseline; if that was also missed, the IdP's own admin audit log monitoring for a newly-added federation trust is the last line before the attacker has durable, org-wide token-minting capability. No single control in the table is expected to be perfect, the chain is stopped by whichever layer actually catches it, which is the point of listing detection strategies at every stage rather than only at the first one.
Trade-offs and pitfalls
- Treating each vector as independent understates chained risk. As the worked example shows, a weak mitigation at one stage (no JIT elevation, so standing over-permissioned service accounts exist) turns a low-severity account takeover into a high-severity IdP compromise attempt. A mature threat model reviews chains across vectors, not just each row of the table in isolation.
- Detection-only coverage for the IdP-compromise vector is not enough given its blast radius. Because a compromised IdP can mint tokens for any identity, this is the one vector where the compensating control (short token lifetime plus a rehearsed rollover runbook) matters as much as the primary mitigation; relying purely on detecting the compromise after the fact leaves too large a window of full-organization exposure.
- Just-in-time elevation without session recording only half-solves privileged escalation. JIT reduces the window an elevated credential exists, but without session recording and brokering, a successful escalation inside that window is still unobserved; the two controls are complementary, not substitutes.
- A common wrong turn is treating provisioning abuse as purely a technical control problem. Dual-control approval workflows help, but the compensating control that actually catches a determined insider or a coerced approver is the human process of periodic access review, a technical gate alone does not substitute for someone actively re-certifying access on a cadence.
Unlock Full Question Bank
Get access to all Identity, Authentication, and Access Management interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.