diff --git a/native/NativeSampleApps/AuthFlowTester/README.md b/native/NativeSampleApps/AuthFlowTester/README.md index 184b0c3deb..b5057f755b 100644 --- a/native/NativeSampleApps/AuthFlowTester/README.md +++ b/native/NativeSampleApps/AuthFlowTester/README.md @@ -47,6 +47,16 @@ External Client App (ECA) login tests for both opaque and JWT token formats with | `testECAJwt_SubsetScopes_NotHybrid` | ECA JWT | Subset | | `testECAJwt_AllScopes` | ECA JWT | All | +#### RTRLoginTests +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. + +| Test | App Config | Hybrid | +|------|-----------|--------| +| `testECAJwtRtr_Hybrid` | ECA JWT RTR | Yes | +| `testECAJwtRtr_NoHybrid` | ECA JWT RTR | No | +| `testECAOpaqueRtr_Hybrid` | ECA Opaque RTR | Yes | +| `testECAOpaqueRtr_NoHybrid` | ECA Opaque RTR | No | + #### BeaconLoginTests Beacon app login tests for lightweight authentication use cases, covering both opaque and JWT token formats. @@ -148,7 +158,7 @@ Multi-user tests additionally verify: ### Configuration -- **App configs** (`KnownAppConfig`): `ECA_OPAQUE`, `ECA_JWT`, `BEACON_OPAQUE`, `BEACON_JWT`, `CA_OPAQUE`, `CA_JWT` +- **App configs** (`KnownAppConfig`): `ECA_OPAQUE`, `ECA_JWT`, `ECA_OPAQUE_RTR`, `ECA_JWT_RTR`, `BEACON_OPAQUE`, `BEACON_JWT`, `CA_OPAQUE`, `CA_JWT` - **Login hosts** (`KnownLoginHostConfig`): `REGULAR_AUTH` (in-app WebView), `ADVANCED_AUTH` (Chrome Custom Tab) - **Scope options** (`ScopeSelection`): `EMPTY` (default/boot config scopes), `SUBSET` (all minus `sfap_api`), `ALL` - **Users** (`KnownUserConfig`): `FIRST` through `FIFTH`, assigned per API level diff --git a/native/NativeSampleApps/AuthFlowTester/build.gradle.kts b/native/NativeSampleApps/AuthFlowTester/build.gradle.kts index bb03964875..adc063727e 100644 --- a/native/NativeSampleApps/AuthFlowTester/build.gradle.kts +++ b/native/NativeSampleApps/AuthFlowTester/build.gradle.kts @@ -96,10 +96,10 @@ android { // TODO: This cannot be resolved until newDSL=true configurations.all { resolutionStrategy { - force("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3") - force("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3") - force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") - force("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3") + force("org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0") + force("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0") + force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") + force("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0") force("androidx.test:runner:1.7.0") force("androidx.test:rules:1.6.1") force("androidx.test.espresso:espresso-core:3.7.0") diff --git a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/RTRLoginTests.kt b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/RTRLoginTests.kt new file mode 100644 index 0000000000..eddaf07d3a --- /dev/null +++ b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/RTRLoginTests.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.samples.authflowtester + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.salesforce.samples.authflowtester.testUtility.AuthFlowTest +import com.salesforce.samples.authflowtester.testUtility.KnownAppConfig.ECA_JWT_RTR +import com.salesforce.samples.authflowtester.testUtility.KnownAppConfig.ECA_OPAQUE_RTR +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for login flows using External Client App (ECA) configurations with Refresh Token Rotation (RTR). + * + * NB: Tests use the first user from ui_test_config.json + */ +@RunWith(AndroidJUnit4::class) +@LargeTest +class RTRLoginTests : AuthFlowTest() { + + // region ECA JWT RTR Tests + + // Login with ECA JWT RTR using hybrid auth token flow. + // Expected to fail until W-22512846 (Enable Named JWTs for Hybrid Flows) is resolved. + // The server currently returns invalid_grant when RTR is used with JWT tokens in hybrid flow. + @Ignore("Won't pass until server completes W-22512846") + @Test + fun testECAJwtRtr_Hybrid() { + loginAndValidate(knownAppConfig = ECA_JWT_RTR) + assertRevokeAndRefreshWorks(isRtr = true) + assertRevokeAndRefreshWorks(isRtr = true) + } + + // Login with ECA JWT RTR without hybrid auth token. + @Test + fun testECAJwtRtr_NoHybrid() { + loginAndValidate(knownAppConfig = ECA_JWT_RTR, useHybridAuthToken = false) + assertRevokeAndRefreshWorks(isRtr = true) + assertRevokeAndRefreshWorks(isRtr = true) + } + + // endregion + + // region ECA Opaque RTR Tests + + // Login with ECA Opaque RTR using hybrid auth token flow. + @Test + fun testECAOpaqueRtr_Hybrid() { + loginAndValidate(knownAppConfig = ECA_OPAQUE_RTR) + assertRevokeAndRefreshWorks(isRtr = true) + assertRevokeAndRefreshWorks(isRtr = true) + } + + // Login with ECA Opaque RTR without hybrid auth token. + @Test + fun testECAOpaqueRtr_NoHybrid() { + loginAndValidate(knownAppConfig = ECA_OPAQUE_RTR, useHybridAuthToken = false) + assertRevokeAndRefreshWorks(isRtr = true) + assertRevokeAndRefreshWorks(isRtr = true) + } + + // endregion +} diff --git a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/AuthFlowTest.kt b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/AuthFlowTest.kt index 26f076deb4..303e7328df 100644 --- a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/AuthFlowTest.kt +++ b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/AuthFlowTest.kt @@ -218,4 +218,19 @@ abstract class AuthFlowTest { app.revokeAccessToken() app.validateApiRequest() } + + fun assertRevokeAndRefreshWorks(isRtr: Boolean) { + val (preAccessToken, preRefreshToken) = app.getTokens() + app.revokeAccessToken() + app.validateApiRequest() + val (postAccessToken, postRefreshToken) = app.getTokens() + + assert(preAccessToken != postAccessToken) { "Access token should have been refreshed" } + + if (isRtr) { + assert(preRefreshToken != postRefreshToken) { "Refresh token should have rotated (RTR app)" } + } else { + assert(preRefreshToken == postRefreshToken) { "Refresh token should not have changed (non-RTR app)" } + } + } } \ No newline at end of file diff --git a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/UITestConfig.kt b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/UITestConfig.kt index a16108a4b0..40c0da773b 100644 --- a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/UITestConfig.kt +++ b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/testUtility/UITestConfig.kt @@ -55,6 +55,8 @@ enum class KnownLoginHostConfig { enum class KnownAppConfig { ECA_OPAQUE, ECA_JWT, + ECA_OPAQUE_RTR, + ECA_JWT_RTR, BEACON_OPAQUE, BEACON_JWT, CA_OPAQUE, @@ -109,6 +111,7 @@ data class AppConfig( val scopes: String, ) { val issuesJwt = name.contains("_jwt") + val isRtr = name.contains("_rtr") val expectedTokenFormat = if (issuesJwt) "jwt" else "Opaque" val scopeList = scopes.split(" ") diff --git a/shared/test/ui_test_config.json.sample b/shared/test/ui_test_config.json.sample index 9b3cb991ac..dcc1d58aff 100644 --- a/shared/test/ui_test_config.json.sample +++ b/shared/test/ui_test_config.json.sample @@ -89,6 +89,18 @@ "consumerKey": "your_consumer_key_here", "redirectUri": "beaconadvancedjwt://success/done", "scopes": "api content id lightning refresh_token sfap_api web" + }, + { + "name": "eca_jwt_rtr", + "consumerKey": "your_consumer_key_here", + "redirectUri": "ecajwtrtr://success/done", + "scopes": "api content id lightning refresh_token sfap_api web" + }, + { + "name": "eca_opaque_rtr", + "consumerKey": "your_consumer_key_here", + "redirectUri": "ecaopaquertr://success/done", + "scopes": "api content id lightning refresh_token sfap_api web" } ] } \ No newline at end of file