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.
Write a Python function that computes the effective permissions of a user given: a role hierarchy (roles may inherit other roles), a mapping of roles to permissions, and a list of roles assigned to the user. The function must handle cycles in role inheritance gracefully and return a deduplicated set of permissions. Include function signature and brief complexity expectations.
Sample Answer
Direct answer
Treat this as a graph traversal, not a recursive tree walk: each role is a node, an inheritance edge points from a role to the roles it inherits from, and a user's effective permissions are the union of every role's direct permissions reachable from their assigned roles. Track visited roles explicitly so a cycle in the hierarchy, which is a real misconfiguration a permissions table can accumulate after enough ad hoc edits, terminates instead of looping forever, and let a plain set naturally deduplicate the result.
Structured elaboration (approach)
Use an iterative traversal with an explicit visited set and a work stack, rather than plain recursion. Two reasons: it sidesteps Python's recursion limit on a very deep hierarchy, and, more importantly, the visited set is exactly what makes a cycle safe: once a role has been processed, revisiting it through a different inheritance path is a no-op instead of a second traversal down the same cycle.
Function signature:
def effective_permissions(
user_roles: list[str],
role_hierarchy: dict[str, list[str]], # role -> parent roles it inherits from
role_permissions: dict[str, set[str]], # role -> permissions granted directly
) -> set[str]:
...
Worked example (executed)
def effective_permissions(user_roles, role_hierarchy, role_permissions):
visited = set()
permissions = set()
stack = list(user_roles)
while stack:
role = stack.pop()
if role in visited:
continue
visited.add(role)
permissions |= role_permissions.get(role, set())
stack.extend(role_hierarchy.get(role, []))
return permissions
def run_demo():
# admin -> manager -> editor -> admin is a cycle: a real misconfiguration
# a permissions table can end up with after enough ad hoc edits. Each role
# also grants its own permission, so we can confirm every permission in
# the cycle is collected exactly once.
role_hierarchy = {
"admin": ["manager"],
"manager": ["editor"],
"editor": ["admin"], # closes the cycle back to admin
"viewer": [],
}
role_permissions = {
"admin": {"users:delete"},
"manager": {"reports:export"},
"editor": {"posts:write"},
"viewer": {"posts:read"},
}
results = []
perms = effective_permissions(["admin"], role_hierarchy, role_permissions)
results.append((
"cyclic hierarchy resolves to all reachable permissions, terminates",
perms == {"users:delete", "reports:export", "posts:write"},
))
perms2 = effective_permissions(["viewer", "manager"], role_hierarchy, role_permissions)
results.append((
"two directly-assigned roles union correctly",
perms2 == {"posts:read", "reports:export", "posts:write", "users:delete"},
))
# reports:export pulls in editor -> admin transitively from "manager",
# confirming multi-hop inheritance beyond the direct assignment.
perms3 = effective_permissions(["ghost-role"], role_hierarchy, role_permissions)
results.append(("unknown role degrades to empty set, no exception", perms3 == set()))
all_pass = True
for name, passed in results:
print(f"[{'PASS' if passed else 'FAIL'}] {name}")
if not passed:
all_pass = False
print(f"\neffective_permissions(['admin'], ...) = {sorted(perms)}")
print(f"ALL_PASS={all_pass}")
if __name__ == "__main__":
run_demo()
Output, from an actual run (python3 effective_permissions.py):
[PASS] cyclic hierarchy resolves to all reachable permissions, terminates
[PASS] two directly-assigned roles union correctly
[PASS] unknown role degrades to empty set, no exception
effective_permissions(['admin'], ...) = ['posts:write', 'reports:export', 'users:delete']
ALL_PASS=True
The first case is the one that actually tests what the question asks for: admin -> manager -> editor -> admin is a genuine cycle, and the function both terminates (rather than looping until the process is killed) and still collects all three permissions reachable around that cycle, not just the ones on the role the traversal happened to start from.
Complexity and edge cases
Let V be the number of distinct roles reachable from user_roles and E the number of inheritance edges among them; the traversal itself is O(V + E), since the visited set guarantees each role is popped from the stack and expanded at most once regardless of how many cycles or redundant paths reach it. Building the final permission set costs O(P), where P is the total number of permission entries across every visited role, since each is inserted into the result set at most once. Space is O(V + P) for the visited set and the accumulated permissions.
Edge cases demonstrated above: a genuine cycle in the hierarchy (the core requirement in the question), a user with multiple directly-assigned roles that overlap in what they transitively grant, and a role name with no entry in either input dictionary (a stale assignment after a role was renamed or deleted), which degrades to contributing nothing rather than raising.
Trade-offs and pitfalls
- A recursive implementation without an explicit visited set is the single most common wrong answer to this exact question. It will either hit Python's recursion limit or loop forever the instant the hierarchy has a cycle, and cycles do happen in real permission tables, usually after enough years of ad hoc "make this role also inherit from that one" edits by different people who never saw the whole graph at once.
- Returning a list instead of a set, or otherwise skipping deduplication, silently allows the same permission to be counted more than once when two different inherited roles both grant it. That's harmless for a plain membership check but can quietly break any downstream code that assumes the length of the result counts distinct permissions.
- This function is deliberately silent about an unknown role, treating it as contributing nothing rather than raising. That's a reasonable default for a stale role assignment (the role was deleted, but a user record still references it), but it is a choice, not a law: a system that wants to catch that condition as a data-integrity bug should log or raise instead, rather than assuming this function's leniency is the right default everywhere it's reused.
Discuss differences between symmetric (HS256) and asymmetric (RS256) JWT signing algorithms. Create a migration plan to move from HS256 to RS256 across many services: key generation, distribution, library updates, handling tokens signed with old keys, preventing algorithm-confusion attacks, and operationalizing kid-based key rotation.
Sample Answer
Direct answer
HS256 (HMAC-SHA256, a symmetric algorithm where the signer and every verifier hold the same secret) and RS256 (RSA signature with SHA-256, an asymmetric algorithm where a private key signs and a public key verifies) differ in exactly one consequential way: with HS256 every verifying service holds a secret that could also forge a token, while with RS256 only the identity provider can sign, and every other service just verifies. Migrate as a phased, dual-running rollout, never a single flag flip: generate and publish the new key, teach every verifier to accept both algorithms keyed by a kid (key ID), cut the issuer over to RS256, wait out the longest token lifetime still in circulation, then retire HS256 entirely.
Structured elaboration
Differences, concretely. In an HS256 world with N independently-operated verifying services, the shared secret exists in N places, meaning N places it can leak from, and every one of those N services technically has the power to mint tokens as if it were the identity provider. RS256 confines signing power to exactly the identity provider; every verifier only ever needs the safely-public public key.
Phase 0: preparation. Generate the RSA key pair (2048-bit minimum, 3072-bit for longer shelf life) inside an HSM (hardware security module) or a cloud KMS (key management service), never as a raw private-key file emailed or copied around. Assign it a kid distinct from anything currently in use.
Phase 1: dual-verification rollout. Update every verifying service's JWT (JSON Web Token) library or middleware so it selects the verification key and algorithm by kid, from an explicit allow-list, rather than trusting the token's own alg claim. Point HS256 verification at the existing shared secret (wherever it's currently stored) and RS256 verification at the new public key, fetched from a new JWKS (JSON Web Key Set) endpoint you stand up as part of this phase. Ship this to every verifying service and confirm both paths work (for example with a canary token of each type) before moving on. Nothing externally visible changes yet: the issuer is still only signing HS256 tokens. This phase is the largest engineering lift, because it touches every independently-deployed verifying service, but it carries zero user-facing risk because no new algorithm is in production use yet.
Phase 2: cut over the issuer. Switch the token-issuing service to sign new tokens with RS256, tagged with the new kid. Tokens already signed with HS256 remain valid, because Phase 1's verifiers still accept them.
Phase 3: sunset window. Wait out the maximum lifetime of any token type still being verified. This is bounded by whichever token type lives longest in your system, typically refresh tokens rather than short-lived access tokens, so the true sunset window is set by the long pole, not the average case. Monitor verification logs for alg: HS256 still occurring; once it drops to zero (or an acceptable floor, accounting for long-lived tokens belonging to sessions that may simply never return), proceed.
Phase 4: retire HS256. Remove HS256 acceptance from every verifying service in a second deploy cycle, and destroy the shared HMAC secret wherever it was stored, so it is useless even if it leaks later.
flowchart LR
P0[Phase 0: generate RSA key pair, assign kid] --> P1[Phase 1: dual verification, verifiers accept HS256 and RS256]
P1 --> P2[Phase 2: issuer cuts over to signing RS256]
P2 --> P3[Phase 3: sunset window, wait out max token lifetime]
P3 --> P4[Phase 4: retire HS256, destroy shared secret]
Distribution. Rather than manually pushing the new public key into each service's configuration (which doesn't scale and drifts), publish it at a JWKS endpoint; verifiers fetch and cache it with a reasonable TTL (time-to-live), re-fetching immediately if they ever see an unrecognized kid.
Library updates. Most mainstream JWT libraries already support RS256 out of the box, so the real work is rarely "does the library support this algorithm." It's fixing how the library is configured: making sure verification is pinned to an explicit algorithm allow-list, selected by kid, instead of trusting whatever the incoming token claims about itself. Teams migrating off HS256 very often discover their existing verification code had exactly this bug (trusting the token's own alg), which brings us to the next point.
Preventing algorithm-confusion attacks. The canonical version of this attack: a verifier calls something like "verify this token using whatever algorithm its header says," an attacker submits a token with alg: HS256 and a signature computed using the RSA public key as if it were an HMAC secret. Since the public key is, by definition, not secret, the attacker can compute a valid-looking HMAC with it, and a verifier that blindly follows the token's own alg claim accepts the forgery. The fix: the verifier's algorithm allow-list is fixed in its own configuration, never taken from the token. During the dual-acceptance window the allow-list is {HS256, RS256}, but which specific key is used to check a given token is driven by kid, a known reference to a known key of a known type, never by blindly trusting the claimed algorithm.
Operationalizing kid-based key rotation. Track every key, including the legacy HMAC secret (give it an explicit id too, even if it's just a label like legacy-hmac-v1), in a small key registry with a status: pending, active-signing, active-verify-only, retired. Build (or reuse) automation that can generate a new key, publish it to JWKS in verify-only status, flip the issuer to sign with it after a soak period, and retire the old key after the token-lifetime window passes. This is exactly the machinery every future rotation, whether routine or an emergency compromise response, reuses, so building it once here pays off on every subsequent rotation.
Worked example
Suppose access tokens live 1 hour and refresh tokens live 30 days, across 20 verifying services (both numbers are given assumptions for this walkthrough, not measurements). Day 0: dual-verification (Phase 1) is deployed everywhere; both algorithms are now accepted. Day 1: the issuer (Phase 2) cuts over to signing only RS256; a refresh token minted at this exact moment could still be HS256 if it slipped in just before cutover, and it carries a 30-day lifetime from its issuance date. The latest possible HS256-signed refresh token is therefore valid until Day 1 + 30 days = Day 31. So the sunset window (Phase 3) must run at least until Day 31, not until the 1-hour access-token lifetime suggests, because the refresh token is the long pole. Only at Day 31 or later is it safe to retire HS256 (Phase 4) and destroy the shared secret, since by then every token that could possibly have been signed under it has expired.
Trade-offs and pitfalls
Skipping the dual-verification phase and flipping the issuer straight to RS256 breaks every live session instantly, since no verifier can check the new signatures yet.
Trusting the token's own alg claim (the root cause of algorithm-confusion attacks) is the single most common bug this migration should catch and fix, not just work around.
Deleting the shared secret or the old public key before every possible outstanding token, including long-lived refresh or "remember me" tokens, has actually expired causes a wave of legitimate "invalid signature" failures.
Treating "our library already supports RS256" as sufficient understates the work: the real lift is the trust and configuration wiring (kid-based key selection, an explicit algorithm allow-list), which a library version bump does not do for you.
Doing this migration as a one-off manual project, rather than building the small rotation-automation described above, means the next rotation, whether routine or an emergency compromise response, starts from scratch instead of reusing tooling that already exists.
Perform a threat model for TOTP (time-based one-time password) second-factor authentication. Identify how replay attacks, desynchronization, brute-force guessing, provisioning interception, and man-in-the-middle attacks could be executed. Recommend mitigations such as skew windows, rate limiting, provisioning protections, and binding tokens to sessions or devices.
Sample Answer
Direct answer
A TOTP (time-based one-time password) second factor is a shared-secret HMAC construction where both the device and the server derive the same 6 to 8 digit code from a secret seed and the current time step, so the two sides never exchange the code over the network in a way an attacker can intercept and reuse indefinitely. The threat model has five concrete attack surfaces: replay of a captured code, clock desynchronization between device and server, brute-force guessing of the short code space, interception of the QR code or seed during provisioning, and man-in-the-middle (MITM) relay of a live code to a second, attacker-controlled session. Each has a standard, well-understood mitigation: single-use enforcement plus a bounded time-skew window, rate limiting with lockout, provisioning-time protections (short-lived enrollment links, out-of-band confirmation), and binding the verified factor to the session it was presented in so a stolen code cannot be replayed into a different session.
Structured elaboration
TOTP is defined by RFC 6238 (time-based) on top of RFC 4226's HOTP (HMAC-based one-time password): both endpoints hold a shared secret K, compute HOTP(K, T) where T is the number of 30-second time steps since the Unix epoch, and truncate the HMAC output to a short decimal code. The server never needs the code in advance because it can recompute it itself from the same seed and the current time; that is what makes threat-modeling this mechanism different from, say, a password, where the server must not know the plaintext at all.
Go through each named attack in turn:
-
Replay attacks. An attacker who observes a valid code (shoulder-surfing, malicious browser extension, compromised log line, or a MITM proxy) can submit it again. Because HOTP/TOTP codes are deterministic per time step, a naive server that only checks "does this code match the expected value" is replayable within that step.
- Mitigation: the server must record the last successfully consumed time step per user and reject any code at or before that step, even if it is otherwise numerically valid. This makes each code single-use regardless of the skew window discussed next.
-
Desynchronization. Device clocks drift. If the client's clock is even one 30-second step ahead or behind the server's, verification fails even for a legitimate user, which pushes real deployments to accept a window of adjacent steps (commonly ±1, sometimes ±2).
- Mitigation: a bounded skew window, and separately, per-user clock drift tracking: if a user consistently verifies at step +1, the server can persist that offset and auto-correct on subsequent checks instead of widening the window for everyone. Widening the window blindly is itself a security cost (see trade-offs below), so per-user drift correction is the senior answer, not just "increase the window."
-
Brute-force guessing. A 6-digit code has 1,000,000 possible values. Guessing has decent odds if an attacker can throw many attempts at the verification endpoint before the window rolls over: with no rate limiting, that is a live risk within a 30 to 90 second acceptance window.
- Mitigation: rate limiting and lockout at the verification endpoint (both per-account and per-source-IP), scoped tightly enough that 30 seconds of guessing cannot realistically cover a meaningful fraction of the code space. A common concrete rule: lock the factor after 5 to 10 consecutive failures, with exponential backoff, and alert on repeated failure bursts as a signal distinct from a single mistyped code.
-
Provisioning interception. Enrollment is the highest-value target: whoever gets the shared secret
Kduring setup, typically delivered as a QR code or a displayed base32 string, gets a permanent, silent second factor that behaves identically to the real one. Intercepting the QR code (screen capture, shoulder-surf during enrollment, a compromised enrollment email or support channel) is strictly worse than replaying one code, because it is not single-use.- Mitigation: enrollment must happen inside an already-authenticated, short-lived session (the QR code should expire in minutes, not persist on a settings page indefinitely), should never be emailed or logged in plaintext, and ideally requires a fresh primary-factor re-authentication immediately before the secret is displayed. Enrollment/re-enrollment events should also notify the user out of band (email or existing verified channel) so a silent re-enrollment by an attacker who has already compromised the account gets caught.
-
Man-in-the-middle (MITM) relay. This is the attack TOTP fundamentally cannot stop on its own: a phishing proxy sits between the user and the real site, and forwards both the password and the live TOTP code to the real site in real time. Because the code itself is valid, the server has no way to distinguish "the legitimate user typed this into the real site" from "the legitimate user typed this into a phishing clone that is relaying it."
- Mitigation: TOTP alone cannot close this gap; it needs to be paired with something that binds the session. Binding the verified MFA event to the session it was presented in (session ID or device fingerprint checked at the time of code submission, not just at login) limits how far a relayed code can travel, and detecting anomalous properties of the session (new device, new IP, new user agent, immediately following the MFA check) lets the server challenge again or flag the session even though the code itself was "correct." The durable fix for phishing-relay is moving to a phishing-resistant factor (WebAuthn/FIDO2), which cryptographically binds the credential to the origin.
Worked example
Take a concrete deployment: 30-second time steps, a ±1 step skew window (accepts the previous, current, and next step, so 90 seconds of nominal acceptance), and a rate limit of 5 attempts per 5 minutes per account.
- Replay defense in numbers: without single-use enforcement, an attacker who captures one valid code has up to 90 seconds (three accepted steps) to reuse it. With single-use-per-step-consumed enforcement, that window collapses to zero: the moment the legitimate user's code is accepted, the server marks that step (and everything before it) as spent, so a captured copy of the same code is rejected even if submitted one second later.
- Brute-force defense in numbers: the code space is 10^6 = 1,000,000 values. At 5 attempts per 5 minutes, an attacker can try at most 5 values inside any single 90-second acceptance window before the account locks, a success probability of 5 / 1,000,000 = 0.0005% per window, not a meaningful attack path. Contrast that with no rate limiting: at a realistic 50 requests/second against an unprotected endpoint, an attacker could submit roughly 50 x 90 = 4,500 guesses inside one 90-second window, a success probability of 4,500 / 1,000,000 = 0.45% per attempt window, and the attacker gets a fresh window every 30 seconds indefinitely. That four-orders-of-magnitude gap is the entire argument for rate limiting existing at all.
- Session-binding defense against relay: if the server records the session identifier present when the TOTP code was submitted and requires the subsequent authenticated request to reuse that same session (rather than accepting the code as a standing "MFA satisfied" flag that any session can claim), a phishing proxy that relays the code into its own attacker-controlled session is blocked at the point the attacker tries to use the resulting authenticated state, because the session the attacker holds never received the MFA check itself.
Trade-offs and pitfalls
- Skew window width is a direct trade-off between usability and brute-force exposure. A wider window (say ±5 steps, 5.5 minutes) tolerates more clock drift and slow typers but multiplies the number of valid codes an attacker can guess against at any instant; senior designs prefer a narrow window plus per-user drift correction over a blanket wide window.
- Rate limiting must be per-account AND per-source, not just one or the other: per-account-only limiting still lets an attacker distribute guesses across many source IPs against one account without tripping IP-based defenses, and per-IP-only limiting misses a slow, patient attacker rotating targets.
- The common pitfall is treating "MFA passed" as a permanent session attribute instead of a check tied to the specific session and, ideally, request context; that is exactly what lets a relayed code from a phishing proxy grant the attacker's own session indefinite access once the real user's code is forwarded through it once.
- Provisioning is the highest-leverage place to under-invest, because a compromised enrollment is silent and permanent until discovered, unlike a single replayed code which is a one-time, noisy event; teams that harden verification but leave enrollment on a long-lived, unauthenticated, or loggable path have not actually raised the bar much.
- TOTP cannot solve phishing-relay by construction, since the whole point of the shared time-based secret is that it produces a value valid for anyone who submits it in time; acknowledging this honestly (rather than claiming "MFA stops phishing") is itself part of a mature threat model.
Compare options for authenticating applications and users to databases: shared application credentials (pooled), per-user DB authentication (impersonation), integrated OS auth (Kerberos), and certificate-based auth. Discuss pros/cons for security, auditing, connection pooling and operational complexity, and recommend patterns for OLTP workloads that require per-user auditing.
Sample Answer
Direct answer
None of the four options is universally best: they trade security and audit granularity against operational simplicity and connection-pooling efficiency. For an online transaction processing (OLTP) workload that genuinely needs per-user auditing, the pattern that usually wins in production is a pooled, service-level credential for the physical connection, combined with an application-set user context recorded on every statement, rather than opening one physical database connection per human user.
Structured elaboration
| Approach | Security | Auditing | Connection pooling | Operational complexity |
|---|---|---|---|---|
| Shared application credentials (pooled) | Coarse; one leaked credential exposes everything the app can do | DB logs show only the shared account; per-user attribution needs application-level logs | Excellent; connections are fungible | Lowest; one credential to rotate |
| Per-user DB authentication (impersonation) | Fine-grained; DB enforces per-user grants natively | Native and accurate at the DB layer | Poor for true per-connection impersonation; works if combined with pooling plus a session-context switch | Higher; user provisioning inside the DB itself |
| Integrated OS auth (Kerberos) | Strong; ticket-based, no password stored by the DB | Native, tied to the org's central identity system | Same tension as per-user auth | Highest; needs Kerberos infrastructure, keytab management, clock-sync sensitivity |
| Certificate-based auth (mutual TLS) | Strong; no shared secret, supports short-lived certs | As granular as certificate issuance (per-service is clean, per-user adds overhead) | Good when certificates are issued per service | Moderate; needs a certificate issuance and rotation pipeline |
Shared application credentials (pooled). One database account, connections reused freely across all app requests. This is what makes efficient pooling straightforward: since every connection authenticates identically, any one of them can serve any request. The cost is attribution: the database's own logs show only the shared account, never which end user actually acted, so per-user attribution has to be reconstructed from application logs, which is a real gap the moment the database is ever queried outside the application.
Per-user database authentication (impersonation). Each end user, or a distinct database principal per user, authenticates individually, or the application impersonates the calling user per connection or statement, for example through a proxy-authentication feature or a role-switching command. This gives the database's own audit log correct, native per-user attribution with no reconstruction needed. The real tension is with pooling: a true one-physical-connection-per-user model doesn't pool at all, since connection count then scales with concurrent users rather than app server capacity, which is exactly why most designs that want per-user attribution keep a pooled physical connection but switch its session-level user context per checkout instead of opening a truly separate connection per user.
Integrated OS authentication (Kerberos). The database trusts an external authentication authority, a ticket issued by a Kerberos Key Distribution Center, instead of managing its own passwords. This is strong (no password ever transmitted to or stored by the database, ticket-based, time-bounded) and gives attribution tied to the organization's central identity system, useful when the same identity needs consistent auditing across many systems, not just this one database. It carries the same pooling tension as per-user authentication, and the highest operational cost of the four: it requires domain and Kerberos infrastructure, keytab management, and is sensitive to clock drift, since Kerberos tickets are time-bounded and fail hard when clocks disagree.
Certificate-based authentication (mutual TLS). Each principal, typically a service, less commonly an individual user, presents an X.509 client certificate the database validates against a trusted certificate authority. This is strong (no shared secret to leak, naturally supports short-lived certificates to bound compromise exposure) and pools well when certificates are issued per service rather than per user, since every connection in the pool then shares one service identity. It requires a certificate issuance and rotation pipeline, real but more moderate than Kerberos, since there's no live directory dependency at connection time.
Recommendation for OLTP with per-user auditing. Keep the physical connection pooled under a service-level credential or a per-service certificate, since connection-pool efficiency is usually the dominant cost driver for OLTP throughput, and have the application set an explicit user-context on the connection for the duration of each unit of work, so every statement is attributable to the real end user in the database's own logs without paying the cost of a connection per user. If the same OLTP workload is also multi-tenant, pair this with row-level security policies keyed on that same session context, so the mechanism giving audit attribution also enforces per-user or per-tenant data isolation at the database layer, not only in application code.
Worked example
An order-management system runs 200 concurrent application threads against a pool of 20 physical PostgreSQL connections. On each request, the application checks out a pooled connection and executes SET LOCAL myapp.user_id = '<end-user-id>' as the first statement of the transaction. Using SET LOCAL (transaction-scoped) rather than SET (session-scoped) matters specifically because it resets automatically at transaction end, so it never leaks into the next, unrelated request that happens to reuse the same physical connection. The database's own audit log, or a row-level security policy comparing current_setting('myapp.user_id') against a row's owner column, then attributes and enforces access per real end user, even though only 20 physical, uniformly-authenticated connections exist underneath.
Trade-offs and pitfalls
The single most dangerous pitfall across every "impersonation on a pooled connection" pattern is forgetting to reset the session context between reuses: using session-scoped SET instead of transaction-scoped SET LOCAL silently misattributes, or worse misauthorizes, the next request that happens to land on that same physical connection. A related pitfall is choosing per-user or Kerberos authentication purely because it sounds more secure, without weighing the real connection-count cost against actual OLTP throughput requirements. The core trade-off: shared pooled credentials remain the right default for high-throughput OLTP where the per-user audit and enforcement need can be met at the application-context layer instead, reserving true per-principal database authentication for lower-volume, higher-sensitivity access, an analyst querying a data warehouse directly, for example, where connection count was never going to be the bottleneck.
Compare common Multi-Factor Authentication (MFA) approaches : TOTP (time-based OTP), SMS OTP, push-based approval, and hardware-backed/U2F/WebAuthn tokens : in terms of security, usability, deployability, and attack surface. For each method, list typical threats (e.g., SIM swapping, phishing, device theft) and describe when you would choose or avoid that method for a user-facing application.
Sample Answer
Direct answer
The four common multi-factor authentication (MFA, proving identity with more than one independent factor) methods trade off along the same two axes: how resistant the method is to phishing, and how much friction and cost it adds. Time-based one-time password (TOTP) apps and hardware-backed passkeys (WebAuthn/FIDO2) sit at the strong end, SMS one-time passwords sit at the weak end because the delivery channel itself can be hijacked independent of anything the user does wrong, and push-based approval sits in between: easy to use, but vulnerable to a specific social-engineering pattern (repeatedly prompting the user until they tap approve by habit or fatigue) that neither of the code-based methods share.
Structured elaboration
| Method | Security (phishing resistance) | Usability | Deployability | Attack surface / typical threats |
|---|---|---|---|---|
| SMS one-time password (OTP) | Weakest: the delivery channel itself can be subverted independent of the user | Highest: no app required, universally understood | Depends on telecom SMS gateways; cost and delivery reliability vary by region | SIM swapping (a carrier is socially engineered into porting the victim's number), interception at the telecom-network level, real-time phishing relay of the code |
| TOTP (authenticator app) | Good: the code itself is never transmitted over a network channel an attacker can pass through | High: requires installing and checking an app, minor typing friction | Cheap, standards-based, works offline once enrolled | Real-time phishing relay (a fake login page that immediately forwards the code the user typed), theft of the enrollment secret from a compromised device or backup |
| Push-based approval | Medium: removes manual code entry, but the approval action itself can be induced | Highest of the code/prompt-based methods: one tap | Requires the vendor's own app and network connectivity; not a cross-vendor standard | MFA fatigue or prompt bombing (sending repeated approval requests until the user taps approve out of habit or annoyance), device theft if the device is unlocked |
| Hardware-backed / WebAuthn (FIDO2) | Strongest: cryptographically bound to the site's own origin, so a look-alike phishing domain simply cannot obtain a valid signature | High once enrolled (tap or biometric), but requires a compatible key or platform authenticator | Hardware cost, and enrollment/recovery process complexity if a user's only authenticator is lost | Physical theft of the token (mitigated by requiring a PIN or biometric on the key itself), gaps in the recovery process |
Why the phishing-resistance ranking holds. SMS and TOTP both ultimately depend on the user (or an attacker impersonating the site) having a code that a phishing page can capture and immediately relay to the real site in real time (an adversary-in-the-middle relay); TOTP is still meaningfully better than SMS because it removes the telecom-layer interception risk (SIM swapping, network-level interception) that has nothing to do with the user's own behavior at all. Push notifications remove the "type a code" step but introduce a different failure mode: repeated, low-friction approval prompts that a user can eventually tap through without reading. WebAuthn is qualitatively different, not just incrementally better, because the cryptographic protocol itself checks the requesting site's origin before it will produce a valid signature, so the phishing page cannot obtain a usable credential regardless of how convincing it looks to the human.
When to choose or avoid each, for a user-facing application. TOTP is a strong, low-cost default for a broad consumer audience: free to implement, no telecom dependency, and meaningfully better than SMS for a modest amount of added friction. SMS OTP should be avoided as the only factor for anything of real value; it is best reserved for a recovery or fallback path for users without a smartphone, not the primary method, since its weaknesses live in infrastructure the application does not control. Push-based approval fits an enterprise or internal workforce application where the user population is known and already carries a managed device; it should be paired with a context check (showing the requesting device, location, or a number the user must match, rather than a bare "approve or deny" prompt) specifically to blunt prompt-bombing. Hardware-backed WebAuthn is the right default for privileged or high-value accounts (administrators, executives, anyone likely to be individually targeted), where phishing resistance matters enough to justify the enrollment friction and hardware cost, even if it is not yet practical to require for every user in a large consumer base on day one.
Worked example
A consumer web application decides its MFA policy by user tier rather than one policy for everyone: ordinary users are offered TOTP as the default second factor (cheap to support, meaningfully better than nothing, and better than SMS) with SMS OTP available only as an account-recovery fallback for a user who cannot install an authenticator app. Administrative accounts with access to production data or billing are required to enroll a hardware-backed WebAuthn key, since those accounts are the ones most likely to be individually targeted with a convincing spear-phishing attempt, and the origin-binding property of WebAuthn is what actually stops that attack, not merely the account holder's own vigilance. Internal support staff, who already carry a company-managed phone, use push-based approval with number matching (the login page displays a two-digit number the user must enter into the push prompt), specifically to close the fatigue-attack path a bare "approve/deny" prompt would leave open.
Trade-offs and pitfalls
The recurring pitfall is treating "we require MFA" as a single fact rather than a spectrum: an application that lets every account, including administrators, satisfy MFA with SMS alone has not meaningfully raised the bar against a targeted attacker willing to attempt a SIM swap, even though it can honestly claim MFA is enabled. A second pitfall is deploying push-based approval without any context or number-matching step; a bare approve/deny prompt is exactly what makes prompt-bombing effective, since the user has no information to distinguish a legitimate login attempt from an attacker's repeated requests. A third pitfall is over-indexing on WebAuthn's security strength while under-investing in its recovery flow: a user whose only hardware key is lost or broken needs a well-designed, equally secure recovery path, or the strongest method in the table becomes the one most likely to lock a legitimate user out.
Design note: cloud console access versus service identities. The comparison above assumes a human is present to complete an interactive challenge. That assumption does not hold for automated API calls made by a service identity (a machine or workload credential, not a person), which cannot tap a push prompt or read a TOTP code. The right policy split is to require MFA, ideally hardware-backed, for interactive console access by human administrators, while protecting service identities through mechanisms built for non-interactive use instead: short-lived, automatically rotated credentials, or workload identity federation that lets a service prove who it is without a standing long-lived secret at all. Treating "no interactive MFA" as a gap to fill with a weaker human-facing method (like requiring a service account to somehow "complete" SMS OTP) is the wrong instinct; the equivalent protection for a service identity is a fundamentally different, non-interactive credential lifecycle.
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.