Skip to content

Commit f96d60b

Browse files
committed
fix: auto login when OAuth2 session is available
but the user disabled the "Prefer OAuth2 option..."
1 parent dbe3ceb commit f96d60b

2 files changed

Lines changed: 157 additions & 12 deletions

File tree

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ class CoderRemoteProvider(
6868
private var pollJob: Job? = null
6969
internal val lastEnvironments = mutableListOf<CoderRemoteEnvironment>()
7070

71-
private val settings = context.settingsStore.readOnly()
72-
7371
private val triggerSshConfig = Channel<Boolean>(Channel.CONFLATED)
7472
private val triggerProviderVisible = Channel<Boolean>(Channel.CONFLATED)
7573
private val dialogUi = DialogUi(context)
@@ -544,11 +542,11 @@ class CoderRemoteProvider(
544542
if (shouldDoAutoSetup()) {
545543
try {
546544
val url = context.deploymentUrl
547-
val credentials = context.secrets.oauthSessionFor(url.toString())?.let {
548-
Credentials.OAuth(it.toSessionContext())
549-
} ?: context.secrets.apiTokenFor(url)?.let {
550-
Credentials.Token(it)
551-
} ?: Credentials.MTls
545+
val credentials = autoSetupCredentials(url) ?: return CoderSetupWizardPage.deploymentUrlStep(
546+
context, settingsPage, visibilityState,
547+
onConnect = onConnect,
548+
onTokenRefreshed = ::onTokenRefreshed,
549+
)
552550
return CoderSetupWizardPage.connectStep(
553551
context, settingsPage, visibilityState,
554552
url = url,
@@ -578,13 +576,25 @@ class CoderRemoteProvider(
578576
}
579577

580578
/**
581-
* Auto-login only on first the firs run if there is a url & token configured or the auth
582-
* should be done via certificates.
579+
* Auto-login only on the first run when stored credentials or mTLS auth can be used.
583580
*/
584-
private fun shouldDoAutoSetup(): Boolean = firstRun && (canAutoLogin() || !settings.requiresTokenAuth)
581+
private fun shouldDoAutoSetup(): Boolean = firstRun && (canAutoLogin() || !context.settingsStore.requiresTokenAuth)
585582

586-
fun canAutoLogin(): Boolean = !context.secrets.apiTokenFor(context.deploymentUrl)
587-
.isNullOrBlank() || context.secrets.oauthSessionFor(context.deploymentUrl.toString()) != null
583+
fun canAutoLogin(): Boolean = autoSetupCredentials(context.deploymentUrl) != null
584+
585+
private fun autoSetupCredentials(url: URL): Credentials? {
586+
if (context.settingsStore.requiresMTlsAuth) return Credentials.MTls
587+
588+
val tokenCredentials = context.secrets.apiTokenFor(url)
589+
?.takeIf { it.isNotBlank() }
590+
?.let { Credentials.Token(it) }
591+
592+
if (!context.settingsStore.preferOAuth2IfAvailable) return tokenCredentials
593+
594+
return context.secrets.oauthSessionFor(url.toString())?.let {
595+
Credentials.OAuth(it.toSessionContext())
596+
} ?: tokenCredentials
597+
}
588598

589599
private suspend fun onTokenRefreshed(url: URL, oauthSessionCtx: CoderOAuthSessionContext) {
590600
oauthSessionCtx.tokenResponse?.accessToken?.let { cli?.login(it) }

src/test/kotlin/com/coder/toolbox/CoderRemoteProviderTest.kt

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.coder.toolbox
22

33
import com.coder.toolbox.cli.CoderCLIManager
4+
import com.coder.toolbox.oauth.TokenEndpointAuthMethod
45
import com.coder.toolbox.sdk.CoderRestClient
56
import com.coder.toolbox.sdk.v2.models.Workspace
67
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
@@ -10,6 +11,8 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceBuild
1011
import com.coder.toolbox.sdk.v2.models.WorkspaceResource
1112
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
1213
import com.coder.toolbox.views.CoderSetupWizardPage
14+
import com.coder.toolbox.views.state.StoredOAuthSession
15+
import com.coder.toolbox.views.state.WizardStep
1316
import io.mockk.clearAllMocks
1417
import io.mockk.coEvery
1518
import io.mockk.coVerify
@@ -256,6 +259,130 @@ class CoderRemoteProviderTest {
256259
verify { mockContext.popupPluginMainPage() }
257260
}
258261

262+
@Test
263+
fun `given mTLS is required when auto setup has stored credentials then mTLS takes precedence`() {
264+
// given
265+
val url = URI("https://coder.example.com").toURL()
266+
every { mockContext.deploymentUrl } returns url
267+
every { mockContext.settingsStore.requiresMTlsAuth } returns true
268+
every { mockContext.settingsStore.requiresTokenAuth } returns false
269+
every { mockContext.settingsStore.preferOAuth2IfAvailable } returns true
270+
every { mockContext.secrets.apiTokenFor(url) } returns "token"
271+
every { mockContext.secrets.oauthSessionFor(url.toString()) } returns storedOAuthSession()
272+
val provider = CoderRemoteProvider(mockContext)
273+
274+
// when
275+
val overridePage = provider.getOverrideUiPage() as CoderSetupWizardPage
276+
277+
// then
278+
assertEquals(WizardStep.CONNECT, overridePage.model.currentStep())
279+
assertNull(overridePage.model.token)
280+
assertNull(overridePage.model.oauthSession)
281+
}
282+
283+
@Test
284+
fun `given OAuth is preferred when auto setup has token and OAuth session then OAuth is used`() {
285+
// given
286+
val url = URI("https://coder.example.com").toURL()
287+
every { mockContext.deploymentUrl } returns url
288+
every { mockContext.settingsStore.requiresMTlsAuth } returns false
289+
every { mockContext.settingsStore.requiresTokenAuth } returns true
290+
every { mockContext.settingsStore.preferOAuth2IfAvailable } returns true
291+
every { mockContext.secrets.apiTokenFor(url) } returns "token"
292+
every { mockContext.secrets.oauthSessionFor(url.toString()) } returns storedOAuthSession()
293+
val provider = CoderRemoteProvider(mockContext)
294+
295+
// when
296+
val overridePage = provider.getOverrideUiPage() as CoderSetupWizardPage
297+
298+
// then
299+
assertEquals(WizardStep.CONNECT, overridePage.model.currentStep())
300+
assertNull(overridePage.model.token)
301+
assertNotNull(overridePage.model.oauthSession)
302+
}
303+
304+
@Test
305+
fun `given OAuth is not preferred when auto setup has token and OAuth session then token is used`() {
306+
// given
307+
val url = URI("https://coder.example.com").toURL()
308+
every { mockContext.deploymentUrl } returns url
309+
every { mockContext.settingsStore.requiresMTlsAuth } returns false
310+
every { mockContext.settingsStore.requiresTokenAuth } returns true
311+
every { mockContext.settingsStore.preferOAuth2IfAvailable } returns false
312+
every { mockContext.secrets.apiTokenFor(url) } returns "token"
313+
every { mockContext.secrets.oauthSessionFor(url.toString()) } returns storedOAuthSession()
314+
val provider = CoderRemoteProvider(mockContext)
315+
316+
// when
317+
val overridePage = provider.getOverrideUiPage() as CoderSetupWizardPage
318+
319+
// then
320+
assertEquals(WizardStep.CONNECT, overridePage.model.currentStep())
321+
assertEquals("token", overridePage.model.token)
322+
assertNull(overridePage.model.oauthSession)
323+
}
324+
325+
@Test
326+
fun `given OAuth is not preferred when auto setup has API token then token is used`() {
327+
// given
328+
val url = URI("https://coder.example.com").toURL()
329+
every { mockContext.deploymentUrl } returns url
330+
every { mockContext.settingsStore.requiresMTlsAuth } returns false
331+
every { mockContext.settingsStore.requiresTokenAuth } returns true
332+
every { mockContext.settingsStore.preferOAuth2IfAvailable } returns false
333+
every { mockContext.secrets.apiTokenFor(url) } returns "api-token"
334+
val provider = CoderRemoteProvider(mockContext)
335+
336+
// when
337+
val overridePage = provider.getOverrideUiPage() as CoderSetupWizardPage
338+
339+
// then
340+
assertEquals(WizardStep.CONNECT, overridePage.model.currentStep())
341+
assertEquals("api-token", overridePage.model.token)
342+
assertNull(overridePage.model.oauthSession)
343+
}
344+
345+
@Test
346+
fun `given OAuth is not preferred when auto setup has no API token then wizard starts at URL step`() {
347+
// given
348+
val url = URI("https://coder.example.com").toURL()
349+
every { mockContext.deploymentUrl } returns url
350+
every { mockContext.settingsStore.requiresMTlsAuth } returns false
351+
every { mockContext.settingsStore.requiresTokenAuth } returns true
352+
every { mockContext.settingsStore.preferOAuth2IfAvailable } returns false
353+
every { mockContext.secrets.apiTokenFor(url) } returns null
354+
val provider = CoderRemoteProvider(mockContext)
355+
356+
// when
357+
val overridePage = provider.getOverrideUiPage() as CoderSetupWizardPage
358+
359+
// then
360+
assertEquals(WizardStep.URL_REQUEST, overridePage.model.currentStep())
361+
assertNull(overridePage.model.token)
362+
assertNull(overridePage.model.oauthSession)
363+
}
364+
365+
@Test
366+
fun `given OAuth is not preferred when auto setup only has OAuth session then wizard starts at URL step`() {
367+
// given
368+
val url = URI("https://coder.example.com").toURL()
369+
every { mockContext.deploymentUrl } returns url
370+
every { mockContext.settingsStore.requiresMTlsAuth } returns false
371+
every { mockContext.settingsStore.requiresTokenAuth } returns true
372+
every { mockContext.settingsStore.preferOAuth2IfAvailable } returns false
373+
every { mockContext.secrets.apiTokenFor(url) } returns null
374+
every { mockContext.secrets.oauthSessionFor(url.toString()) } returns storedOAuthSession()
375+
val provider = CoderRemoteProvider(mockContext)
376+
377+
// when
378+
val overridePage = provider.getOverrideUiPage() as CoderSetupWizardPage
379+
380+
// then
381+
assertEquals(WizardStep.URL_REQUEST, overridePage.model.currentStep())
382+
assertNull(overridePage.model.token)
383+
assertNull(overridePage.model.oauthSession)
384+
}
385+
259386
@Test
260387
fun `given no existing environment then one is created`() = runTest {
261388
// given
@@ -411,4 +538,12 @@ class CoderRemoteProviderTest {
411538
set(target, value)
412539
}
413540
}
541+
542+
private fun storedOAuthSession(): StoredOAuthSession = StoredOAuthSession(
543+
clientId = "client-id",
544+
clientSecret = "client-secret",
545+
refreshToken = "refresh-token",
546+
tokenAuthMethod = TokenEndpointAuthMethod.CLIENT_SECRET_BASIC,
547+
tokenEndpoint = "https://coder.example.com/oauth/token"
548+
)
414549
}

0 commit comments

Comments
 (0)