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
12 changes: 11 additions & 1 deletion native/NativeSampleApps/AuthFlowTester/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions native/NativeSampleApps/AuthFlowTester/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +49 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please disable this test for the time being if we expect it to fail?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Done in: ed0a6f6


// 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)" }
}
}
Comment on lines +222 to +235

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For RTR maybe we should run the revoke twice to ensure the new RT is being used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double revoke in tests now - see ed0a6f6

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(" ")

Expand Down
12 changes: 12 additions & 0 deletions shared/test/ui_test_config.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Loading