Skip to content

Commit 208225b

Browse files
authored
fix: remember token when switching deployments (#120)
If we log in on deployment 1, then log out and login to deployment 2 and then in the same session we try to log in back to deployment 1, the token is no longer valid. The plugin will associate with deployment 1 the token from the second deployment. There is an overly complicated block of code inherited from Gateway plugin with multiple fallback sequences for both the deployment url and token from multiple sources (secrets store, data dir config, env, etc...). This fix simplifies the approach, we only store the url and the token in the secrets store, the token is always associated to a hostname. If there is no previous URL to remember (like the first time login) we default to https://dev.coder.com/ and empty token.
1 parent e4ce4c4 commit 208225b

File tree

14 files changed

+134
-231
lines changed

14 files changed

+134
-231
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
### Fixed
1111

1212
- url on the main page is now refreshed when switching between multiple deployments (via logout/login or URI handling)
13+
- tokens are now remembered after switching between multiple deployments
1314

1415
## 0.2.2 - 2025-05-21
1516

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class CoderRemoteProvider(
6464
// On the first load, automatically log in if we can.
6565
private var firstRun = true
6666
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
67-
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(context.deploymentUrl?.first ?: ""))
67+
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(context.deploymentUrl.toString()))
6868
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)
6969
override val environments: MutableStateFlow<LoadableState<List<RemoteProviderEnvironment>>> = MutableStateFlow(
7070
LoadableState.Loading
@@ -336,6 +336,7 @@ class CoderRemoteProvider(
336336
// Store the URL and token for use next time.
337337
context.secrets.lastDeploymentURL = client.url.toString()
338338
context.secrets.lastToken = client.token ?: ""
339+
context.secrets.storeTokenFor(client.url, context.secrets.lastToken)
339340
// Currently we always remember, but this could be made an option.
340341
context.secrets.rememberMe = true
341342
this.client = client
Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.coder.toolbox
22

3-
import com.coder.toolbox.settings.SettingSource
43
import com.coder.toolbox.store.CoderSecretsStore
54
import com.coder.toolbox.store.CoderSettingsStore
65
import com.coder.toolbox.util.toURL
@@ -13,6 +12,7 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1312
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1413
import com.jetbrains.toolbox.api.ui.ToolboxUi
1514
import kotlinx.coroutines.CoroutineScope
15+
import java.net.URL
1616

1717
data class CoderToolboxContext(
1818
val ui: ToolboxUi,
@@ -37,31 +37,11 @@ data class CoderToolboxContext(
3737
* 3. CODER_URL.
3838
* 4. URL in global cli config.
3939
*/
40-
val deploymentUrl: Pair<String, SettingSource>?
41-
get() = this.secrets.lastDeploymentURL.let {
42-
if (it.isNotBlank()) {
43-
it to SettingSource.LAST_USED
44-
} else {
45-
this.settingsStore.defaultURL()
40+
val deploymentUrl: URL
41+
get() {
42+
if (this.secrets.lastDeploymentURL.isNotBlank()) {
43+
return this.secrets.lastDeploymentURL.toURL()
4644
}
45+
return this.settingsStore.defaultURL.toURL()
4746
}
48-
49-
/**
50-
* Try to find a token.
51-
*
52-
* Order of preference:
53-
*
54-
* 1. Last used token, if it was for this deployment.
55-
* 2. Token on disk for this deployment.
56-
* 3. Global token for Coder, if it matches the deployment.
57-
*/
58-
fun getToken(deploymentURL: String?): Pair<String, SettingSource>? = this.secrets.lastToken.let {
59-
if (it.isNotBlank() && this.secrets.lastDeploymentURL == deploymentURL) {
60-
it to SettingSource.LAST_USED
61-
} else {
62-
if (deploymentURL != null) {
63-
this.settingsStore.token(deploymentURL.toURL())
64-
} else null
65-
}
66-
}
6747
}

src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.coder.toolbox.browser
22

3+
import com.coder.toolbox.util.toURL
34
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
4-
import java.net.URI
55

66

77
suspend fun LocalDesktopManager.browse(rawUrl: String, errorHandler: suspend (BrowserException) -> Unit) {
88
try {
9-
val url = URI.create(rawUrl).toURL()
9+
val url = rawUrl.toURL()
1010
this.openUrl(url)
1111
} catch (e: Exception) {
1212
errorHandler(

src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface ReadOnlyCoderSettings {
1010
/**
1111
* The default URL to show in the connection window.
1212
*/
13-
val defaultURL: String?
13+
val defaultURL: String
1414

1515
/**
1616
* Used to download the Coder CLI which is necessary to proxy SSH
@@ -116,16 +116,6 @@ interface ReadOnlyCoderSettings {
116116
*/
117117
val networkInfoDir: String
118118

119-
/**
120-
* The default URL to show in the connection window.
121-
*/
122-
fun defaultURL(): Pair<String, SettingSource>?
123-
124-
/**
125-
* Given a deployment URL, try to find a token for it if required.
126-
*/
127-
fun token(deploymentURL: URL): Pair<String, SettingSource>?
128-
129119
/**
130120
* Where the specified deployment should put its data.
131121
*/

src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.coder.toolbox.store
22

33
import com.jetbrains.toolbox.api.core.PluginSecretStore
4+
import java.net.URL
45

56

67
/**
@@ -26,4 +27,10 @@ class CoderSecretsStore(private val store: PluginSecretStore) {
2627
var rememberMe: Boolean
2728
get() = get("remember-me").toBoolean()
2829
set(value) = set("remember-me", value.toString())
30+
31+
fun tokenFor(url: URL): String? = store[url.host]
32+
33+
fun storeTokenFor(url: URL, token: String) {
34+
store[url.host] = token
35+
}
2936
}

src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.coder.toolbox.store
33
import com.coder.toolbox.settings.Environment
44
import com.coder.toolbox.settings.ReadOnlyCoderSettings
55
import com.coder.toolbox.settings.ReadOnlyTLSSettings
6-
import com.coder.toolbox.settings.SettingSource
76
import com.coder.toolbox.util.Arch
87
import com.coder.toolbox.util.OS
98
import com.coder.toolbox.util.expand
@@ -35,7 +34,7 @@ class CoderSettingsStore(
3534
) : ReadOnlyTLSSettings
3635

3736
// Properties implementation
38-
override val defaultURL: String? get() = store[DEFAULT_URL]
37+
override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com"
3938
override val binarySource: String? get() = store[BINARY_SOURCE]
4039
override val binaryDirectory: String? get() = store[BINARY_DIRECTORY]
4140
override val defaultCliBinaryNameByOsAndArch: String get() = getCoderCLIForOS(getOS(), getArch())
@@ -71,48 +70,6 @@ class CoderSettingsStore(
7170
.normalize()
7271
.toString()
7372

74-
/**
75-
* The default URL to show in the connection window.
76-
*/
77-
override fun defaultURL(): Pair<String, SettingSource>? {
78-
val envURL = env.get(CODER_URL)
79-
if (!defaultURL.isNullOrEmpty()) {
80-
return defaultURL!! to SettingSource.SETTINGS
81-
} else if (envURL.isNotBlank()) {
82-
return envURL to SettingSource.ENVIRONMENT
83-
} else {
84-
val (configUrl, _) = readConfig(Path.of(globalConfigDir))
85-
if (!configUrl.isNullOrBlank()) {
86-
return configUrl to SettingSource.CONFIG
87-
}
88-
}
89-
return null
90-
}
91-
92-
/**
93-
* Given a deployment URL, try to find a token for it if required.
94-
*/
95-
override fun token(deploymentURL: URL): Pair<String, SettingSource>? {
96-
// No need to bother if we do not need token auth anyway.
97-
if (!requireTokenAuth) {
98-
return null
99-
}
100-
// Try the deployment's config directory. This could exist if someone
101-
// has entered a URL that they are not currently connected to, but have
102-
// connected to in the past.
103-
val (_, deploymentToken) = readConfig(dataDir(deploymentURL).resolve("config"))
104-
if (!deploymentToken.isNullOrBlank()) {
105-
return deploymentToken to SettingSource.DEPLOYMENT_CONFIG
106-
}
107-
// Try the global config directory, in case they previously set up the
108-
// CLI with this URL.
109-
val (configUrl, configToken) = readConfig(Path.of(globalConfigDir))
110-
if (configUrl == deploymentURL.toString() && !configToken.isNullOrBlank()) {
111-
return configToken to SettingSource.CONFIG
112-
}
113-
return null
114-
}
115-
11673
/**
11774
* Where the specified deployment should put its data.
11875
*/

src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.coder.toolbox.views
33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.sdk.CoderRestClient
6+
import com.coder.toolbox.util.toURL
7+
import com.coder.toolbox.views.state.AuthContext
68
import com.coder.toolbox.views.state.AuthWizardState
79
import com.coder.toolbox.views.state.WizardStep
810
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
@@ -23,17 +25,30 @@ class AuthWizardPage(
2325
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
2426
context.ui.showUiPage(settingsPage)
2527
})
28+
2629
private val signInStep = SignInStep(context, this::notify)
2730
private val tokenStep = TokenStep(context)
28-
private val connectStep = ConnectStep(context, shouldAutoLogin, this::notify, this::displaySteps, onConnect)
29-
31+
private val connectStep = ConnectStep(
32+
context,
33+
shouldAutoLogin,
34+
this::notify,
35+
this::displaySteps,
36+
onConnect
37+
)
3038

3139
/**
3240
* Fields for this page, displayed in order.
3341
*/
3442
override val fields: MutableStateFlow<List<UiField>> = MutableStateFlow(emptyList())
3543
override val actionButtons: MutableStateFlow<List<RunnableActionDescription>> = MutableStateFlow(emptyList())
3644

45+
init {
46+
if (shouldAutoLogin.value) {
47+
AuthContext.url = context.secrets.lastDeploymentURL.toURL()
48+
AuthContext.token = context.secrets.lastToken
49+
}
50+
}
51+
3752
override fun beforeShow() {
3853
displaySteps()
3954
}

src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.cli.ensureCLI
66
import com.coder.toolbox.plugin.PluginManager
77
import com.coder.toolbox.sdk.CoderRestClient
8-
import com.coder.toolbox.util.toURL
8+
import com.coder.toolbox.views.state.AuthContext
99
import com.coder.toolbox.views.state.AuthWizardState
1010
import com.jetbrains.toolbox.api.localization.LocalizableString
1111
import com.jetbrains.toolbox.api.ui.components.LabelField
@@ -50,34 +50,38 @@ class ConnectStep(
5050
context.i18n.pnotr("")
5151
}
5252

53-
val url = context.deploymentUrl?.first?.toURL()
54-
statusField.textState.update { context.i18n.pnotr("Connecting to ${url?.host}...") }
53+
if (AuthContext.isNotReadyForAuth()) {
54+
errorField.textState.update {
55+
context.i18n.pnotr("URL and token were not properly configured. Please go back and provide a proper URL and token!")
56+
}
57+
return
58+
}
59+
60+
statusField.textState.update { context.i18n.pnotr("Connecting to ${AuthContext.url!!.host}...") }
5561
connect()
5662
}
5763

5864
/**
5965
* Try connecting to Coder with the provided URL and token.
6066
*/
6167
private fun connect() {
62-
val url = context.deploymentUrl?.first?.toURL()
63-
val token = context.getToken(context.deploymentUrl?.first)?.first
64-
if (url == null) {
68+
if (!AuthContext.hasUrl()) {
6569
errorField.textState.update { context.i18n.ptrl("URL is required") }
6670
return
6771
}
6872

69-
if (token.isNullOrBlank()) {
73+
if (!AuthContext.hasToken()) {
7074
errorField.textState.update { context.i18n.ptrl("Token is required") }
7175
return
7276
}
7377
signInJob?.cancel()
7478
signInJob = context.cs.launch {
7579
try {
76-
statusField.textState.update { (context.i18n.ptrl("Authenticating to ${url.host}...")) }
80+
statusField.textState.update { (context.i18n.ptrl("Authenticating to ${AuthContext.url!!.host}...")) }
7781
val client = CoderRestClient(
7882
context,
79-
url,
80-
token,
83+
AuthContext.url!!,
84+
AuthContext.token!!,
8185
PluginManager.pluginInfo.version,
8286
)
8387
// allows interleaving with the back/cancel action
@@ -92,19 +96,20 @@ class ConnectStep(
9296
yield()
9397
cli.login(client.token)
9498
}
95-
statusField.textState.update { (context.i18n.ptrl("Successfully configured ${url.host}...")) }
99+
statusField.textState.update { (context.i18n.ptrl("Successfully configured ${AuthContext.url!!.host}...")) }
96100
// allows interleaving with the back/cancel action
97101
yield()
98-
onConnect(client, cli)
102+
AuthContext.reset()
99103
AuthWizardState.resetSteps()
104+
onConnect(client, cli)
100105
} catch (ex: CancellationException) {
101106
if (ex.message != USER_HIT_THE_BACK_BUTTON) {
102-
notify("Connection to ${url.host} was configured", ex)
107+
notify("Connection to ${AuthContext.url!!.host} was configured", ex)
103108
onBack()
104109
refreshWizard()
105110
}
106111
} catch (ex: Exception) {
107-
notify("Failed to configure ${url.host}", ex)
112+
notify("Failed to configure ${AuthContext.url!!.host}", ex)
108113
onBack()
109114
refreshWizard()
110115
}
@@ -120,6 +125,7 @@ class ConnectStep(
120125
signInJob?.cancel(CancellationException(USER_HIT_THE_BACK_BUTTON))
121126
} finally {
122127
if (shouldAutoLogin.value) {
128+
AuthContext.reset()
123129
AuthWizardState.resetSteps()
124130
context.secrets.rememberMe = false
125131
} else {

0 commit comments

Comments
 (0)