Skip to content

Commit 23cab56

Browse files
authored
impl: support for certificate based authentication (#155)
We now skip token input screen if the user provided a public and a private key for mTLS authentication on both the usual welcome screen and in the URI handling. Attention: the official coder deployment supports only authentication via token, which is why I could not fully test an end to end scenario.
1 parent 3a21b45 commit 23cab56

File tree

9 files changed

+57
-18
lines changed

9 files changed

+57
-18
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- support for certificate based authentication
8+
59
## 0.5.0 - 2025-07-17
610

711
### Added

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.5.0
1+
version=0.5.1
22
group=com.coder.toolbox
33
name=coder-toolbox

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ class CoderRemoteProvider(
245245
environments.value = LoadableState.Value(emptyList())
246246
isInitialized.update { false }
247247
client = null
248-
CoderCliSetupWizardState.resetSteps()
248+
CoderCliSetupWizardState.goToFirstStep()
249249
}
250250

251251
override val svgIcon: SvgIcon =

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ open class CoderRestClient(
9494
.build()
9595
}
9696

97-
if (token != null) {
97+
if (context.settingsStore.requireTokenAuth) {
98+
if (token.isNullOrBlank()) {
99+
throw IllegalStateException("Token is required for $url deployment")
100+
}
98101
builder = builder.addInterceptor {
99102
it.proceed(
100103
it.request().newBuilder().addHeader("Coder-Session-Token", token).build()

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ open class CoderProtocolHandler(
6464

6565
context.logger.info("Handling $uri...")
6666
val deploymentURL = resolveDeploymentUrl(params) ?: return
67-
val token = resolveToken(params) ?: return
67+
val token = if (!context.settingsStore.requireTokenAuth) null else resolveToken(params) ?: return
6868
val workspaceName = resolveWorkspaceName(params) ?: return
6969
val restClient = buildRestClient(deploymentURL, token) ?: return
7070
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return
@@ -128,7 +128,7 @@ open class CoderProtocolHandler(
128128
return workspace
129129
}
130130

131-
private suspend fun buildRestClient(deploymentURL: String, token: String): CoderRestClient? {
131+
private suspend fun buildRestClient(deploymentURL: String, token: String?): CoderRestClient? {
132132
try {
133133
return authenticate(deploymentURL, token)
134134
} catch (ex: Exception) {
@@ -140,11 +140,11 @@ open class CoderProtocolHandler(
140140
/**
141141
* Returns an authenticated Coder CLI.
142142
*/
143-
private suspend fun authenticate(deploymentURL: String, token: String): CoderRestClient {
143+
private suspend fun authenticate(deploymentURL: String, token: String?): CoderRestClient {
144144
val client = CoderRestClient(
145145
context,
146146
deploymentURL.toURL(),
147-
if (settings.requireTokenAuth) token else null,
147+
token,
148148
PluginManager.pluginInfo.version
149149
)
150150
client.initializeSession()

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class ConnectStep(
4747
context.i18n.pnotr("")
4848
}
4949

50-
if (CoderCliSetupContext.isNotReadyForAuth()) {
50+
if (context.settingsStore.requireTokenAuth && CoderCliSetupContext.isNotReadyForAuth()) {
5151
errorField.textState.update {
5252
context.i18n.pnotr("URL and token were not properly configured. Please go back and provide a proper URL and token!")
5353
}
@@ -67,7 +67,7 @@ class ConnectStep(
6767
return
6868
}
6969

70-
if (!CoderCliSetupContext.hasToken()) {
70+
if (context.settingsStore.requireTokenAuth && !CoderCliSetupContext.hasToken()) {
7171
errorField.textState.update { context.i18n.ptrl("Token is required") }
7272
return
7373
}
@@ -77,7 +77,7 @@ class ConnectStep(
7777
val client = CoderRestClient(
7878
context,
7979
CoderCliSetupContext.url!!,
80-
CoderCliSetupContext.token!!,
80+
if (context.settingsStore.requireTokenAuth) CoderCliSetupContext.token else null,
8181
PluginManager.pluginInfo.version,
8282
)
8383
// allows interleaving with the back/cancel action
@@ -91,17 +91,17 @@ class ConnectStep(
9191
statusField.textState.update { (context.i18n.pnotr(progress)) }
9292
}
9393
// We only need to log in if we are using token-based auth.
94-
if (client.token != null) {
94+
if (context.settingsStore.requireTokenAuth) {
9595
statusField.textState.update { (context.i18n.ptrl("Configuring Coder CLI...")) }
9696
// allows interleaving with the back/cancel action
9797
yield()
98-
cli.login(client.token)
98+
cli.login(client.token!!)
9999
}
100100
statusField.textState.update { (context.i18n.ptrl("Successfully configured ${CoderCliSetupContext.url!!.host}...")) }
101101
// allows interleaving with the back/cancel action
102102
yield()
103103
CoderCliSetupContext.reset()
104-
CoderCliSetupWizardState.resetSteps()
104+
CoderCliSetupWizardState.goToFirstStep()
105105
onConnect(client, cli)
106106
} catch (ex: CancellationException) {
107107
if (ex.message != USER_HIT_THE_BACK_BUTTON) {
@@ -127,10 +127,14 @@ class ConnectStep(
127127
} finally {
128128
if (shouldAutoLogin.value) {
129129
CoderCliSetupContext.reset()
130-
CoderCliSetupWizardState.resetSteps()
130+
CoderCliSetupWizardState.goToFirstStep()
131131
context.secrets.rememberMe = false
132132
} else {
133-
CoderCliSetupWizardState.goToPreviousStep()
133+
if (context.settingsStore.requireTokenAuth) {
134+
CoderCliSetupWizardState.goToPreviousStep()
135+
} else {
136+
CoderCliSetupWizardState.goToFirstStep()
137+
}
134138
}
135139
}
136140
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ class DeploymentUrlStep(
8585
notify("URL is invalid", e)
8686
return false
8787
}
88-
CoderCliSetupWizardState.goToNextStep()
88+
if (context.settingsStore.requireTokenAuth) {
89+
CoderCliSetupWizardState.goToNextStep()
90+
} else {
91+
CoderCliSetupWizardState.goToLastStep()
92+
}
8993
return true
9094
}
9195

src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ object CoderCliSetupWizardState {
2525
currentStep = WizardStep.entries.toTypedArray()[(currentStep.ordinal - 1) % WizardStep.entries.size]
2626
}
2727

28-
fun resetSteps() {
28+
fun goToLastStep() {
29+
currentStep = WizardStep.CONNECT
30+
}
31+
32+
fun goToFirstStep() {
2933
currentStep = WizardStep.URL_REQUEST
3034
}
3135
}

src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ class CoderRestClientTest {
225225
val client = CoderRestClient(context, URL(url), "token")
226226
assertEquals(user.username, runBlocking { client.me() }.username)
227227

228-
val tests = listOf("invalid", null)
228+
val tests = listOf("invalid")
229229
tests.forEach { token ->
230230
val ex =
231231
assertFailsWith(
@@ -238,6 +238,26 @@ class CoderRestClientTest {
238238
srv.stop(0)
239239
}
240240

241+
@Test
242+
fun `exception is raised when token is required for authentication and token value is null or empty`() {
243+
listOf("", null).forEach { token ->
244+
val ex =
245+
assertFailsWith(
246+
exceptionClass = IllegalStateException::class,
247+
block = {
248+
runBlocking {
249+
CoderRestClient(
250+
context,
251+
URI.create("https://coder.com").toURL(),
252+
token
253+
).me()
254+
}
255+
},
256+
)
257+
assertEquals(ex.message, "Token is required for https://coder.com deployment")
258+
}
259+
}
260+
241261
@Test
242262
fun testGetsWorkspaces() {
243263
val tests =

0 commit comments

Comments
 (0)