Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@
request.successBlock = successBlock;

if (!self.requiresAuthentication) {
NSAssert(!request.requiresAuthentication , @"Use SFRestAPI sharedInstance for authenticated requests");

Check failure on line 295 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/RestAPI/SFRestAPI.m

View workflow job for this annotation

GitHub Actions / ios-pr (SalesforceSDKCore, ^18) / test-ios

testLogin, Use SFRestAPI sharedInstance for authenticated requests (NSInternalInconsistencyException)
}

// Adds this request to the list of active requests if it's not already on the list.
Expand Down Expand Up @@ -364,6 +364,22 @@
request.successBlock(dataForDelegate, response);
}
} else {
// DPoP nonce challenge (HTTP 400 with use_dpop_nonce in body): harvest the
// server-issued nonce and retry the request once with the updated proof.
// This covers the post-restart case where the in-memory nonce cache is empty
// and the first outbound DPoP call (e.g. revoke) triggers a nonce challenge.
// RFC 9449 §8 — the server SHOULD return the desired nonce in DPoP-Nonce.
NSString *bodyStr = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
if (statusCode == 400
&& !request.dpopNonceRetried
&& [bodyStr containsString:SFSDKDPoPRequestDecorator.nonceErrorCode]) {
request.dpopNonceRetried = YES;
[SFSDKDPoPRequestDecorator harvestNonceFromResponse:response
requestURL:finalRequest.URL
scope:strongSelf.user.credentials.identifier];
[strongSelf enqueueRequest:request shouldRetry:shouldRetry];
return;
}
if (shouldRetry && [strongSelf shouldRetryTask:dataTask withData:data]) {
[strongSelf replayRequest:request response:response];
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) SFRestRequestFailBlock failureBlock;
@property (nonatomic, copy, nullable) SFRestResponseBlock successBlock;

/// Set to YES after a DPoP nonce-challenge retry so the retry fires at most once per request.
@property (nonatomic, assign) BOOL dpopNonceRetried;


+ (nonnull NSString *)restUrlForBaseUrl:(nullable NSString *)baseUrl serviceHostType:(SFSDKRestServiceHostType)hostType credentials:(nonnull SFOAuthCredentials *)credentials;
+ (NSString *)toQueryString:(nullable NSDictionary *)components;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,29 @@ class DPoPLoginTests: BaseAuthFlowTester {

// MARK: - Restart

/// Restart app after DPoP login and verify session and keypair persist.
/// Restart app after DPoP login; verify the EC keypair and session survive, and that
/// revoke+refresh works despite the in-memory nonce cache being empty after restart.
///
/// Skipped pending SDK fix: on iOS, revoke after app restart fails because the DPoP
/// nonce cache is in-memory only and there is no nonce-challenge retry on the
/// `RestClient` request path (only the token endpoint retries). Android's equivalent
/// test passes only because its revoke goes over raw OkHttp on the login host,
/// bypassing DPoP entirely — iOS revokes go to the instance host through the
/// DPoP-decorating REST stack.
/// After restart the nonce cache is cold. The first revoke call sends a nonce-less DPoP
/// proof; the server returns HTTP 400 `use_dpop_nonce`. `SFRestAPI.enqueueRequest` now
/// detects this, harvests the server-issued nonce from the response header, and retries
/// the request once with the updated proof (W-23501382).
func test_givenDPoPUser_whenAppRestart_thenSessionAndKeypairSurvive() throws {
throw XCTSkip("TODO: Pending SDK fix: RestClient path lacks nonce-challenge retry; post-restart DPoP revoke fails because in-memory nonce cache is empty.")
launchLoginAndValidate(
loginHost: .regularAuth,
user: .third,
staticAppConfigName: .ecaJwtDpop,
useHybridFlow: false,
useDPoP: true
)

restartAndValidateUser(
loginHost: .regularAuth,
user: .third,
userAppConfigName: .ecaJwtDpop,
useHybridFlow: false
)
assertRevokeAndRefreshWorks(isRtr: false, isDPoP: true, useHybridFlow: false, isJwt: true)
}

// MARK: - Pool Server Login
Expand Down
2 changes: 1 addition & 1 deletion native/SampleApps/AuthFlowTester/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ All DPoP tests live here — basic login, RTR, multi-user, migration, server enf
| `testLogin_DPoP_ECA_Without_DPoP_Fails` | ECA JWT DPoP | — | Server enforcement: DPoP-enforced ECA rejects login without DPoP (`useDPoP: false`); no account created |
| `test_givenBearerSession_whenUpgradeToDPoP_thenDPoPBound` | ECA JWT → ECA JWT DPoP | — | Bearer → DPoP in-place upgrade via `UserAccountManager.upgradeToDPoP`; consumer key unchanged, `token_type: "DPoP"` post-upgrade |
| `test_givenDPoPSession_whenDowngradeFromDPoP_thenBearerUnbound` | ECA JWT | — | DPoP → Bearer in-place downgrade via `UserAccountManager.downgradeFromDPoP`; consumer key unchanged, `token_type` no longer `"DPoP"` post-downgrade. Runs on the DPoP-*optional* ECA JWT app (a DPoP-*enforced* app would reject the downgrade's unbound `/authorize` request) |
| `test_givenDPoPUser_whenAppRestart_thenSessionAndKeypairSurvive` | ECA JWT DPoP | — | `XCTSkip` (pending SDK fix — RestClient path lacks nonce-challenge retry; post-restart DPoP revoke fails because in-memory nonce cache is empty) |
| `test_givenDPoPUser_whenAppRestart_thenSessionAndKeypairSurvive` | ECA JWT DPoP | — | EC keypair persists in Keychain; revoke succeeds after restart via nonce-challenge retry |
| `test_givenDPoP_whenLoginViaPoolServer_thenTokenTypeIsDPoP` | ECA JWT DPoP | — | Pool server login with DPoP; `dpop_jkt` accepted; L1 (production) marker in UA |
| `test_givenDPoPECA_whenAdminLogin_thenDPoPBindingWorksThroughBrowser` | ECA JWT DPoP | — | Login for Admins hand-off to ASWebAuthenticationSession works with DPoP binding |

Expand Down
Loading