Skip to content

Commit 54b3e0a

Browse files
committed
fix: remember token when switching deployments
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 fcb9dc7 commit 54b3e0a

File tree

8 files changed

+69
-30
lines changed

8 files changed

+69
-30
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
- improved workspace status reporting (icon and colors) when it is failed, stopping, deleting, stopped or when we are
88
establishing the SSH connection.
99

10+
### Fixed
11+
12+
- tokens are now remembered after switching between multiple deployments
13+
1014
## 0.2.2 - 2025-05-21
1115

1216
### Added

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ class CoderRemoteProvider(
335335
// Store the URL and token for use next time.
336336
context.secrets.lastDeploymentURL = client.url.toString()
337337
context.secrets.lastToken = client.token ?: ""
338+
context.secrets.storeTokenFor(client.url, context.secrets.lastToken)
338339
// Currently we always remember, but this could be made an option.
339340
context.secrets.rememberMe = true
340341
this.client = client

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/views/AuthWizardPage.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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.views.state.AuthContext
67
import com.coder.toolbox.views.state.AuthWizardState
78
import com.coder.toolbox.views.state.WizardStep
89
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
@@ -23,9 +24,17 @@ class AuthWizardPage(
2324
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
2425
context.ui.showUiPage(settingsPage)
2526
})
26-
private val signInStep = SignInStep(context, this::notify)
27-
private val tokenStep = TokenStep(context)
28-
private val connectStep = ConnectStep(context, shouldAutoLogin, this::notify, this::displaySteps, onConnect)
27+
28+
private val authContext: AuthContext = AuthContext()
29+
private val signInStep = SignInStep(context, authContext, this::notify)
30+
private val tokenStep = TokenStep(context, authContext)
31+
private val connectStep = ConnectStep(
32+
context,
33+
authContext,
34+
shouldAutoLogin,
35+
this::notify,
36+
this::displaySteps, onConnect
37+
)
2938

3039

3140
/**

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

Lines changed: 8 additions & 5 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
@@ -25,6 +25,7 @@ private const val USER_HIT_THE_BACK_BUTTON = "User hit the back button"
2525
*/
2626
class ConnectStep(
2727
private val context: CoderToolboxContext,
28+
private val authContext: AuthContext,
2829
private val shouldAutoLogin: StateFlow<Boolean>,
2930
private val notify: (String, Throwable) -> Unit,
3031
private val refreshWizard: () -> Unit,
@@ -49,18 +50,18 @@ class ConnectStep(
4950
errorField.textState.update {
5051
context.i18n.pnotr("")
5152
}
53+
if (authContext.isNotReadyForAuth()) return
5254

53-
val url = context.deploymentUrl?.first?.toURL()
54-
statusField.textState.update { context.i18n.pnotr("Connecting to ${url?.host}...") }
55+
statusField.textState.update { context.i18n.pnotr("Connecting to ${authContext.url!!.host}...") }
5556
connect()
5657
}
5758

5859
/**
5960
* Try connecting to Coder with the provided URL and token.
6061
*/
6162
private fun connect() {
62-
val url = context.deploymentUrl?.first?.toURL()
63-
val token = context.getToken(context.deploymentUrl?.first)?.first
63+
val url = authContext.url
64+
val token = authContext.token
6465
if (url == null) {
6566
errorField.textState.update { context.i18n.ptrl("URL is required") }
6667
return
@@ -96,6 +97,8 @@ class ConnectStep(
9697
// allows interleaving with the back/cancel action
9798
yield()
9899
onConnect(client, cli)
100+
101+
authContext.reset()
99102
AuthWizardState.resetSteps()
100103
} catch (ex: CancellationException) {
101104
if (ex.message != USER_HIT_THE_BACK_BUTTON) {

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,34 @@ package com.coder.toolbox.views
22

33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.util.toURL
5+
import com.coder.toolbox.views.state.AuthContext
56
import com.coder.toolbox.views.state.AuthWizardState
67
import com.jetbrains.toolbox.api.localization.LocalizableString
7-
import com.jetbrains.toolbox.api.ui.components.LabelField
88
import com.jetbrains.toolbox.api.ui.components.RowGroup
99
import com.jetbrains.toolbox.api.ui.components.TextField
1010
import com.jetbrains.toolbox.api.ui.components.TextType
1111
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
1212
import kotlinx.coroutines.flow.update
1313
import java.net.MalformedURLException
14+
import java.net.URI
1415

1516
/**
1617
* A page with a field for providing the Coder deployment URL.
1718
*
1819
* Populates with the provided URL, at which point the user can accept or
1920
* enter their own.
2021
*/
21-
class SignInStep(private val context: CoderToolboxContext, private val notify: (String, Throwable) -> Unit) :
22+
class SignInStep(
23+
private val context: CoderToolboxContext,
24+
private val authContext: AuthContext,
25+
private val notify: (String, Throwable) -> Unit
26+
) :
2227
WizardStep {
2328
private val urlField = TextField(context.i18n.ptrl("Deployment URL"), "", TextType.General)
24-
private val descriptionField = LabelField(context.i18n.pnotr(""))
2529
private val errorField = ValidationErrorField(context.i18n.pnotr(""))
2630

2731
override val panel: RowGroup = RowGroup(
2832
RowGroup.RowField(urlField),
29-
RowGroup.RowField(descriptionField),
3033
RowGroup.RowField(errorField)
3134
)
3235

@@ -37,11 +40,7 @@ class SignInStep(private val context: CoderToolboxContext, private val notify: (
3740
context.i18n.pnotr("")
3841
}
3942
urlField.textState.update {
40-
context.deploymentUrl?.first ?: ""
41-
}
42-
43-
descriptionField.textState.update {
44-
context.i18n.pnotr(context.deploymentUrl?.second?.description("URL") ?: "")
43+
context.secrets.lastDeploymentURL
4544
}
4645
}
4746

@@ -62,7 +61,7 @@ class SignInStep(private val context: CoderToolboxContext, private val notify: (
6261
notify("URL is invalid", e)
6362
return false
6463
}
65-
context.secrets.lastDeploymentURL = url
64+
authContext.url = URI.create(url).toURL()
6665
AuthWizardState.goToNextStep()
6766
return true
6867
}

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package com.coder.toolbox.views
33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.util.toURL
55
import com.coder.toolbox.util.withPath
6+
import com.coder.toolbox.views.state.AuthContext
67
import com.coder.toolbox.views.state.AuthWizardState
78
import com.jetbrains.toolbox.api.localization.LocalizableString
8-
import com.jetbrains.toolbox.api.ui.components.LabelField
99
import com.jetbrains.toolbox.api.ui.components.LinkField
1010
import com.jetbrains.toolbox.api.ui.components.RowGroup
1111
import com.jetbrains.toolbox.api.ui.components.TextField
@@ -20,15 +20,16 @@ import kotlinx.coroutines.flow.update
2020
* Populate with the provided token, at which point the user can accept or
2121
* enter their own.
2222
*/
23-
class TokenStep(private val context: CoderToolboxContext) : WizardStep {
23+
class TokenStep(
24+
private val context: CoderToolboxContext,
25+
private val authContext: AuthContext
26+
) : WizardStep {
2427
private val tokenField = TextField(context.i18n.ptrl("Token"), "", TextType.Password)
25-
private val descriptionField = LabelField(context.i18n.pnotr(""))
2628
private val linkField = LinkField(context.i18n.ptrl("Get a token"), "")
2729
private val errorField = ValidationErrorField(context.i18n.pnotr(""))
2830

2931
override val panel: RowGroup = RowGroup(
3032
RowGroup.RowField(tokenField),
31-
RowGroup.RowField(descriptionField),
3233
RowGroup.RowField(linkField),
3334
RowGroup.RowField(errorField)
3435
)
@@ -39,13 +40,11 @@ class TokenStep(private val context: CoderToolboxContext) : WizardStep {
3940
context.i18n.pnotr("")
4041
}
4142
tokenField.textState.update {
42-
context.getToken(context.deploymentUrl?.first)?.first ?: ""
43-
}
44-
descriptionField.textState.update {
45-
context.i18n.pnotr(
46-
context.getToken(context.deploymentUrl?.first)?.second?.description("token")
47-
?: "No existing token for ${context.deploymentUrl} found."
48-
)
43+
if (authContext.hasUrl()) {
44+
context.secrets.tokenFor(authContext.url!!) ?: ""
45+
} else {
46+
""
47+
}
4948
}
5049
(linkField.urlState as MutableStateFlow).update {
5150
context.deploymentUrl?.first?.toURL()?.withPath("/login?redirect=%2Fcli-auth")?.toString() ?: ""
@@ -59,7 +58,7 @@ class TokenStep(private val context: CoderToolboxContext) : WizardStep {
5958
return false
6059
}
6160

62-
context.secrets.lastToken = token
61+
authContext.token = token
6362
AuthWizardState.goToNextStep()
6463
return true
6564
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.coder.toolbox.views.state
2+
3+
import java.net.URL
4+
5+
data class AuthContext(
6+
var url: URL? = null,
7+
var token: String? = null
8+
) {
9+
fun hasUrl(): Boolean = url != null
10+
11+
fun isNotReadyForAuth(): Boolean = !(hasUrl() && token != null)
12+
13+
fun reset() {
14+
url = null
15+
token = null
16+
}
17+
}

0 commit comments

Comments
 (0)