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.
Design a passwordless challenge–response protocol for authenticating mobile clients to a server using asymmetric keys stored in secure enclave / platform authenticator. Describe message formats, enrollment flow, authentication flow, and how you prevent replay, cloning of keys, and account recovery abuse.
Sample Answer
Direct answer
The core design is a public-key challenge-response protocol: at enrollment, the mobile app asks the device's secure enclave (iOS Secure Enclave, Android StrongBox/Keystore) to generate a key pair that never leaves hardware in exportable form, and registers only the public key with the server; at every subsequent login the server sends a fresh, single-use random challenge, the enclave signs it, and the server verifies the signature against the stored public key. Because the private key is non-exportable and the challenge is unique per attempt, an attacker who observes network traffic gets neither a reusable credential (defeats replay) nor material to reconstruct the key elsewhere (defeats cloning); the remaining design work is making the message formats explicit and making account recovery, the one path that necessarily does not depend on the original key, resistant to abuse.
Structured elaboration
Message formats. Two request/response pairs are needed, both JSON over TLS:
Enrollment request (client to server):
{
"device_id": "b7e2...",
"public_key": "base64-encoded-P256-public-key",
"key_attestation": "base64-encoded-attestation-object",
"app_instance_nonce": "server-issued, single-use, from a prior /enroll/start call"
}
Enrollment response (server to client): {"credential_id": "uuid", "status": "enrolled"}, after the server has verified the attestation object chains to a trusted hardware root and the nonce matches an unexpired, unused value it issued.
Authentication challenge (server to client, in response to a login attempt): {"challenge": "32-byte random, base64", "credential_id": "uuid", "expires_at": "unix-ts, +60s"}.
Authentication response (client to server):
{
"credential_id": "uuid",
"signature": "base64(sign(challenge || rp_id || timestamp, private_key))",
"timestamp": "unix-ts",
"device_id": "b7e2..."
}
Signing the concatenation of the challenge, a relying-party identifier, and a client timestamp (not the bare challenge alone) is deliberate: it is what lets the server reject a signature that is valid in isolation but was produced for a different service, or replayed outside its validity window, discussed next.
Enrollment flow. Enrollment must happen inside an already-authenticated session (the user has just logged in via an existing credential, or this is first-time account setup gated by an out-of-band verification like email or SMS confirmation, not a fresh anonymous request). The server issues a single-use nonce, the app asks the secure enclave to generate a P-256 key pair scoped to this app/service, retrieves a key attestation from the enclave (a certificate chain proving the key was generated inside genuine, unextractable hardware, not software), and sends the public key plus attestation to the server. The server verifies the attestation chain against the vendor's known root certificates before accepting the key; skipping this check is the most common corner cut, since without it a compromised or emulated client could submit a software-generated key pair claiming to be enclave-backed, and the server would have no way to tell the difference later.
Authentication flow. The client requests a challenge for a specific credential_id. The server generates a cryptographically random, single-use challenge with a short expiry (30 to 60 seconds is typical) and records it as issued-but-unconsumed. The enclave signs challenge || rp_id || timestamp using the private key, which never leaves the hardware boundary, and the client sends the signature back. The server verifies the signature against the stored public key, checks the challenge matches an issued-and-unconsumed one, checks the timestamp is within the challenge's validity window, and then marks the challenge consumed.
Preventing replay. Two layers combine: the challenge is single-use (consumed on first successful verification, and any resubmission is rejected outright regardless of signature validity), and it has a short expiry independent of consumption, so a captured signature cannot be held and replayed even seconds later against a system clock that has not yet marked it consumed for some operational reason (a crashed request, a retried client). The signed timestamp inside the payload is a second, independent check: even if a challenge were somehow reissued or reused due to a server bug, a stale timestamp outside the tolerance window fails verification on its own.
Preventing key cloning. The defense here is architectural, not just protocol-level: the private key is generated inside the secure enclave with the "non-exportable" flag set, so the only operation available on it is "sign," never "export" or "read." Verifying this at enrollment via key attestation (checking that the public key really was generated inside the claimed hardware, not just claimed to be) closes the gap where a compromised app or a rooted/jailbroken device fakes an enclave-backed key with an ordinary software key pair the attacker can copy freely. A secondary defense is binding the credential to a specific device_id reported at enrollment and re-verifying it loosely at each authentication, so a stolen credential ID paired with a signature from different device fingerprint patterns is at least a detectable anomaly, even though the cryptography alone would still verify correctly (device_id is not itself cryptographically bound the way the signed payload is, so this is a detection signal, not a guarantee).
Account recovery abuse. Recovery is the one flow that, by construction, must succeed without the original private key, since the whole scenario for needing it is that the key is gone (device lost, wiped, or replaced). This makes it the natural target for an attacker, and it needs to be strictly harder than normal authentication, not easier: require a second, independently-registered device or authenticator where one exists; where none exists, route through identity re-verification against data the server already holds (never a factor the attacker could plausibly also produce, like "the email you signed up with" alone) plus a mandatory delay and a notification to the account's existing verified contact channels before the recovery takes effect, so a legitimate user has a window to notice and stop an attacker-initiated recovery. Re-enrollment of a new device credential should always be logged and surfaced to the user as a security event, since a silent successful recovery is indistinguishable from a takeover until the real user notices something is wrong.
Worked example
sequenceDiagram
participant App as Mobile app
participant SE as Secure enclave
participant S as Server
Note over App,S: Enrollment (inside authenticated session)
App->>S: POST /enroll/start
S-->>App: nonce (single-use)
App->>SE: generate key pair (non-exportable)
SE-->>App: public_key, attestation
App->>S: POST /enroll {public_key, attestation, nonce}
S->>S: verify attestation chain, verify nonce unused
S-->>App: credential_id
Note over App,S: Authentication
App->>S: POST /auth/challenge {credential_id}
S-->>App: challenge (random, expires in 60s)
App->>SE: sign(challenge || rp_id || timestamp)
SE-->>App: signature
App->>S: POST /auth/verify {credential_id, signature, timestamp}
S->>S: verify signature, challenge unconsumed, timestamp in window
S-->>App: session token
Trace a captured-and-replayed signature against this design: an attacker on the same network captures the /auth/verify request, including a valid signature, and resubmits it one second later. The server's challenge-consumption check rejects it immediately, because that challenge value was already marked consumed on the first, legitimate submission; the signature being cryptographically valid is irrelevant once the challenge itself is spent. If the attacker instead waits and resubmits an old, never-consumed challenge from an earlier aborted session, the expiry check (the 60-second window recorded at issuance) rejects it on the grounds that the challenge itself has expired, independent of whether it was ever consumed. Both checks have to be present because they defend against different failure modes: consumption defends against replay of a just-used challenge, expiry defends against replay of an old, abandoned one.
Trade-offs and pitfalls
- Skipping key attestation verification at enrollment is the single biggest silent failure mode: the protocol's entire "keys never leave hardware" guarantee depends on the server actually confirming that at enrollment time; without it, a compromised client can enroll an ordinary exportable key pair that looks identical to a real one from that point forward.
- Binding the signed payload to a relying-party identifier and timestamp, not just the bare challenge, costs almost nothing and closes cross-service replay: without it, a signature obtained by one service (or a malicious relying party the user also trusts) could in principle be replayed against another service sharing the same credential scheme.
- The device_id check is a detection signal, not a cryptographic guarantee, and should not be relied on as the primary anti-cloning defense; treating it as sufficient on its own is a common shortcut that understates the actual protection the enclave provides.
- Recovery flows are where teams under support pressure quietly weaken the design: a "just email us and we'll reset it" path that skips identity re-verification and delay/notification defeats the entire premise of hardware-bound, non-exportable keys, because the attacker simply routes around the cryptography through the recovery process instead.
- Challenge expiry and challenge consumption are not redundant with each other; a design that implements only one of the two leaves a real gap (a long-lived unconsumed challenge, or a consumed-but-not-expiry-checked challenge under a clock or storage bug) that the other check exists specifically to close.
Explain how to implement proof-of-possession (PoP) tokens or token binding for mobile clients to reduce token replay risk. Cover key generation and storage on device (secure enclave / keystore), enrollment flows, how the server verifies possession, rotation, and limitations for iOS and Android platforms.
Sample Answer
Direct answer
Proof-of-possession (PoP), the property that a bearer of a token must also prove it holds a specific private key, not just present the token string, turns a token from "whoever has this string can use it" into "whoever holds this specific hardware-protected key can use it." On mobile, that means generating an asymmetric key pair inside the device's dedicated secure hardware (the iOS Secure Enclave or the Android hardware-backed Keystore) so the private key never exists anywhere the app's own process, an attacker's malware, or a stolen token-string replay could reach it, then having the server verify a fresh signature over each request (or a short-lived proof) rather than trusting the token string alone. The current standardized mechanism for this at the application/OAuth2 layer is DPoP (Demonstrating Proof of Possession, RFC 9449, published 2023), which binds an access token to a client-held key via a signed proof sent alongside it.
Structured elaboration
Key generation and storage on device. On iOS, the Secure Enclave (a dedicated hardware security coprocessor separate from the main CPU) can generate a key pair such that the private key never leaves the enclave and all signing happens inside it; the real, documented constraint is that Secure Enclave keys are elliptic-curve only (P-256), not RSA, and are usable only for signing/verification, not for encryption/decryption. On Android, the hardware-backed Keystore serves the same role: on devices with a StrongBox Keymaster (a discrete secure element, available since Android 9 / API level 28, though hardware support varies significantly by manufacturer and device tier) keys are generated and used inside dedicated secure hardware; on devices without StrongBox, keys still get TEE (Trusted Execution Environment)-backed protection, a weaker but still hardware-isolated guarantee, and the app can query which level it actually got at runtime rather than assuming the strongest case.
Enrollment flow.
sequenceDiagram
participant D as Device
participant H as Secure Enclave / Keystore
participant A as Auth Server
Note over D,H: Enrollment
D->>H: generate key pair (non-exportable)
H-->>D: public key
D->>A: register public key for this device + account
Note over D,H: Per-request proof
A->>D: challenge / request context
D->>H: sign(challenge)
H-->>D: signature
D->>A: access_token + signature
A->>A: verify signature against stored public key
A-->>D: request authorized (token bound to this device)
At enrollment, the app asks the platform's secure hardware to generate a non-exportable key pair, receives only the public key back (the private key cannot leave the hardware even if the app itself is compromised), and registers that public key with the server against the account and device. Both platforms support key attestation (a certificate chain, signed by the manufacturer, proving the key really was generated inside genuine secure hardware rather than software pretending to be): Android's Keystore attestation (available since Android 7.0) and Apple's App Attest (introduced with iOS 14, which uses a Secure Enclave-generated attestation key verified by Apple's own servers). A server that skips attestation verification cannot actually tell a hardware-backed key from a software-simulated one claiming to be hardware-backed.
How the server verifies possession per request. Rather than trusting the access token string alone, the server requires a fresh, short-lived proof for each request (or a small batch of requests within a tight window): a DPoP-style proof is a JWT (JSON Web Token), signed by the device's private key, containing the HTTP method, the URL, a timestamp, and a unique identifier (jti), which the server checks against the public key it stored at enrollment, rejects if the timestamp is stale or the jti has been seen before (replay protection), and confirms matches the specific access token being presented (binding the two together so a stolen token string alone, without the private key, is useless).
Rotation. The device-held key pair should rotate periodically (e.g. on a fixed schedule, or on events like a biometric re-enrollment or an app reinstall), following the same enrollment flow again to register the new public key while the old one remains valid for a short overlap window so in-flight requests signed just before rotation are not rejected. Losing the ability to sign with the old key (a lost or wiped device) simply means that device's registration is dead; the account's other enrolled devices are unaffected, since each device has its own independent key pair.
Worked example
A banking app enrolls a user's phone: the Secure Enclave generates key pair K1, the app registers K1's public half with the server along with an App Attest certificate proving K1 was genuinely generated in that phone's secure hardware. Weeks later, an attacker who has captured the user's access token string (say, via a compromised third-party SDK bundled in a different app that logged network traffic) tries to replay it from their own machine. The request reaches the server with the stolen token but no valid proof: the attacker does not hold the phone's Secure Enclave, so they cannot produce a signature over the current request that verifies against K1's registered public key. The server rejects the request, and the stolen token string alone accomplishes nothing. Meanwhile, the legitimate user's next real request from their phone signs a fresh proof (a new timestamp, a new jti) with K1 inside the enclave; the server verifies it against the same registered public key and authorizes the request normally, with the user experiencing no difference from an ordinary token-only flow.
Trade-offs and pitfalls
iOS limitations: Secure Enclave's elliptic-curve-only, sign/verify-only constraint means the design must be built around signing a proof rather than any scheme that would need the enclave itself to perform encryption or use RSA; App Attest also has practical friction (rate limits on attestation calls, and it requires a real network round trip to Apple's servers to validate, which needs its own fallback story for a fully offline enrollment attempt). Android limitations: StrongBox availability is inconsistent across the device fleet (present on flagship-tier devices, often absent on budget hardware, meaning the server must accept and clearly track a lower-assurance TEE-only attestation level for those devices rather than requiring StrongBox universally), and because Android is a more open platform, a rooted device can, in the worst case, undermine assumptions that hold more reliably on iOS's tighter hardware/software integration; attestation is exactly the tool that lets the server detect and treat rooted or emulated devices differently rather than pretending the guarantee is uniform.
Cross-cutting pitfalls. The most common mistake is verifying the DPoP-style proof's signature but skipping key attestation entirely, which silently downgrades the whole design to "any key the app claims to hold," since without attestation the server has no way to confirm the key was ever actually generated in secure hardware rather than in ordinary app memory. The second is treating enrollment as a one-time event with no rotation story, which means a single compromised key (extracted once, however unlikely given the hardware protections) stays valid for the life of the account rather than for a bounded window. Recovery flows deserve equal design attention: a user who loses their only enrolled device needs an account-recovery path that does not simply fall back to a weaker, unbound credential, or the whole hardware-backed design is only as strong as its escape hatch.
Explain Proof Key for Code Exchange (PKCE) and how it enhances the OAuth2 authorization code flow for public clients (native apps and SPAs). Describe, step-by-step, how to generate and validate the code_verifier and code_challenge, which hashing method to use, where values should be stored, and how PKCE prevents authorization-code interception attacks.
Sample Answer
Direct answer
Proof Key for Code Exchange (PKCE, usually pronounced "pixy") is an extension to the OAuth 2.0 authorization code flow that lets a public client, one that cannot safely hold a secret, such as a native mobile app or a browser-based single-page application (SPA), prove that it is the same party that started the authorization request when it later redeems the authorization code for tokens. It replaces the client secret a public client can't keep anyway with a dynamically generated, single-use proof.
Structured elaboration
The step-by-step mechanics:
- Before starting the authorization request, the client generates a
code_verifier: a cryptographically random string, 43 to 128 characters long, built only from unreserved URL-safe characters (letters, digits,-,.,_,~). - The client derives a
code_challengefrom it:code_challenge = BASE64URL-ENCODE(SHA256(code_verifier)). This is theS256method, and it is the only method that should be used in practice. The spec also defines a legacyplainmethod where the challenge equals the verifier in cleartext; that provides no protection against anyone who can observe the authorization request, so treatS256as mandatory rather than a preference. - The client sends the authorization request to the identity provider, including
code_challengeandcode_challenge_method=S256alongside the usualclient_id,redirect_uri, andscope. The verifier itself is never sent at this step, only its hash. - The identity provider stores the
code_challengeagainst the authorization code it is about to issue. - After the user authenticates and consents, the identity provider redirects back to the client with the authorization code.
- The client exchanges that code at the token endpoint, this time including the original
code_verifierin plaintext. - The identity provider recomputes
SHA256(code_verifier), base64url-encodes the result, and compares it against the storedcode_challenge. It issues tokens only if they match.
Where values live: the code_verifier is held only in the requesting client's own memory or session-scoped storage for the duration of this single flow, whether that's an in-memory variable in a mobile app or sessionStorage in a browser SPA. It is never transmitted anywhere except the final token-exchange request, and it is never persisted beyond that one flow.
Why this stops authorization-code interception: possessing the authorization code is no longer sufficient to obtain tokens. The code travels over one channel (a redirect the operating system or browser routes), while the verifier travels over a separate, direct request straight from the legitimate client to the token endpoint. An attacker who only intercepts the redirect never sees the verifier.
This also explains the difference in how SPAs and server-rendered apps (SSR) approach PKCE. A SPA runs entirely inside the browser and is a public client by construction: anything it holds, including a would-be secret, is visible to anyone inspecting the page, so it must use the authorization code flow with PKCE. An SSR application has a confidential backend that can hold a real client secret and perform the code exchange server-side, so PKCE was historically considered optional there. Current guidance still recommends it everywhere as defense in depth: RFC 9700, the IETF's published OAuth 2.0 security best-current-practice document, recommends PKCE for public clients and separately deprecates the older implicit grant; the still-in-draft OAuth 2.1 goes further and would make PKCE mandatory for every client type, confidential or public.
Worked example
Trace the attack PKCE exists to close:
- Legitimate mobile app A registers a custom URL scheme, say
myapp://callback, to receive the OAuth redirect after login. - A malicious app B, installed on the same device, registers that exact same custom URL scheme. Nothing on many mobile platforms prevents two different apps from claiming the same scheme.
- A victim starts the login flow in the legitimate app A. The identity provider authenticates them and redirects to
myapp://callback?code=abc123. - The operating system cannot disambiguate which app should receive that redirect, and may hand it to the malicious app B instead of the legitimate app A.
- Without PKCE: app B is a public client with only a
client_id, which is not secret information (it's typically bundled directly inside the app binary). It calls the token endpoint directly with the interceptedcode=abc123and receives a valid access token for the victim's account. - With PKCE: app B has the intercepted code, but not the
code_verifierthat legitimate app A generated and is holding privately in its own memory. Its token-exchange attempt fails theSHA256comparison in step 7 above, and the stolen code is worthless on its own.
Trade-offs and pitfalls
- PKCE protects the code-for-token exchange specifically; it does not replace transport security. Redirect URIs must still be validated exactly, and the authorization endpoint and token endpoint both still require TLS.
- Accepting the
plaincode_challenge_methoddefeats the purpose against any attacker capable of observing the authorization request itself, which some network-level positions can do. TreatS256as the only acceptable choice;plainexists mainly for constrained clients that cannot compute a SHA256 hash, which is effectively no client in practice today. - The identity provider, not just the client, has to enforce PKCE correctly: it must require a
code_verifierat redemption whenever acode_challengewas present at issuance. If an authorization server would silently accept the code without a verifier, a downgrade attack strips PKCE's protection entirely, and a well-behaved client cannot compensate for a server that skips this check.
That is every published Identity, Authentication, and Access Management question for Mobile Developer so far. Browse the other topics in this category, or practice this one interactively.