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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy