From 0160e6f89838a049a61574d8434414eff9dce5d0 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 18 Jul 2025 01:02:50 +0300 Subject: [PATCH 1/5] impl: support for certificate based authentication We now skip token input screen if the user provided a public and a private key for mTLS authentication. --- CHANGELOG.md | 1 + .../com/coder/toolbox/CoderRemoteProvider.kt | 2 +- .../com/coder/toolbox/sdk/CoderRestClient.kt | 5 ++++- .../com/coder/toolbox/views/ConnectStep.kt | 18 +++++++++++------- .../coder/toolbox/views/DeploymentUrlStep.kt | 6 +++++- .../views/state/CoderCliSetupWizardState.kt | 6 +++++- 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a1b072..599ad48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - support for matching workspace agent in the URI via the agent name +- support for certificate based authentication ### Removed diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index be4c40a..0e0bf57 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -244,7 +244,7 @@ class CoderRemoteProvider( environments.value = LoadableState.Value(emptyList()) isInitialized.update { false } client = null - CoderCliSetupWizardState.resetSteps() + CoderCliSetupWizardState.goToFirstStep() } override val svgIcon: SvgIcon = diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 9aa3dfb..187e5db 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -94,7 +94,10 @@ open class CoderRestClient( .build() } - if (token != null) { + if (context.settingsStore.requireTokenAuth) { + if (token.isNullOrBlank()) { + throw IllegalStateException("Token is required for $url deployment") + } builder = builder.addInterceptor { it.proceed( it.request().newBuilder().addHeader("Coder-Session-Token", token).build() diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 9964d0c..8d0e2d3 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -67,7 +67,7 @@ class ConnectStep( return } - if (!CoderCliSetupContext.hasToken()) { + if (context.settingsStore.requireTokenAuth && !CoderCliSetupContext.hasToken()) { errorField.textState.update { context.i18n.ptrl("Token is required") } return } @@ -77,7 +77,7 @@ class ConnectStep( val client = CoderRestClient( context, CoderCliSetupContext.url!!, - CoderCliSetupContext.token!!, + if (context.settingsStore.requireTokenAuth) CoderCliSetupContext.token else null, PluginManager.pluginInfo.version, ) // allows interleaving with the back/cancel action @@ -91,17 +91,17 @@ class ConnectStep( statusField.textState.update { (context.i18n.pnotr(progress)) } } // We only need to log in if we are using token-based auth. - if (client.token != null) { + if (context.settingsStore.requireTokenAuth) { statusField.textState.update { (context.i18n.ptrl("Configuring Coder CLI...")) } // allows interleaving with the back/cancel action yield() - cli.login(client.token) + cli.login(client.token!!) } statusField.textState.update { (context.i18n.ptrl("Successfully configured ${CoderCliSetupContext.url!!.host}...")) } // allows interleaving with the back/cancel action yield() CoderCliSetupContext.reset() - CoderCliSetupWizardState.resetSteps() + CoderCliSetupWizardState.goToFirstStep() onConnect(client, cli) } catch (ex: CancellationException) { if (ex.message != USER_HIT_THE_BACK_BUTTON) { @@ -127,10 +127,14 @@ class ConnectStep( } finally { if (shouldAutoLogin.value) { CoderCliSetupContext.reset() - CoderCliSetupWizardState.resetSteps() + CoderCliSetupWizardState.goToFirstStep() context.secrets.rememberMe = false } else { - CoderCliSetupWizardState.goToPreviousStep() + if (context.settingsStore.requireTokenAuth) { + CoderCliSetupWizardState.goToPreviousStep() + } else { + CoderCliSetupWizardState.goToFirstStep() + } } } } diff --git a/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt index aa87b57..7ba9ea1 100644 --- a/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt @@ -57,7 +57,11 @@ class DeploymentUrlStep( notify("URL is invalid", e) return false } - CoderCliSetupWizardState.goToNextStep() + if (context.settingsStore.requireTokenAuth) { + CoderCliSetupWizardState.goToNextStep() + } else { + CoderCliSetupWizardState.goToLastStep() + } return true } diff --git a/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt b/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt index f1efca4..92a0845 100644 --- a/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt +++ b/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt @@ -25,7 +25,11 @@ object CoderCliSetupWizardState { currentStep = WizardStep.entries.toTypedArray()[(currentStep.ordinal - 1) % WizardStep.entries.size] } - fun resetSteps() { + fun goToLastStep() { + currentStep = WizardStep.CONNECT + } + + fun goToFirstStep() { currentStep = WizardStep.URL_REQUEST } } From 08789e62d858ff3d39a7107aa85653f4df860c3f Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sun, 20 Jul 2025 00:52:39 +0300 Subject: [PATCH 2/5] fix: don't require token if mTLS is configured --- src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 8d0e2d3..e01971c 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -47,7 +47,7 @@ class ConnectStep( context.i18n.pnotr("") } - if (CoderCliSetupContext.isNotReadyForAuth()) { + if (context.settingsStore.requireTokenAuth && CoderCliSetupContext.isNotReadyForAuth()) { errorField.textState.update { context.i18n.pnotr("URL and token were not properly configured. Please go back and provide a proper URL and token!") } From a3538b9bbcc44d71ad265253496958ffa5da555a Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sun, 20 Jul 2025 00:56:37 +0300 Subject: [PATCH 3/5] impl: support for certificate based authentication in URI handler --- .../kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index 23b015d..f299528 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -64,7 +64,7 @@ open class CoderProtocolHandler( context.logger.info("Handling $uri...") val deploymentURL = resolveDeploymentUrl(params) ?: return - val token = resolveToken(params) ?: return + val token = if (!context.settingsStore.requireTokenAuth) null else resolveToken(params) ?: return val workspaceName = resolveWorkspaceName(params) ?: return val restClient = buildRestClient(deploymentURL, token) ?: return val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return @@ -128,7 +128,7 @@ open class CoderProtocolHandler( return workspace } - private suspend fun buildRestClient(deploymentURL: String, token: String): CoderRestClient? { + private suspend fun buildRestClient(deploymentURL: String, token: String?): CoderRestClient? { try { return authenticate(deploymentURL, token) } catch (ex: Exception) { @@ -140,11 +140,11 @@ open class CoderProtocolHandler( /** * Returns an authenticated Coder CLI. */ - private suspend fun authenticate(deploymentURL: String, token: String): CoderRestClient { + private suspend fun authenticate(deploymentURL: String, token: String?): CoderRestClient { val client = CoderRestClient( context, deploymentURL.toURL(), - if (settings.requireTokenAuth) token else null, + token, PluginManager.pluginInfo.version ) client.initializeSession() From ae7ed672c38c0c2a7e30087d88fcd5ab0ac9d066 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sun, 20 Jul 2025 00:59:22 +0300 Subject: [PATCH 4/5] chore: next version is 0.5.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9513b30..79386fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.5.0 +version=0.5.1 group=com.coder.toolbox name=coder-toolbox From 5733c0ec566a978c2090c39c8826ca27a718fec6 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sun, 20 Jul 2025 01:10:45 +0300 Subject: [PATCH 5/5] chore: add UTs for token authentication --- .../coder/toolbox/sdk/CoderRestClientTest.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt index c42ead2..49314c5 100644 --- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt +++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt @@ -225,7 +225,7 @@ class CoderRestClientTest { val client = CoderRestClient(context, URL(url), "token") assertEquals(user.username, runBlocking { client.me() }.username) - val tests = listOf("invalid", null) + val tests = listOf("invalid") tests.forEach { token -> val ex = assertFailsWith( @@ -238,6 +238,26 @@ class CoderRestClientTest { srv.stop(0) } + @Test + fun `exception is raised when token is required for authentication and token value is null or empty`() { + listOf("", null).forEach { token -> + val ex = + assertFailsWith( + exceptionClass = IllegalStateException::class, + block = { + runBlocking { + CoderRestClient( + context, + URI.create("https://coder.com").toURL(), + token + ).me() + } + }, + ) + assertEquals(ex.message, "Token is required for https://coder.com deployment") + } + } + @Test fun testGetsWorkspaces() { val tests =