Skip to content

Commit 0e3ca46

Browse files
committed
Authenticate with session token
- gives users more flexibility in terms of how they want to authenticate (coder, github, etc) - better UX because there is less input from user - the flow opens up a http page with the login path - user is required to copy and paste session token - also simplified some data models
1 parent 705dc0a commit 0e3ca46

9 files changed

+105
-88
lines changed

src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ package com.coder.gateway.models
22

33
import com.coder.gateway.sdk.v1.Workspace
44

5-
data class CoderWorkspacesWizardModel(var loginModel: LoginModel, var workspaces: List<Workspace>)
5+
data class CoderWorkspacesWizardModel(var coderURL: String = "https://localhost", var token: String = "", var workspaces: List<Workspace> = mutableListOf())

src/main/kotlin/com/coder/gateway/models/LoginModel.kt

-3
This file was deleted.

src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt

+16-12
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import com.coder.gateway.sdk.convertors.InstantConverter
44
import com.coder.gateway.sdk.ex.AuthenticationException
55
import com.coder.gateway.sdk.v2.CoderV2RestFacade
66
import com.coder.gateway.sdk.v2.models.AgentGitSSHKeys
7-
import com.coder.gateway.sdk.v2.models.LoginWithPasswordRequest
87
import com.coder.gateway.sdk.v2.models.User
98
import com.coder.gateway.sdk.v2.models.Workspace
109
import com.google.gson.Gson
1110
import com.google.gson.GsonBuilder
1211
import com.intellij.openapi.components.Service
12+
import okhttp3.Cookie
13+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
1314
import okhttp3.JavaNetCookieJar
1415
import okhttp3.OkHttpClient
1516
import okhttp3.logging.HttpLoggingInterceptor
@@ -22,14 +23,22 @@ import java.time.Instant
2223
@Service(Service.Level.APP)
2324
class CoderRestClientService {
2425
private lateinit var retroRestClient: CoderV2RestFacade
25-
lateinit var me: User
26+
lateinit var coderURL: URL
2627
lateinit var sessionToken: String
28+
lateinit var me: User
2729

2830
/**
2931
* This must be called before anything else. It will authenticate with coder and retrieve a session token
3032
* @throws [AuthenticationException] if authentication failed
3133
*/
32-
fun initClientSession(url: URL, email: String, password: String) {
34+
fun initClientSession(url: URL, token: String): User? {
35+
val cookieUrl = url.toHttpUrlOrNull()!!
36+
val cookieJar = JavaNetCookieJar(CookieManager()).apply {
37+
saveFromResponse(
38+
cookieUrl,
39+
listOf(Cookie.parse(cookieUrl, "session_token=$token")!!)
40+
)
41+
}
3342
val gson: Gson = GsonBuilder()
3443
.registerTypeAdapter(Instant::class.java, InstantConverter())
3544
.setPrettyPrinting()
@@ -42,27 +51,22 @@ class CoderRestClientService {
4251
.client(
4352
OkHttpClient.Builder()
4453
.addInterceptor(interceptor)
45-
.cookieJar(JavaNetCookieJar(CookieManager()))
54+
.cookieJar(cookieJar)
4655
.build()
4756
)
4857
.addConverterFactory(GsonConverterFactory.create(gson))
4958
.build()
5059
.create(CoderV2RestFacade::class.java)
5160

52-
val loginResponse = retroRestClient.authenticate(LoginWithPasswordRequest(email, password)).execute()
53-
54-
if (!loginResponse.isSuccessful) {
55-
throw AuthenticationException("Authentication failed with code:${loginResponse.code()}, reason: ${loginResponse.message()}")
56-
}
57-
58-
sessionToken = loginResponse.body()?.sessionToken!!
59-
6061
val userResponse = retroRestClient.me().execute()
6162
if (!userResponse.isSuccessful) {
6263
throw IllegalStateException("Could not retrieve information about logged use:${userResponse.code()}, reason: ${userResponse.message()}")
6364
}
6465

66+
coderURL = url
67+
sessionToken = token
6568
me = userResponse.body()!!
69+
return me
6670
}
6771

6872
fun workspaces(): List<Workspace> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.coder.gateway.sdk
2+
3+
import java.net.URL
4+
5+
6+
fun String.toURL(): URL {
7+
return URL(this)
8+
}
9+
10+
fun URL.withPath(path: String): URL {
11+
return URL(this.protocol, this.host, this.port, path)
12+
}

src/main/kotlin/com/coder/gateway/views/CoderGatewayConnectorWizardView.kt

+14-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.coder.gateway.views
22

33
import com.coder.gateway.models.CoderWorkspacesWizardModel
4-
import com.coder.gateway.models.LoginModel
54
import com.coder.gateway.views.steps.CoderAuthStepView
65
import com.coder.gateway.views.steps.CoderWorkspacesStepView
76
import com.coder.gateway.views.steps.CoderWorkspacesWizardStep
@@ -12,18 +11,13 @@ import com.intellij.ui.dsl.builder.panel
1211
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
1312
import com.intellij.util.ui.components.BorderLayoutPanel
1413
import com.jetbrains.gateway.api.GatewayUI
15-
import kotlinx.coroutines.CoroutineScope
16-
import kotlinx.coroutines.Dispatchers
17-
import kotlinx.coroutines.launch
18-
import kotlinx.coroutines.withContext
1914
import java.awt.Component
2015
import javax.swing.JButton
2116

2217
class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable {
23-
private val cs = CoroutineScope(Dispatchers.Main)
2418
private var steps = arrayListOf<CoderWorkspacesWizardStep>()
2519
private var currentStep = 0
26-
private val model = CoderWorkspacesWizardModel(LoginModel(), emptyList())
20+
private val model = CoderWorkspacesWizardModel()
2721

2822
private lateinit var previousButton: JButton
2923
private lateinit var nextButton: JButton
@@ -72,29 +66,27 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable {
7266
}
7367

7468
private fun next() {
75-
cs.launch {
76-
withContext(Dispatchers.Main) { doNextCallback() }
77-
if (currentStep + 1 < steps.size) {
78-
remove(steps[currentStep].component)
69+
if (!doNextCallback()) return
70+
if (currentStep + 1 < steps.size) {
71+
remove(steps[currentStep].component)
72+
updateUI()
73+
currentStep++
74+
steps[currentStep].apply {
75+
addToCenter(component)
76+
onInit(model)
7977
updateUI()
80-
currentStep++
81-
steps[currentStep].apply {
82-
addToCenter(component)
83-
onInit(model)
84-
updateUI()
85-
86-
nextButton.text = nextActionText
87-
previousButton.text = previousActionText
88-
}
78+
79+
nextButton.text = nextActionText
80+
previousButton.text = previousActionText
8981
}
9082
}
9183
}
9284

9385

94-
private suspend fun doNextCallback() {
86+
private fun doNextCallback(): Boolean {
9587
steps[currentStep].apply {
9688
component.apply()
97-
onNext(model)
89+
return onNext(model)
9890
}
9991
}
10092

src/main/kotlin/com/coder/gateway/views/steps/CoderAuthStepView.kt

+54-43
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@ package com.coder.gateway.views.steps
22

33
import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.models.CoderWorkspacesWizardModel
5-
import com.coder.gateway.models.LoginModel
65
import com.coder.gateway.sdk.CoderCLIManager
76
import com.coder.gateway.sdk.CoderRestClientService
87
import com.coder.gateway.sdk.OS
98
import com.coder.gateway.sdk.getOS
10-
import com.intellij.credentialStore.CredentialAttributes
11-
import com.intellij.credentialStore.askPassword
9+
import com.coder.gateway.sdk.toURL
10+
import com.coder.gateway.sdk.withPath
11+
import com.intellij.ide.BrowserUtil
1212
import com.intellij.ide.IdeBundle
1313
import com.intellij.openapi.Disposable
1414
import com.intellij.openapi.application.ApplicationManager
15+
import com.intellij.openapi.application.ModalityState
16+
import com.intellij.openapi.application.invokeAndWaitIfNeeded
1517
import com.intellij.openapi.progress.ProgressIndicator
1618
import com.intellij.openapi.progress.ProgressManager
1719
import com.intellij.openapi.progress.Task
1820
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
1921
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
22+
import com.intellij.ui.AppIcon
2023
import com.intellij.ui.IconManager
24+
import com.intellij.ui.components.JBTextField
25+
import com.intellij.ui.components.dialog
2126
import com.intellij.ui.dsl.builder.BottomGap
2227
import com.intellij.ui.dsl.builder.RightGap
2328
import com.intellij.ui.dsl.builder.TopGap
@@ -29,12 +34,12 @@ import kotlinx.coroutines.CoroutineScope
2934
import kotlinx.coroutines.Dispatchers
3035
import kotlinx.coroutines.cancel
3136
import org.zeroturnaround.exec.ProcessExecutor
32-
import java.net.URL
37+
import java.awt.Dimension
3338
import java.util.logging.Logger
3439

3540
class CoderAuthStepView : CoderWorkspacesWizardStep, Disposable {
3641
private val cs = CoroutineScope(Dispatchers.Main)
37-
private var model = LoginModel()
42+
private var model = CoderWorkspacesWizardModel()
3843
private val coderClient: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java)
3944

4045
override val component = panel {
@@ -51,14 +56,8 @@ class CoderAuthStepView : CoderWorkspacesWizardStep, Disposable {
5156
row {
5257
browserLink(CoderGatewayBundle.message("gateway.connector.view.login.documentation.action"), "https://coder.com/docs/coder/latest/workspaces")
5358
}.bottomGap(BottomGap.MEDIUM)
54-
row {
55-
label(CoderGatewayBundle.message("gateway.connector.view.login.url.label"))
56-
textField().resizableColumn().horizontalAlign(HorizontalAlign.FILL).gap(RightGap.SMALL).bindText(model::url)
57-
cell()
58-
}
59-
60-
row(CoderGatewayBundle.message("gateway.connector.view.login.email.label")) {
61-
textField().resizableColumn().bindText(model::email)
59+
row(CoderGatewayBundle.message("gateway.connector.view.login.url.label")) {
60+
textField().resizableColumn().horizontalAlign(HorizontalAlign.FILL).gap(RightGap.SMALL).bindText(model::coderURL)
6261
cell()
6362
}
6463
}
@@ -71,45 +70,31 @@ class CoderAuthStepView : CoderWorkspacesWizardStep, Disposable {
7170

7271
override fun onInit(wizardModel: CoderWorkspacesWizardModel) {
7372
model.apply {
74-
url = wizardModel.loginModel.url
75-
email = wizardModel.loginModel.email
76-
password = wizardModel.loginModel.password
73+
coderURL = wizardModel.coderURL
74+
token = wizardModel.token
7775
}
7876
component.apply()
7977
}
8078

81-
override suspend fun onNext(wizardModel: CoderWorkspacesWizardModel) {
82-
val password = askPassword(
83-
null,
84-
CoderGatewayBundle.message("gateway.connector.view.login.credentials.dialog.title"),
85-
CoderGatewayBundle.message("gateway.connector.view.login.password.label"),
86-
CredentialAttributes("Coder"),
87-
true
88-
)
89-
model.password = password
90-
val authTask = object : Task.Modal(null, "Authenticate and setup coder", false) {
91-
override fun run(pi: ProgressIndicator) {
92-
93-
pi.apply {
94-
text = "Authenticating ${model.email} on ${model.url}..."
95-
fraction = 0.3
96-
}
97-
98-
val url = URL(model.url)
99-
coderClient.initClientSession(url, model.email, model.password!!)
100-
wizardModel.apply {
101-
loginModel = model.copy()
102-
}
79+
override fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean {
80+
BrowserUtil.browse(model.coderURL.toURL().withPath("/login?redirect=%2Fcli-auth"))
81+
val pastedToken = askToken()
10382

83+
if (pastedToken?.isNullOrBlank() == true || coderClient.initClientSession(model.coderURL.toURL(), pastedToken) == null) {
84+
return false
85+
}
86+
model.token = pastedToken
87+
val authTask = object : Task.Modal(null, CoderGatewayBundle.message("gateway.connector.view.login.cli.downloader.dialog.title"), false) {
88+
override fun run(pi: ProgressIndicator) {
10489
pi.apply {
10590
text = "Downloading coder cli..."
106-
fraction = 0.4
91+
fraction = 0.1
10792
}
10893

109-
val cliManager = CoderCLIManager(URL(url.protocol, url.host, url.port, ""))
94+
val cliManager = CoderCLIManager(model.coderURL.toURL())
11095
val cli = cliManager.download() ?: throw IllegalStateException("Could not download coder binary")
11196
if (getOS() != OS.WINDOWS) {
112-
pi.fraction = 0.5
97+
pi.fraction = 0.4
11398
val chmodOutput = ProcessExecutor().command("chmod", "+x", cli.toAbsolutePath().toString()).readOutput(true).execute().outputUTF8()
11499
logger.info("chmod +x ${cli.toAbsolutePath()} $chmodOutput")
115100
}
@@ -118,15 +103,41 @@ class CoderAuthStepView : CoderWorkspacesWizardStep, Disposable {
118103
fraction = 0.5
119104
}
120105

121-
val loginOutput = ProcessExecutor().command(cli.toAbsolutePath().toString(), "login", url.toString(), "--token", coderClient.sessionToken).readOutput(true).execute().outputUTF8()
106+
val loginOutput = ProcessExecutor().command(cli.toAbsolutePath().toString(), "login", model.coderURL, "--token", coderClient.sessionToken).readOutput(true).execute().outputUTF8()
122107
logger.info("coder-cli login output: $loginOutput")
123-
pi.fraction = 0.6
108+
pi.fraction = 0.8
124109
val sshConfigOutput = ProcessExecutor().command(cli.toAbsolutePath().toString(), "config-ssh").readOutput(true).execute().outputUTF8()
125110
logger.info("coder-cli config-ssh output: $sshConfigOutput")
126111
pi.fraction = 1.0
127112
}
128113
}
114+
wizardModel.apply {
115+
coderURL = model.coderURL
116+
token = model.token
117+
}
129118
ProgressManager.getInstance().run(authTask)
119+
return true
120+
}
121+
122+
private fun askToken(): String? {
123+
return invokeAndWaitIfNeeded(ModalityState.any()) {
124+
lateinit var sessionTokenTextField: JBTextField
125+
126+
val panel = panel {
127+
row {
128+
label(CoderGatewayBundle.message("gateway.connector.view.login.token.label"))
129+
sessionTokenTextField = textField().applyToComponent {
130+
minimumSize = Dimension(320, -1)
131+
}.component
132+
}
133+
}
134+
135+
AppIcon.getInstance().requestAttention(null, true)
136+
if (!dialog(CoderGatewayBundle.message("gateway.connector.view.login.token.dialog"), panel = panel, focusedComponent = sessionTokenTextField).showAndGet()) {
137+
return@invokeAndWaitIfNeeded null
138+
}
139+
return@invokeAndWaitIfNeeded sessionTokenTextField.text
140+
}
130141
}
131142

132143
override fun dispose() {

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,25 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
8686
}
8787
}
8888

89-
override suspend fun onNext(wizardModel: CoderWorkspacesWizardModel) {
89+
override fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean {
9090
val workspace = workspacesView.selectedValue
9191
if (workspace != null) {
9292
logger.info("Connecting to ${workspace.name}...")
9393
cs.launch {
9494
GatewayUI.getInstance().connect(
9595
mapOf(
9696
"type" to "coder",
97-
"coder_url" to wizardModel.loginModel.url,
97+
"coder_url" to coderClient.coderURL.toString(),
9898
"workspace_name" to workspace.name,
9999
"username" to coderClient.me.username,
100-
"password" to wizardModel.loginModel.password!!,
101100
"session_token" to coderClient.sessionToken,
102101
"project_path" to tfProject.text
103102
)
104103
)
105104
}
105+
return true
106106
}
107+
return false
107108
}
108109

109110
override fun dispose() {

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesWizardStep.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ sealed interface CoderWorkspacesWizardStep {
1010
val previousActionText: String
1111

1212
fun onInit(wizardModel: CoderWorkspacesWizardModel)
13-
suspend fun onNext(wizardModel: CoderWorkspacesWizardModel)
13+
fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean
1414
}

src/main/resources/messages/CoderGatewayBundle.properties

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ gateway.connector.view.login.header.text=Coder Workspaces
55
gateway.connector.view.login.comment.text=Self-hosted developer workspaces in the cloud or on premise. Coder Workspaces empower developers with secure, consistent, and fast developer workspaces.
66
gateway.connector.view.login.documentation.action=Explore Coder Workspaces
77
gateway.connector.view.login.url.label=Url:
8-
gateway.connector.view.login.email.label=Email:
9-
gateway.connector.view.login.password.label=Password:
8+
gateway.connector.view.login.token.dialog=Paste your token here:
9+
gateway.connector.view.login.token.label=Session Token:
1010
gateway.connector.view.coder.auth.next.text=Connect
11-
gateway.connector.view.login.credentials.dialog.title=Coder Credentials
11+
gateway.connector.view.login.cli.downloader.dialog.title=Authenticate and setup coder
1212
gateway.connector.view.coder.workspaces.connect.text=Download and Start IDE
1313
gateway.connector.view.coder.workspaces.choose.text=Choose a workspace

0 commit comments

Comments
 (0)