fix(android): reset auth on new setup codes

This commit is contained in:
Ayaan Zaidi
2026-04-08 21:08:48 +05:30
parent 11bd40fe8a
commit 6090afa0e5
7 changed files with 106 additions and 6 deletions

View File

@@ -204,6 +204,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
prefs.setGatewayPassword(value)
}
fun resetGatewaySetupAuth() {
ensureRuntime().resetGatewaySetupAuth()
}
fun setOnboardingCompleted(value: Boolean) {
if (value) {
ensureRuntime()

View File

@@ -556,6 +556,12 @@ class NodeRuntime(
fun setGatewayToken(value: String) = prefs.setGatewayToken(value)
fun setGatewayBootstrapToken(value: String) = prefs.setGatewayBootstrapToken(value)
fun setGatewayPassword(value: String) = prefs.setGatewayPassword(value)
fun resetGatewaySetupAuth() {
prefs.clearGatewaySetupAuth()
val deviceId = identityStore.loadOrCreate().deviceId
deviceAuthStore.clearToken(deviceId, "node")
deviceAuthStore.clearToken(deviceId, "operator")
}
fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value)
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
@@ -1325,8 +1331,6 @@ internal fun resolveOperatorSessionConnectAuth(
val storedToken = storedOperatorToken?.trim()?.takeIf { it.isNotEmpty() }
if (storedToken != null) {
// Bootstrap can seed the operator token, but operator should reconnect
// through the stored device-token path rather than bootstrap auth itself.
return NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = null,
@@ -1334,6 +1338,15 @@ internal fun resolveOperatorSessionConnectAuth(
)
}
val explicitBootstrapToken = auth.bootstrapToken?.trim()?.takeIf { it.isNotEmpty() }
if (explicitBootstrapToken != null) {
return NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = explicitBootstrapToken,
password = null,
)
}
return null
}

View File

@@ -402,6 +402,18 @@ class SecurePrefs(
securePrefs.edit { putString(key, password.trim()) }
}
fun clearGatewaySetupAuth() {
val instanceId = _instanceId.value
securePrefs.edit {
remove("gateway.manual.token")
remove("gateway.token.$instanceId")
remove("gateway.bootstrapToken.$instanceId")
remove("gateway.password.$instanceId")
}
_gatewayToken.value = ""
_gatewayBootstrapToken.value = ""
}
fun loadGatewayTlsFingerprint(stableId: String): String? {
val key = "gateway.tls.$stableId"
return plainPrefs.getString(key, null)?.trim()?.takeIf { it.isNotEmpty() }

View File

@@ -293,6 +293,9 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
}
validationText = null
if (inputMode == ConnectInputMode.SetupCode) {
viewModel.resetGatewaySetupAuth()
}
viewModel.setManualEnabled(true)
viewModel.setManualHost(config.host)
viewModel.setManualPort(config.port)

View File

@@ -580,6 +580,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
return@addOnSuccessListener
}
setupCode = scannedSetupCode.setupCode
viewModel.resetGatewaySetupAuth()
gatewayInputMode = GatewayInputMode.SetupCode
gatewayError = null
attemptedConnect = false
@@ -817,6 +818,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
)
return@Button
}
viewModel.resetGatewaySetupAuth()
gatewayUrl = parsedSetup.url
viewModel.setGatewayBootstrapToken(parsedSetup.bootstrapToken.orEmpty())
val sharedToken = parsedSetup.token.orEmpty().trim()
@@ -899,6 +901,8 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
viewModel.setManualTls(parsed.config.tls)
if (gatewayInputMode == GatewayInputMode.Manual) {
viewModel.setGatewayBootstrapToken("")
} else {
viewModel.resetGatewaySetupAuth()
}
if (token.isNotEmpty()) {
viewModel.setGatewayToken(token)

View File

@@ -1,6 +1,8 @@
package ai.openclaw.app
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.DeviceAuthStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
import ai.openclaw.app.gateway.GatewayTlsProbeResult
@@ -21,14 +23,14 @@ import java.util.UUID
@Config(sdk = [34])
class GatewayBootstrapAuthTest {
@Test
fun skipsOperatorSessionWhenOnlyBootstrapAuthExists() {
assertFalse(
fun connectsOperatorSessionWhenOnlyBootstrapAuthExists() {
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = "", bootstrapToken = "bootstrap-1", password = ""),
storedOperatorToken = "",
),
)
assertFalse(
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = null,
@@ -75,6 +77,20 @@ class GatewayBootstrapAuthTest {
assertEquals(NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = null, password = null), resolved)
}
@Test
fun resolveOperatorSessionConnectAuthUsesBootstrapWhenNoStoredOperatorTokenExists() {
val resolved =
resolveOperatorSessionConnectAuth(
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = null,
)
assertEquals(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
resolved,
)
}
@Test
fun resolveOperatorSessionConnectAuthPrefersExplicitSharedAuth() {
val resolved =
@@ -152,7 +168,7 @@ class GatewayBootstrapAuthTest {
assertEquals("fp-1", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "nodeSession"))
assertNull(desiredBootstrapToken(runtime, "operatorSession"))
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "operatorSession"))
}
@Test
@@ -178,6 +194,33 @@ class GatewayBootstrapAuthTest {
assertNull(runtime.pendingGatewayTrust.value)
}
@Test
fun resetGatewaySetupAuth_clearsStoredGatewayAndDeviceTokens() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
android.content.Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
val runtime = NodeRuntime(app, prefs)
val deviceId = DeviceIdentityStore(app).loadOrCreate().deviceId
val authStore = DeviceAuthStore(prefs)
prefs.setGatewayToken("stale-shared-token")
prefs.setGatewayBootstrapToken("stale-bootstrap-token")
prefs.setGatewayPassword("stale-password")
authStore.saveToken(deviceId, "node", "stale-node-token")
authStore.saveToken(deviceId, "operator", "stale-operator-token")
runtime.resetGatewaySetupAuth()
assertNull(prefs.loadGatewayToken())
assertNull(prefs.loadGatewayBootstrapToken())
assertNull(prefs.loadGatewayPassword())
assertNull(authStore.loadToken(deviceId, "node"))
assertNull(authStore.loadToken(deviceId, "operator"))
}
private fun waitForGatewayTrustPrompt(runtime: NodeRuntime): NodeRuntime.GatewayTrustPrompt {
repeat(50) {
runtime.pendingGatewayTrust.value?.let { return it }

View File

@@ -2,6 +2,7 @@ package ai.openclaw.app
import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -35,4 +36,24 @@ class SecurePrefsTest {
assertEquals("bootstrap-token", prefs.loadGatewayBootstrapToken())
assertEquals("bootstrap-token", prefs.gatewayBootstrapToken.value)
}
@Test
fun clearGatewaySetupAuth_removesStoredGatewayAuth() {
val context = RuntimeEnvironment.getApplication()
val securePrefs = context.getSharedPreferences("openclaw.node.secure.test.clear", Context.MODE_PRIVATE)
securePrefs.edit().clear().commit()
val prefs = SecurePrefs(context, securePrefsOverride = securePrefs)
prefs.setGatewayToken("shared-token")
prefs.setGatewayBootstrapToken("bootstrap-token")
prefs.setGatewayPassword("password-token")
prefs.clearGatewaySetupAuth()
assertEquals("", prefs.gatewayToken.value)
assertEquals("", prefs.gatewayBootstrapToken.value)
assertNull(prefs.loadGatewayToken())
assertNull(prefs.loadGatewayBootstrapToken())
assertNull(prefs.loadGatewayPassword())
}
}