Skip to content

impl: support for certificate based authentication #155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- support for certificate based authentication

## 0.5.0 - 2025-07-17

### Added
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=0.5.0
version=0.5.1
group=com.coder.toolbox
name=coder-toolbox
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ class CoderRemoteProvider(
environments.value = LoadableState.Value(emptyList())
isInitialized.update { false }
client = null
CoderCliSetupWizardState.resetSteps()
CoderCliSetupWizardState.goToFirstStep()
}

override val svgIcon: SvgIcon =
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
Expand Down
20 changes: 12 additions & 8 deletions src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
}
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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()
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ class DeploymentUrlStep(
notify("URL is invalid", e)
return false
}
CoderCliSetupWizardState.goToNextStep()
if (context.settingsStore.requireTokenAuth) {
CoderCliSetupWizardState.goToNextStep()
} else {
CoderCliSetupWizardState.goToLastStep()
}
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
22 changes: 21 additions & 1 deletion src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 =
Expand Down
Loading