Skip to content

Commit dcc0e1b

Browse files
committed
Fix OAuth callback transition to connect wizard
Route OAuth callbacks through a pending OAuth connection instead of reading wizard model state directly, then create and schedule a fresh connect-step wizard after token exchange.
1 parent f96d60b commit dcc0e1b

5 files changed

Lines changed: 99 additions & 17 deletions

File tree

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.coder.toolbox.views.SuspendBiConsumer
2929
import com.coder.toolbox.views.state.CoderOAuthSessionContext
3030
import com.coder.toolbox.views.state.Credentials
3131
import com.coder.toolbox.views.state.PageRouter
32+
import com.coder.toolbox.views.state.PendingOAuthConnection
3233
import com.coder.toolbox.views.state.toSessionContext
3334
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
3435
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
@@ -417,15 +418,17 @@ class CoderRemoteProvider(
417418
)
418419
}
419420

420-
val activeWizard = router.activeWizard ?: return context.logAndShowError(
421-
FAILED_TO_HANDLE_OAUTH2_TITLE,
422-
"OAuth2 callback arrived but the setup wizard is no longer active"
423-
)
424-
val activeOAuthSession = activeWizard.model.oauthSession ?: return context.logAndShowError(
421+
if (!router.hasActiveWizard) {
422+
return context.logAndShowError(
423+
FAILED_TO_HANDLE_OAUTH2_TITLE,
424+
"OAuth2 callback arrived but the setup wizard is no longer active"
425+
)
426+
}
427+
val pendingOAuthConnection = router.pendingOAuthConnection ?: return context.logAndShowError(
425428
FAILED_TO_HANDLE_OAUTH2_TITLE,
426429
"OAuth2 callback arrived but no OAuth session was started"
427430
)
428-
params["state"]?.takeIf { it == activeOAuthSession.state }
431+
params["state"]?.takeIf { it == pendingOAuthConnection.session.state }
429432
?: return context.logAndShowError(
430433
FAILED_TO_HANDLE_OAUTH2_TITLE,
431434
"Server responded back with an invalid state that does not match the initial authorization state sent to the server"
@@ -443,19 +446,29 @@ class CoderRemoteProvider(
443446
)
444447
return
445448
}
446-
exchangeOAuthCodeForToken(code, activeOAuthSession, activeWizard)
449+
exchangeOAuthCodeForToken(code, pendingOAuthConnection)
447450
}
448451

449452
private suspend fun exchangeOAuthCodeForToken(
450453
code: String,
451-
oauthSessionContext: CoderOAuthSessionContext,
452-
wizard: CoderSetupWizardPage,
454+
pendingOAuthConnection: PendingOAuthConnection,
453455
) {
454456
try {
455457
context.logger.info("Handling OAuth callback...")
456458

459+
val oauthSessionContext = pendingOAuthConnection.session
457460
val tokenResponse = OAuth2Client(context).exchangeCode(oauthSessionContext, code)
458-
wizard.advanceToConnectWithOAuth(oauthSessionContext.copy(tokenResponse = tokenResponse))
461+
val wizard = CoderSetupWizardPage.connectStep(
462+
context, settingsPage, visibilityState,
463+
url = pendingOAuthConnection.url,
464+
credentials = Credentials.OAuth(oauthSessionContext.copy(tokenResponse = tokenResponse)),
465+
onConnect = onConnect,
466+
onTokenRefreshed = ::onTokenRefreshed,
467+
)
468+
router.navigate(wizard)
469+
470+
context.envPageManager.showPluginEnvironmentsPage(true)
471+
context.ui.showUiPage(wizard)
459472
} catch (e: Exception) {
460473
context.logAndShowError("OAuth Error", "Exception during token exchange: ${e.message}", e)
461474
}
@@ -523,7 +536,7 @@ class CoderRemoteProvider(
523536
*/
524537
override fun getOverrideUiPage(): UiPage? {
525538
// Show the setup wizard if one is already scheduled.
526-
router.activeWizard?.let { return it }
539+
router.activePage?.let { return it }
527540

528541
// Let the default workspace UI render if the HTTP client is initialized.
529542
if (client != null) return null

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.sdk.CoderRestClient
66
import com.coder.toolbox.views.state.CoderOAuthSessionContext
77
import com.coder.toolbox.views.state.Credentials
8+
import com.coder.toolbox.views.state.PendingOAuthConnection
89
import com.coder.toolbox.views.state.WizardModel
910
import com.coder.toolbox.views.state.WizardStep
1011
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
@@ -118,9 +119,10 @@ class CoderSetupWizardPage private constructor(
118119
}
119120
}
120121

121-
fun advanceToConnectWithOAuth(oauthSession: CoderOAuthSessionContext) {
122-
model.oauthSession = oauthSession
123-
model.goTo(WizardStep.CONNECT)
122+
fun pendingOAuthConnection(): PendingOAuthConnection? {
123+
val url = model.url ?: return null
124+
val oauthSession = model.oauthSession ?: return null
125+
return PendingOAuthConnection(url, oauthSession)
124126
}
125127

126128
private fun navigateBackFromConnect() {

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ sealed interface PageRoute {
2020
class PageRouter {
2121
private val route = MutableStateFlow<PageRoute>(PageRoute.None)
2222

23-
val activeWizard: CoderSetupWizardPage?
23+
private val activeWizard: CoderSetupWizardPage?
2424
get() = (route.value as? PageRoute.Wizard)?.page
2525

26+
val activePage: CoderPage?
27+
get() = activeWizard
28+
29+
val hasActiveWizard: Boolean
30+
get() = activeWizard != null
31+
32+
val pendingOAuthConnection: PendingOAuthConnection?
33+
get() = activeWizard?.pendingOAuthConnection()
34+
2635
/**
2736
* Returns the page already on this route, or builds a new one and
2837
* registers it.
@@ -47,4 +56,4 @@ class PageRouter {
4756
activeWizard?.dispose()
4857
route.value = PageRoute.None
4958
}
50-
}
59+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,9 @@ sealed interface Credentials {
6464
object MTls : Credentials
6565
data class Token(val value: String) : Credentials
6666
data class OAuth(val session: CoderOAuthSessionContext) : Credentials
67-
}
67+
}
68+
69+
data class PendingOAuthConnection(
70+
val url: URL,
71+
val session: CoderOAuthSessionContext
72+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.coder.toolbox.views.state
2+
3+
import com.coder.toolbox.oauth.TokenEndpointAuthMethod
4+
import com.coder.toolbox.views.CoderSetupWizardPage
5+
import io.mockk.every
6+
import io.mockk.mockk
7+
import java.net.URI
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertNull
11+
12+
class PageRouterTest {
13+
14+
@Test
15+
fun `given no active wizard when pending OAuth connection is requested then null is returned`() {
16+
// given
17+
val router = PageRouter()
18+
19+
// when
20+
val pendingOAuthConnection = router.pendingOAuthConnection
21+
22+
// then
23+
assertNull(pendingOAuthConnection)
24+
}
25+
26+
@Test
27+
fun `given active wizard when pending OAuth connection is requested then wizard connection is returned`() {
28+
// given
29+
val pendingConnection = PendingOAuthConnection(
30+
url = URI("https://coder.example.com").toURL(),
31+
session = oauthSession()
32+
)
33+
val wizard = mockk<CoderSetupWizardPage>(relaxed = true)
34+
every { wizard.pendingOAuthConnection() } returns pendingConnection
35+
val router = PageRouter()
36+
router.navigate(wizard)
37+
38+
// when
39+
val pendingOAuthConnection = router.pendingOAuthConnection
40+
41+
// then
42+
assertEquals(pendingConnection, pendingOAuthConnection)
43+
}
44+
45+
private fun oauthSession(): CoderOAuthSessionContext = CoderOAuthSessionContext(
46+
clientId = "client-id",
47+
clientSecret = "client-secret",
48+
tokenCodeVerifier = "verifier",
49+
state = "state",
50+
tokenEndpoint = "https://coder.example.com/oauth/token",
51+
tokenAuthMethod = TokenEndpointAuthMethod.CLIENT_SECRET_BASIC
52+
)
53+
}

0 commit comments

Comments
 (0)