From 0fea82ff267330172366539d28718761ad1a3428 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 1 Oct 2026 13:46:42 -0700 Subject: [PATCH 1/5] @W-24342940: Fix DPoP nonce reuse across hosts for community logins Community/Experience Cloud resource servers (identity, REST) never issue their own DPoP-Nonce challenge; they hard-reject a proof with no nonce instead. The client must reuse the nonce most recently issued at the /token endpoint. DPoPNonceCache was keyed strictly by (credentialsIdentifier, host) with no fallback, so a nonce harvested for the login/token host was never available when building a proof for a different resource host, causing community DPoP logins to fail. DPoPNonceCache.get() now falls back to the most recently stored nonce for the credentialsIdentifier (any host) when there is no entry for the exact host. An exact host match always takes precedence. clear()/clearAll() also remove the fallback entry. This mirrors the credential-scoped fallback already used by the iOS implementation. --- .../androidsdk/auth/dpop/DPoPNonceCache.kt | 22 ++++++++-- .../auth/dpop/DPoPNonceCacheTest.kt | 40 +++++++++++++++++++ .../auth/dpop/DPoPRequestDecoratorTest.kt | 38 ++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt index a250ea51a6..fdc0655c80 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt @@ -35,28 +35,44 @@ import java.util.concurrent.ConcurrentHashMap * client must echo that value in the `nonce` claim of its next DPoP proof for the * same endpoint. This cache is keyed by `(credentialsIdentifier, host)` so that * the AS nonce (login host) and RS nonce (instance host) never overwrite each other. - * This matches the per-host isolation used by the iOS implementation, while also - * ensuring per-user isolation consistent with [DPoPKeyManager]. + * + * Some resource servers (e.g. communities/Experience Cloud sites) never issue their + * own `DPoP-Nonce` challenge; they expect the client to carry forward whatever nonce + * was last issued for that credential, regardless of host. To support that, [get] + * falls back to the most recently stored nonce for [credentialsIdentifier] (any host) + * when there is no entry for the exact `(credentialsIdentifier, host)` pair. An exact + * host match always takes precedence over the fallback. This matches the credential-scoped + * fallback used by the iOS implementation (`DPoPNonceCache.swift`'s `nonce(htu:scope:) ?? + * latest(forScope:)`), layered on top of Android's existing per-host isolation. */ object DPoPNonceCache { private val cache = ConcurrentHashMap() + private val latestByCredential = ConcurrentHashMap() private fun cacheKey(credentialsIdentifier: String, host: String) = "$credentialsIdentifier|$host" + /** + * Returns the nonce cached for the exact `(credentialsIdentifier, host)` pair, or, + * if none was ever stored for that host, the most recently stored nonce for + * [credentialsIdentifier] on any host. Returns null if neither is available. + */ fun get(credentialsIdentifier: String, host: String): String? = - cache[cacheKey(credentialsIdentifier, host)] + cache[cacheKey(credentialsIdentifier, host)] ?: latestByCredential[credentialsIdentifier] fun store(credentialsIdentifier: String, host: String, nonce: String) { cache[cacheKey(credentialsIdentifier, host)] = nonce + latestByCredential[credentialsIdentifier] = nonce } fun clear(credentialsIdentifier: String) { cache.keys.removeAll { it.startsWith("$credentialsIdentifier|") } + latestByCredential.remove(credentialsIdentifier) } fun clearAll() { cache.clear() + latestByCredential.clear() } } diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt index 1194c0b090..8d7fced710 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt @@ -90,6 +90,46 @@ class DPoPNonceCacheTest { assertEquals("RS nonce must be retrievable by instance host", rsNonce, DPoPNonceCache.get(id, instanceHost)) } + /* + * W-24342940: some resource servers (e.g. communities/Experience Cloud sites) never issue + * their own DPoP-Nonce challenge; they expect the client to carry forward the nonce most + * recently issued for that credential, regardless of host. When no nonce was ever stored + * for the exact (credentialsIdentifier, host) pair, get() must fall back to the most + * recently stored nonce for that credential on any host. + */ + @Test + fun test_givenNonceStoredForOtherHost_whenGetForUnseenHost_thenFallbackNonceReturned() { + val tokenHost = "community.my.site.com" + val resourceHost = "community.my.salesforce.com" + DPoPNonceCache.store(id, tokenHost, "token-host-nonce") + assertEquals("token-host-nonce", DPoPNonceCache.get(id, resourceHost)) + } + + @Test + fun test_givenExactHostMatch_whenFallbackAlsoAvailable_thenExactMatchTakesPrecedence() { + DPoPNonceCache.store(id, loginHost, "fallback-nonce") + DPoPNonceCache.store(id, instanceHost, "exact-nonce") + assertEquals("exact-nonce", DPoPNonceCache.get(id, instanceHost)) + } + + @Test + fun test_givenFallbackNonceStored_whenClear_thenFallbackIsAlsoRemoved() { + DPoPNonceCache.store(id, loginHost, "token-host-nonce") + DPoPNonceCache.clear(id) + assertNull(DPoPNonceCache.get(id, "some.other.host.salesforce.com")) + } + + @Test + fun test_givenTwoDifferentIdentifiers_whenOneHasFallbackOnly_thenOtherIdentifierDoesNotLeak() { + val id2 = "user2" + DPoPNonceCache.store(id, loginHost, "user1-nonce") + // id2 never stores anything, including for loginHost or any other host. + assertNull(DPoPNonceCache.get(id2, loginHost)) + assertNull(DPoPNonceCache.get(id2, instanceHost)) + // id's fallback must still resolve correctly for a host it never used directly. + assertEquals("user1-nonce", DPoPNonceCache.get(id, instanceHost)) + } + @Test fun test_givenConcurrentWrites_whenMultipleThreads_thenNoRace() { val threadCount = 20 diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt index 97eea51d5b..6466fdb103 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt @@ -26,6 +26,7 @@ */ package com.salesforce.androidsdk.auth.dpop +import android.util.Base64 import androidx.test.ext.junit.runners.AndroidJUnit4 import com.salesforce.androidsdk.accounts.UserAccount import com.salesforce.androidsdk.accounts.UserAccountBuilder @@ -34,6 +35,7 @@ import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody +import org.json.JSONObject import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -291,6 +293,42 @@ class DPoPRequestDecoratorTest { } } + private fun decodeJson(segment: String): JSONObject = JSONObject( + String( + Base64.decode(segment, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP), + Charsets.UTF_8, + ), + ) + + /* + * W-24342940: a community/Experience Cloud resource server never issues its own + * DPoP-Nonce challenge; it rejects a proof carrying no nonce. The client must reuse the + * nonce harvested from the /token host when attaching a proof for a different (resource) + * host under the same credential. + */ + @Test + fun applyAuthHeaders_dpopAccount_nonceHarvestedOnDifferentHost_fallsBackToLatestNonce() { + DPoPNonceCache.clearAll() + try { + seedKeyPair(testScope) + DPoPNonceCache.store(testScope, "community.my.site.com", "token-host-nonce") + + val builder = Request.Builder().url("https://community.my.salesforce.com/id/orgId/userId").get() + DPoPRequestDecorator.applyAuthHeaders(builder, userAccount(tokenType = "DPoP")) + + val proof = builder.build().header(DPoPRequestDecorator.DPOP_HEADER) + assertNotNull("Expected a DPoP proof header", proof) + val payload = decodeJson(proof!!.split(".")[1]) + assertEquals( + "Expected the /token-host nonce to be reused for the resource host", + "token-host-nonce", + payload.getString("nonce"), + ) + } finally { + DPoPNonceCache.clearAll() + } + } + @Test fun applyAuthHeaders_lowercaseDPoPTokenType_stampsDPoPSchemeNotBearer() { // Regression for W-24027018: server returns lowercase "dpop" in token refresh responses. From a82d54216e5f6a8218a22214ea8099017294f0ae Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 1 Oct 2026 14:06:17 -0700 Subject: [PATCH 2/5] @W-24342940: Clarify DPoPNonceCache doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct the class/get() KDoc and matching test comments: Salesforce issues DPoP-Nonce only from the token endpoint, never from a resource server (identity, REST) — this isn't specific to communities. Resource-server responses never carry DPoP-Nonce, so the client reuses the latest token-endpoint nonce for the credential on every DPoP call; an exact per-host entry still takes precedence if a server ever does issue one. Logins where the token host differs from the resource hosts (communities, login.* pool servers) rely on this fallback. --- .../androidsdk/auth/dpop/DPoPNonceCache.kt | 29 ++++++++++--------- .../auth/dpop/DPoPNonceCacheTest.kt | 10 +++---- .../auth/dpop/DPoPRequestDecoratorTest.kt | 6 ++-- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt index fdc0655c80..2ba03dc334 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt @@ -31,19 +31,21 @@ import java.util.concurrent.ConcurrentHashMap /** * Thread-safe in-memory nonce cache for DPoP proof JWTs. * - * RFC 9449 §8 allows the AS/RS to supply a `DPoP-Nonce` response header. The - * client must echo that value in the `nonce` claim of its next DPoP proof for the - * same endpoint. This cache is keyed by `(credentialsIdentifier, host)` so that - * the AS nonce (login host) and RS nonce (instance host) never overwrite each other. + * RFC 9449 §8 allows a server to supply a `DPoP-Nonce` response header. Salesforce + * only ever issues nonces from the token endpoint — resource-server responses + * (identity, REST) never carry `DPoP-Nonce`, on success or on rejection. This cache + * is keyed by `(credentialsIdentifier, host)` so a per-host nonce, if a server ever + * does supply one, takes precedence. * - * Some resource servers (e.g. communities/Experience Cloud sites) never issue their - * own `DPoP-Nonce` challenge; they expect the client to carry forward whatever nonce - * was last issued for that credential, regardless of host. To support that, [get] - * falls back to the most recently stored nonce for [credentialsIdentifier] (any host) - * when there is no entry for the exact `(credentialsIdentifier, host)` pair. An exact - * host match always takes precedence over the fallback. This matches the credential-scoped - * fallback used by the iOS implementation (`DPoPNonceCache.swift`'s `nonce(htu:scope:) ?? - * latest(forScope:)`), layered on top of Android's existing per-host isolation. + * Since resource servers don't issue their own nonce, [get] falls back to the most + * recently stored nonce for [credentialsIdentifier] (any host) when there is no entry + * for the exact `(credentialsIdentifier, host)` pair, so the client reuses the latest + * token-endpoint nonce on every DPoP call for that credential. An exact host match + * always takes precedence over the fallback. Logins where the token host differs from + * the resource hosts (e.g. communities, login.* pool servers) rely on this fallback. + * This matches the credential-scoped fallback used by the iOS implementation + * (`DPoPNonceCache.swift`'s `nonce(htu:scope:) ?? latest(forScope:)`), layered on top + * of Android's existing per-host isolation. */ object DPoPNonceCache { @@ -56,7 +58,8 @@ object DPoPNonceCache { /** * Returns the nonce cached for the exact `(credentialsIdentifier, host)` pair, or, * if none was ever stored for that host, the most recently stored nonce for - * [credentialsIdentifier] on any host. Returns null if neither is available. + * [credentialsIdentifier] on any host — i.e. the latest nonce issued at the token + * endpoint for this credential. Returns null if neither is available. */ fun get(credentialsIdentifier: String, host: String): String? = cache[cacheKey(credentialsIdentifier, host)] ?: latestByCredential[credentialsIdentifier] diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt index 8d7fced710..aba717cd28 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt @@ -91,11 +91,11 @@ class DPoPNonceCacheTest { } /* - * W-24342940: some resource servers (e.g. communities/Experience Cloud sites) never issue - * their own DPoP-Nonce challenge; they expect the client to carry forward the nonce most - * recently issued for that credential, regardless of host. When no nonce was ever stored - * for the exact (credentialsIdentifier, host) pair, get() must fall back to the most - * recently stored nonce for that credential on any host. + * W-24342940: Salesforce only issues DPoP-Nonce from the token endpoint; resource + * servers (identity, REST) never issue their own, so the client must carry forward + * the nonce most recently issued for that credential, regardless of host. When no + * nonce was ever stored for the exact (credentialsIdentifier, host) pair, get() must + * fall back to the most recently stored nonce for that credential on any host. */ @Test fun test_givenNonceStoredForOtherHost_whenGetForUnseenHost_thenFallbackNonceReturned() { diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt index 6466fdb103..b23c324b6c 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt @@ -301,9 +301,9 @@ class DPoPRequestDecoratorTest { ) /* - * W-24342940: a community/Experience Cloud resource server never issues its own - * DPoP-Nonce challenge; it rejects a proof carrying no nonce. The client must reuse the - * nonce harvested from the /token host when attaching a proof for a different (resource) + * W-24342940: resource servers (identity, REST) never issue their own DPoP-Nonce + * challenge; they reject a proof carrying no nonce. The client must reuse the nonce + * harvested from the /token host when attaching a proof for a different (resource) * host under the same credential. */ @Test From 72b15bd3aa9be4357392306915570ac686df8ee5 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 1 Oct 2026 15:04:44 -0700 Subject: [PATCH 3/5] @W-24342940: Update nonce harvest test for credential fallback Pre-seed the pre-redirect host with its own distinct nonce so the cross-host-redirect test still proves the harvest lands under the response host rather than the pre-redirect host. The new credential- wide fallback in DPoPNonceCache.get() made the old assertNull on the pre-redirect host pass vacuously (via the fallback) regardless of which host the harvest actually wrote to. --- ...AuthenticationUtilitiesIntegrationUserTest.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/AuthenticationUtilitiesIntegrationUserTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/AuthenticationUtilitiesIntegrationUserTest.kt index 6c27c4f2d7..50c31bfbc1 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/AuthenticationUtilitiesIntegrationUserTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/AuthenticationUtilitiesIntegrationUserTest.kt @@ -208,11 +208,22 @@ class AuthenticationUtilitiesIntegrationUserTest { * instance), the harvested nonce must be stored under the response's * host, not the pre-redirect request's host — otherwise a retry proof * built for the response's host never finds it. + * + * The pre-redirect host is pre-seeded with its own, distinct nonce so + * this is a real test of per-host storage rather than of + * [DPoPNonceCache.get]'s cross-host fallback (which would otherwise mask + * a regression here, since the fallback returns the credential's latest + * nonce for any host with no exact entry — including the pre-redirect + * host — and `instance-nonce` would satisfy both assertions below even + * if the harvest wrongly landed on the pre-redirect host). The exact + * `(credentialsIdentifier, "instance.test")` entry staying at the + * pre-seeded value proves the harvest never overwrote it. */ @Test fun test_fetchIsSalesforceIntegrationUser_crossHostRedirect_harvestsNonceUnderResponseHost() { generateOrLoadKeyPair(alias) clear(credentialsIdentifier) + store(credentialsIdentifier, "instance.test", "pre-redirect-nonce") httpAccess.enqueueIntegrationUserSuccess( isIntegrationUser = false, headers = mapOf("DPoP-Nonce" to "instance-nonce"), @@ -223,8 +234,9 @@ class AuthenticationUtilitiesIntegrationUserTest { fetchIsSalesforceIntegrationUser(tokenResponse, "https://login.salesforce.com") - assertNull( - "Nonce must not be stored under the pre-redirect request host", + assertEquals( + "Pre-redirect request host's own nonce must not be overwritten by the response host's harvest", + "pre-redirect-nonce", get(credentialsIdentifier, "instance.test") ) assertEquals( From 8b3740a20e4241b51416ba3a420267532439fff1 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 2 Oct 2026 16:58:51 -0700 Subject: [PATCH 4/5] @W-24342940: Address review - drop ticket IDs from test comments, soften nonce KDoc --- .../salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt | 11 ++++++----- .../androidsdk/auth/dpop/DPoPNonceCacheTest.kt | 4 ++-- .../androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt index 2ba03dc334..4bcd30dd71 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCache.kt @@ -32,12 +32,13 @@ import java.util.concurrent.ConcurrentHashMap * Thread-safe in-memory nonce cache for DPoP proof JWTs. * * RFC 9449 §8 allows a server to supply a `DPoP-Nonce` response header. Salesforce - * only ever issues nonces from the token endpoint — resource-server responses - * (identity, REST) never carry `DPoP-Nonce`, on success or on rejection. This cache - * is keyed by `(credentialsIdentifier, host)` so a per-host nonce, if a server ever - * does supply one, takes precedence. + * issues nonces from the token endpoint; resource-server responses (identity, REST) + * are not expected to carry one, though the SDK still harvests a `DPoP-Nonce` from + * any response that does (e.g. the userinfo nonce-challenge retry in + * `AuthenticationUtilities`). This cache is keyed by `(credentialsIdentifier, host)` + * so a nonce supplied by a specific host takes precedence for that host. * - * Since resource servers don't issue their own nonce, [get] falls back to the most + * Since resource servers aren't expected to issue their own nonce, [get] falls back to the most * recently stored nonce for [credentialsIdentifier] (any host) when there is no entry * for the exact `(credentialsIdentifier, host)` pair, so the client reuses the latest * token-endpoint nonce on every DPoP call for that credential. An exact host match diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt index aba717cd28..2a3c3e3ec9 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt @@ -91,8 +91,8 @@ class DPoPNonceCacheTest { } /* - * W-24342940: Salesforce only issues DPoP-Nonce from the token endpoint; resource - * servers (identity, REST) never issue their own, so the client must carry forward + * Salesforce issues DPoP-Nonce from the token endpoint; resource servers (identity, + * REST) are not expected to issue their own, so the client must carry forward * the nonce most recently issued for that credential, regardless of host. When no * nonce was ever stored for the exact (credentialsIdentifier, host) pair, get() must * fall back to the most recently stored nonce for that credential on any host. diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt index b23c324b6c..8935f84e58 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPRequestDecoratorTest.kt @@ -301,8 +301,8 @@ class DPoPRequestDecoratorTest { ) /* - * W-24342940: resource servers (identity, REST) never issue their own DPoP-Nonce - * challenge; they reject a proof carrying no nonce. The client must reuse the nonce + * Community logins: the token host differs from the resource hosts (identity, REST), + * which reject a proof carrying no nonce. The client must reuse the nonce * harvested from the /token host when attaching a proof for a different (resource) * host under the same credential. */ From 2dbdf9f19bf4e96a8ff86a1950d4cb97de093118 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Sat, 3 Oct 2026 20:30:30 -0700 Subject: [PATCH 5/5] @W-24342940: Address review - make exact-host precedence test distinguish exact from fallback nonce --- .../com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt index 2a3c3e3ec9..283ec19937 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/dpop/DPoPNonceCacheTest.kt @@ -107,8 +107,9 @@ class DPoPNonceCacheTest { @Test fun test_givenExactHostMatch_whenFallbackAlsoAvailable_thenExactMatchTakesPrecedence() { - DPoPNonceCache.store(id, loginHost, "fallback-nonce") + // The exact-host nonce is written first so the most recent (fallback) nonce differs from it. DPoPNonceCache.store(id, instanceHost, "exact-nonce") + DPoPNonceCache.store(id, loginHost, "fallback-nonce") assertEquals("exact-nonce", DPoPNonceCache.get(id, instanceHost)) }