Skip to content

Commit 46bee79

Browse files
authored
Merge pull request #2888 from wmathurin/ui_tests_rtr
Add UI tests for refresh token rotation
2 parents ca12605 + 8d3f50e commit 46bee79

6 files changed

Lines changed: 134 additions & 5 deletions

File tree

native/NativeSampleApps/AuthFlowTester/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ External Client App (ECA) login tests for both opaque and JWT token formats with
4747
| `testECAJwt_SubsetScopes_NotHybrid` | ECA JWT | Subset |
4848
| `testECAJwt_AllScopes` | ECA JWT | All |
4949

50+
#### RTRLoginTests
51+
Tests for ECA configurations with Refresh Token Rotation (RTR) enabled. Verifies that the refresh token rotates on each token refresh cycle. The `assertRevokeAndRefreshWorks` check asserts the refresh token **changes** after a revoke/refresh cycle for RTR apps.
52+
53+
| Test | App Config | Hybrid |
54+
|------|-----------|--------|
55+
| `testECAJwtRtr_Hybrid` | ECA JWT RTR | Yes |
56+
| `testECAJwtRtr_NoHybrid` | ECA JWT RTR | No |
57+
| `testECAOpaqueRtr_Hybrid` | ECA Opaque RTR | Yes |
58+
| `testECAOpaqueRtr_NoHybrid` | ECA Opaque RTR | No |
59+
5060
#### BeaconLoginTests
5161
Beacon app login tests for lightweight authentication use cases, covering both opaque and JWT token formats.
5262

@@ -148,7 +158,7 @@ Multi-user tests additionally verify:
148158

149159
### Configuration
150160

151-
- **App configs** (`KnownAppConfig`): `ECA_OPAQUE`, `ECA_JWT`, `BEACON_OPAQUE`, `BEACON_JWT`, `CA_OPAQUE`, `CA_JWT`
161+
- **App configs** (`KnownAppConfig`): `ECA_OPAQUE`, `ECA_JWT`, `ECA_OPAQUE_RTR`, `ECA_JWT_RTR`, `BEACON_OPAQUE`, `BEACON_JWT`, `CA_OPAQUE`, `CA_JWT`
152162
- **Login hosts** (`KnownLoginHostConfig`): `REGULAR_AUTH` (in-app WebView), `ADVANCED_AUTH` (Chrome Custom Tab)
153163
- **Scope options** (`ScopeSelection`): `EMPTY` (default/boot config scopes), `SUBSET` (all minus `sfap_api`), `ALL`
154164
- **Users** (`KnownUserConfig`): `FIRST` through `FIFTH`, assigned per API level

native/NativeSampleApps/AuthFlowTester/build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ android { // TODO: This cannot be resolved until newDSL=true
9696

9797
configurations.all {
9898
resolutionStrategy {
99-
force("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3")
100-
force("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3")
101-
force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
102-
force("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3")
99+
force("org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0")
100+
force("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0")
101+
force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
102+
force("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0")
103103
force("androidx.test:runner:1.7.0")
104104
force("androidx.test:rules:1.6.1")
105105
force("androidx.test.espresso:espresso-core:3.7.0")
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2026-present, salesforce.com, inc.
3+
* All rights reserved.
4+
* Redistribution and use of this software in source and binary forms, with or
5+
* without modification, are permitted provided that the following conditions
6+
* are met:
7+
* - Redistributions of source code must retain the above copyright notice, this
8+
* list of conditions and the following disclaimer.
9+
* - Redistributions in binary form must reproduce the above copyright notice,
10+
* this list of conditions and the following disclaimer in the documentation
11+
* and/or other materials provided with the distribution.
12+
* - Neither the name of salesforce.com, inc. nor the names of its contributors
13+
* may be used to endorse or promote products derived from this software without
14+
* specific prior written permission of salesforce.com, inc.
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package com.salesforce.samples.authflowtester
28+
29+
import androidx.test.ext.junit.runners.AndroidJUnit4
30+
import androidx.test.filters.LargeTest
31+
import com.salesforce.samples.authflowtester.testUtility.AuthFlowTest
32+
import com.salesforce.samples.authflowtester.testUtility.KnownAppConfig.ECA_JWT_RTR
33+
import com.salesforce.samples.authflowtester.testUtility.KnownAppConfig.ECA_OPAQUE_RTR
34+
import org.junit.Ignore
35+
import org.junit.Test
36+
import org.junit.runner.RunWith
37+
38+
/**
39+
* Tests for login flows using External Client App (ECA) configurations with Refresh Token Rotation (RTR).
40+
*
41+
* NB: Tests use the first user from ui_test_config.json
42+
*/
43+
@RunWith(AndroidJUnit4::class)
44+
@LargeTest
45+
class RTRLoginTests : AuthFlowTest() {
46+
47+
// region ECA JWT RTR Tests
48+
49+
// Login with ECA JWT RTR using hybrid auth token flow.
50+
// Expected to fail until W-22512846 (Enable Named JWTs for Hybrid Flows) is resolved.
51+
// The server currently returns invalid_grant when RTR is used with JWT tokens in hybrid flow.
52+
@Ignore("Won't pass until server completes W-22512846")
53+
@Test
54+
fun testECAJwtRtr_Hybrid() {
55+
loginAndValidate(knownAppConfig = ECA_JWT_RTR)
56+
assertRevokeAndRefreshWorks(isRtr = true)
57+
assertRevokeAndRefreshWorks(isRtr = true)
58+
}
59+
60+
// Login with ECA JWT RTR without hybrid auth token.
61+
@Test
62+
fun testECAJwtRtr_NoHybrid() {
63+
loginAndValidate(knownAppConfig = ECA_JWT_RTR, useHybridAuthToken = false)
64+
assertRevokeAndRefreshWorks(isRtr = true)
65+
assertRevokeAndRefreshWorks(isRtr = true)
66+
}
67+
68+
// endregion
69+
70+
// region ECA Opaque RTR Tests
71+
72+
// Login with ECA Opaque RTR using hybrid auth token flow.
73+
@Test
74+
fun testECAOpaqueRtr_Hybrid() {
75+
loginAndValidate(knownAppConfig = ECA_OPAQUE_RTR)
76+
assertRevokeAndRefreshWorks(isRtr = true)
77+
assertRevokeAndRefreshWorks(isRtr = true)
78+
}
79+
80+
// Login with ECA Opaque RTR without hybrid auth token.
81+
@Test
82+
fun testECAOpaqueRtr_NoHybrid() {
83+
loginAndValidate(knownAppConfig = ECA_OPAQUE_RTR, useHybridAuthToken = false)
84+
assertRevokeAndRefreshWorks(isRtr = true)
85+
assertRevokeAndRefreshWorks(isRtr = true)
86+
}
87+
88+
// endregion
89+
}

native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/AuthFlowTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,19 @@ abstract class AuthFlowTest {
218218
app.revokeAccessToken()
219219
app.validateApiRequest()
220220
}
221+
222+
fun assertRevokeAndRefreshWorks(isRtr: Boolean) {
223+
val (preAccessToken, preRefreshToken) = app.getTokens()
224+
app.revokeAccessToken()
225+
app.validateApiRequest()
226+
val (postAccessToken, postRefreshToken) = app.getTokens()
227+
228+
assert(preAccessToken != postAccessToken) { "Access token should have been refreshed" }
229+
230+
if (isRtr) {
231+
assert(preRefreshToken != postRefreshToken) { "Refresh token should have rotated (RTR app)" }
232+
} else {
233+
assert(preRefreshToken == postRefreshToken) { "Refresh token should not have changed (non-RTR app)" }
234+
}
235+
}
221236
}

native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/UITestConfig.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ enum class KnownLoginHostConfig {
5555
enum class KnownAppConfig {
5656
ECA_OPAQUE,
5757
ECA_JWT,
58+
ECA_OPAQUE_RTR,
59+
ECA_JWT_RTR,
5860
BEACON_OPAQUE,
5961
BEACON_JWT,
6062
CA_OPAQUE,
@@ -109,6 +111,7 @@ data class AppConfig(
109111
val scopes: String,
110112
) {
111113
val issuesJwt = name.contains("_jwt")
114+
val isRtr = name.contains("_rtr")
112115
val expectedTokenFormat = if (issuesJwt) "jwt" else "Opaque"
113116
val scopeList = scopes.split(" ")
114117

shared/test/ui_test_config.json.sample

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@
8989
"consumerKey": "your_consumer_key_here",
9090
"redirectUri": "beaconadvancedjwt://success/done",
9191
"scopes": "api content id lightning refresh_token sfap_api web"
92+
},
93+
{
94+
"name": "eca_jwt_rtr",
95+
"consumerKey": "your_consumer_key_here",
96+
"redirectUri": "ecajwtrtr://success/done",
97+
"scopes": "api content id lightning refresh_token sfap_api web"
98+
},
99+
{
100+
"name": "eca_opaque_rtr",
101+
"consumerKey": "your_consumer_key_here",
102+
"redirectUri": "ecaopaquertr://success/done",
103+
"scopes": "api content id lightning refresh_token sfap_api web"
92104
}
93105
]
94106
}

0 commit comments

Comments
 (0)