DPoP proof JWT on API calls (with nonce retry per RFC 9449 §8/§9)#4060
DPoP proof JWT on API calls (with nonce retry per RFC 9449 §8/§9)#4060sfdctaka wants to merge 13 commits into
Conversation
- Add SFSDKDPoPRequestDecorator.sendWithNonceRetry helper: harvest DPoP-Nonce on every response, retry once on use_dpop_nonce challenge, surface NSError on second consecutive challenge. - Wire helper into REST API, identity coordinator, and userinfo. Photo path harvests only (cosmetic, no retry), gated on DPoP credentials so Bearer logins are unaffected. - Add kSFOAuthErrorDPoPNonceExhausted to the OAuth error enum for the terminal nonce-exhaustion case.
Nonce support
wmathurin
left a comment
There was a problem hiding this comment.
LGTM - but if nonce are sent by the token end point, we might be able to simplify the code a bit? Harvest the nonce whenever a refresh call happens. And also treat a nonce failure like 401s (to force a refresh and therefore to get a new nonce).
Let me look into this now. |
Reviewer feedback noted that since the token endpoint is the only source of DPoP-Nonce headers, the resource-server retry helper was redundant — a nonce rejection has the same client-side effect as a 401, and the existing 401-refresh-replay path already harvests a fresh nonce during the refresh and re-stamps the proof on replay via applyAuthHeaders + DPoPNonceCache.latest(forScope:). - Remove DPoPRequestDecorator.sendWithNonceRetry(...) - Remove kSFOAuthErrorDPoPNonceExhausted - Revert SFRestAPI, SFIdentityCoordinator, SFUserAccountManager callsites to plain network sendRequest:dataResponseBlock: - Keep applyAuthHeaders pre-stamp at each resource-server callsite - Update DPoPNonceCache doc comment to reflect token-endpoint-only harvest design - Drop SFSDKDPoPTests retry-helper test block and DPoPNonceExhausted symbol test; keep Bearer-photo cache-leak regression test
|
@wmathurin Dropped the resource-server reactive-retry layer and folded nonce failures into the existing 401→refresh→replay path, per your suggestion. |
If applyAuthHeaders returns an error, sending the request without the auth header is pointless — the server will reject it. Surface the failure to the caller (delegate / completion / error block) and return instead of issuing an unauthenticated request.
I love the simplification. I see that the nonce cache is still in memory only so an application restart will force a refresh (because of missing the nonce for creating the JWT proof). Should we persist the nonce to disk (alongside the access token and other user credentials) to avoid that situation? Or maybe we could store the JWT proof that we generate from the nonce (as long as there is a 1-1 mapping from nonce to JWT proof)? |
This PR extends DPoP coverage from the token endpoint to the SDK's authenticated REST and identity calls, plus adds the nonce-retry machinery the resource server requires per RFC 9449 §8/§9.
What's included
DPoP on outbound API calls
SFRestRequest/SFRestAPI: whentokenType == "DPoP", requests are stamped withAuthorization: DPoP <access_token>and aDPoPproof header bound to the request's method (htm) and URL (htu), with the access-token hash (ath) per RFC 9449 §4.3.SFIdentityCoordinator: same stamping for the identity endpoint.Authorization: Bearer …is byte-identical to pre-DPoP output for non-DPoP token responses.Nonce challenge + retry (RFC 9449 §8 / §9)
DPoPNonceCache: per-account, per-htucache of the most recent server-issued nonce. Harvested from any 200, 400, or 401 response carrying aDPoP-Nonceheader.noncewhen the cache has one for the targethtu.DPoP-Nonce(or a 400 body containingerror=use_dpop_nonce), the originating request is retried once with the freshly harvested nonce. Retry is one-shot to avoid loops.Key material
DPoPKeyStore: per-account ES256 / P-256 keypair, generated on first DPoP use and persisted in the keychain. The private key never leaves secure storage. cnf.jkt thumbprint computed via JWK SHA-256.Token-type wiring
SFOAuthCredentials.tokenType(added in earlier PR) drives scheme selection."DPoP"⇒ DPoP path; anything else (including nil) ⇒ Bearer path.Backward compatibility
Purely additive. Non-DPoP orgs see no behavior change. The new headers are only emitted when the token endpoint returns
token_type=DPoP.Test coverage
SFSDKDPoPTests(988 lines): proof builder, nonce cache (per-account isolation, per-htukeying, eviction), request decorator (Bearer vs DPoP branch, nonce attachment, ath computation), end-to-end request stamping, retry on nonce challenge.SFOAuthCredentialsTestsand token-endpoint response tests updated.Notes for reviewers