Skip to content

Commit

Permalink
Add 'Authenticate in Browser' in sign-in dialog (#1888)
Browse files Browse the repository at this point in the history
Resolves
https://linear.app/sourcegraph/issue/CODY-2308/enterprise-users-unable-to-use-browser-redirects-for-authentication-on.

## Test plan
1. Go to Settings
2. `Log In with Token to Sourcegraph Enterprise`
3. Try different server inputs: `sourcegraph.com`,
`sourcegraph.sourcegraph.com`, invalid server url, empty server url
4. `Authenticate automatically`

## Demo


~https://github.com/sourcegraph/jetbrains/assets/19799111/0f64b145-96ae-4952-a168-5f811e0a0ec1~



https://github.com/user-attachments/assets/a2f0e950-5b4d-4c32-acdb-8b5971c171da


## Final look

<img width="536" alt="image"
src="https://github.com/user-attachments/assets/f88d77a0-b17a-48a8-87a6-5ed1ff14e8fc">
  • Loading branch information
mkondratek authored Jul 31, 2024
1 parent 9bd1f55 commit 0c4a398
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 107 deletions.
26 changes: 13 additions & 13 deletions src/main/kotlin/com/sourcegraph/cody/auth/SourcegraphAuthService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.util.Url
import com.intellij.util.Urls
import com.sourcegraph.config.ConfigUtil
import java.util.concurrent.CompletableFuture
import org.jetbrains.ide.BuiltInServerManager

Expand All @@ -14,39 +13,40 @@ internal class SourcegraphAuthService : AuthServiceBase() {
override val name: String
get() = SERVICE_NAME

fun authorize(authMethod: SsoAuthMethod): CompletableFuture<String> {
return authorize(SourcegraphAuthRequest(name, authMethod))
fun authorize(server: String, authMethod: SsoAuthMethod): CompletableFuture<String> {
return authorize(SourcegraphAuthRequest(name, server, authMethod))
}

private class SourcegraphAuthRequest(
override val serviceName: String,
authMethod: SsoAuthMethod
val server: String,
val authMethod: SsoAuthMethod
) : AuthRequest {
private val port: Int
get() = BuiltInServerManager.getInstance().port

override val authUrlWithParameters: Url = createUrl(authMethod)
override val authUrlWithParameters: Url = createUrl()

private fun createUrl(authMethod: SsoAuthMethod) =
private fun createUrl() =
when (authMethod) {
SsoAuthMethod.GITHUB -> {
val end =
".auth/openidconnect/login?prompt_auth=github&pc=sams&redirect=/user/settings/tokens/new/callback?requestFrom=JETBRAINS-$port"
Urls.newFromEncoded(ConfigUtil.DOTCOM_URL + end)
Urls.newFromEncoded(server + end)
}
SsoAuthMethod.GITLAB -> {
val end =
".auth/openidconnect/login?prompt_auth=gitlab&pc=sams&redirect=/user/settings/tokens/new/callback?requestFrom=JETBRAINS-$port"
Urls.newFromEncoded(ConfigUtil.DOTCOM_URL + end)
Urls.newFromEncoded(server + end)
}
SsoAuthMethod.GOOGLE -> {
val end =
".auth/openidconnect/login?prompt_auth=google&pc=sams&redirect=/user/settings/tokens/new/callback?requestFrom=JETBRAINS-$port"
Urls.newFromEncoded(ConfigUtil.DOTCOM_URL + end)
Urls.newFromEncoded(server + end)
}
else ->
SERVICE_URL.addParameters(
mapOf("requestFrom" to "JETBRAINS", "port" to port.toString()))
serviceUrl(server)
.addParameters(mapOf("requestFrom" to "JETBRAINS", "port" to port.toString()))
}
}

Expand All @@ -58,7 +58,7 @@ internal class SourcegraphAuthService : AuthServiceBase() {
get() = service()

@JvmStatic
val SERVICE_URL: Url =
Urls.newFromEncoded(ConfigUtil.DOTCOM_URL + "user/settings/tokens/new/callback")
fun serviceUrl(server: String): Url =
Urls.newFromEncoded(server + "user/settings/tokens/new/callback")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import com.intellij.openapi.wm.ToolWindowManager
import com.sourcegraph.cody.CodyToolWindowFactory
import com.sourcegraph.cody.config.CodyAuthenticationManager
import com.sourcegraph.cody.config.CodyPersistentAccountsHost
import com.sourcegraph.cody.config.signInWithSourcegraphDialog
import com.sourcegraph.cody.config.SourcegraphInstanceLoginDialog
import com.sourcegraph.common.ui.DumbAwareEDTAction
import com.sourcegraph.config.ConfigUtil
import java.awt.Dimension

class SignInWithEnterpriseInstanceAction(
private val defaultServer: String = ConfigUtil.DOTCOM_URL
Expand All @@ -20,14 +19,15 @@ class SignInWithEnterpriseInstanceAction(
val authManager = CodyAuthenticationManager.getInstance(project)
val serverUrl = authManager.account?.server?.url ?: defaultServer
val dialog =
signInWithSourcegraphDialog(project, e.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT))
.apply {
contentPane.minimumSize = Dimension(MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT)
setServer(serverUrl)
}
SourcegraphInstanceLoginDialog(
project, e.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT), serverUrl)
if (dialog.showAndGet()) {
accountsHost.addAccount(
dialog.server, dialog.login, dialog.displayName, dialog.token, dialog.id)
dialog.codyAuthData.server,
dialog.codyAuthData.login,
dialog.codyAuthData.account.displayName,
dialog.codyAuthData.token,
dialog.codyAuthData.account.id)
if (ConfigUtil.isCodyEnabled()) {
// Open Cody sidebar
val toolWindowManager = ToolWindowManager.getInstance(project)
Expand Down
33 changes: 26 additions & 7 deletions src/main/kotlin/com/sourcegraph/cody/config/BaseLoginDialog.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.sourcegraph.cody.config

import com.intellij.collaboration.async.CompletableFutureUtil
import com.intellij.collaboration.async.CompletableFutureUtil.completionOnEdt
import com.intellij.collaboration.async.CompletableFutureUtil.errorOnEdt
import com.intellij.collaboration.async.CompletableFutureUtil.successOnEdt
import com.intellij.openapi.application.ModalityState
Expand All @@ -13,17 +12,20 @@ import com.intellij.openapi.util.Disposer
import com.sourcegraph.cody.api.SourcegraphApiRequestExecutor
import com.sourcegraph.cody.auth.SsoAuthMethod
import java.awt.Component
import javax.swing.Action
import javax.swing.JComponent

abstract class BaseLoginDialog(
project: Project?,
parent: Component?,
protected val project: Project?,
protected val parent: Component?,
executorFactory: SourcegraphApiRequestExecutor.Factory,
private val authMethod: SsoAuthMethod
) : DialogWrapper(project, parent, false, IdeModalityType.PROJECT) {

protected val loginPanel = CodyLoginPanel(executorFactory)

override fun createActions(): Array<Action> = arrayOf(cancelAction, okAction)

var id: String = ""
private set

Expand Down Expand Up @@ -65,10 +67,8 @@ abstract class BaseLoginDialog(
val emptyProgressIndicator = EmptyProgressIndicator(modalityState)
Disposer.register(disposable) { emptyProgressIndicator.cancel() }

startGettingToken()
loginPanel
.acquireDetailsAndToken(emptyProgressIndicator, authMethod)
.completionOnEdt(modalityState) { finishGettingToken() }
.successOnEdt(modalityState) { (details, newToken) ->
login = details.username
displayName = details.displayName
Expand All @@ -82,7 +82,26 @@ abstract class BaseLoginDialog(
}
}

protected open fun startGettingToken() = Unit
fun authenticate() {
val modalityState = ModalityState.stateForComponent(loginPanel)
val emptyProgressIndicator = EmptyProgressIndicator(modalityState)
Disposer.register(disposable) { emptyProgressIndicator.cancel() }

loginPanel.setAuthUI()
okAction.isEnabled = false

loginPanel
.acquireDetailsAndToken(emptyProgressIndicator, SsoAuthMethod.DEFAULT)
.successOnEdt(modalityState) { (details, newToken) ->
login = details.username
displayName = details.displayName
token = newToken
id = details.id

protected open fun finishGettingToken() = Unit
close(OK_EXIT_CODE, true)
}
.errorOnEdt(modalityState) {
if (!CompletableFutureUtil.isCancellation(it)) startTrackingValidation()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ class CodyAccountListModel(private val project: Project) :
.login(
parentComponent,
CodyLoginRequest(
login = account.name,
title = "Edit Sourcegraph Account",
server = account.server,
login = account.name,
token = token,
customRequestHeaders = account.server.customRequestHeaders,
title = "Edit Sourcegraph Account",
loginButtonText = "Save account",
))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import com.intellij.util.ui.UIUtil.getInactiveTextColor
import com.sourcegraph.cody.api.SourcegraphApiRequestExecutor
import com.sourcegraph.cody.auth.SourcegraphAuthService
import com.sourcegraph.cody.auth.SsoAuthMethod
import com.sourcegraph.common.CodyBundle
import javax.swing.JComponent

class CodyAuthCredentialsUi(val factory: SourcegraphApiRequestExecutor.Factory) :
CodyCredentialsUi() {

override fun getPreferredFocusableComponent(): JComponent? = null

override fun getValidator(): Validator = { null }
override fun getValidationInfo(): ValidationInfo? = null

override fun createExecutor(server: SourcegraphServerPath): SourcegraphApiRequestExecutor =
factory.create(server, "")
Expand All @@ -28,7 +29,7 @@ class CodyAuthCredentialsUi(val factory: SourcegraphApiRequestExecutor.Factory)
indicator: ProgressIndicator,
authMethod: SsoAuthMethod
): Pair<CodyAccountDetails, String> {
val token = acquireToken(indicator, authMethod)
val token = acquireToken(indicator, executor.server.url, authMethod)
// The token has changed, so create a new executor to talk to the same server with the new
// token.
val newExecutor = factory.create(executor.server, token)
Expand All @@ -43,17 +44,20 @@ class CodyAuthCredentialsUi(val factory: SourcegraphApiRequestExecutor.Factory)

override fun Panel.centerPanel() {
row {
val progressLabel =
JBLabel("Logging in, check your browser").apply {
cell(
JBLabel(CodyBundle.getString("login.dialog.check-browser")).apply {
icon = AnimatedIcon.Default.INSTANCE
foreground = getInactiveTextColor()
}
cell(progressLabel)
})
}
}

private fun acquireToken(indicator: ProgressIndicator, authMethod: SsoAuthMethod): String {
val credentialsFuture = SourcegraphAuthService.instance.authorize(authMethod)
private fun acquireToken(
indicator: ProgressIndicator,
server: String,
authMethod: SsoAuthMethod
): String {
val credentialsFuture = SourcegraphAuthService.instance.authorize(server, authMethod)
try {
return ProgressIndicatorUtils.awaitWithCheckCanceled(credentialsFuture, indicator)
} catch (pce: ProcessCanceledException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import javax.swing.JPanel
abstract class CodyCredentialsUi {
abstract fun getPreferredFocusableComponent(): JComponent?

abstract fun getValidator(): Validator
abstract fun getValidationInfo(): ValidationInfo?

abstract fun createExecutor(server: SourcegraphServerPath): SourcegraphApiRequestExecutor

Expand Down
12 changes: 8 additions & 4 deletions src/main/kotlin/com/sourcegraph/cody/config/CodyLoginPanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ import javax.swing.JTextField

internal typealias UniqueLoginPredicate = (login: String, server: SourcegraphServerPath) -> Boolean

class ServerTextField : ExtendableTextField(SourcegraphServerPath.DEFAULT_HOST, 0)

class CodyLoginPanel(
executorFactory: SourcegraphApiRequestExecutor.Factory,
) : Wrapper() {

private val serverTextField = ExtendableTextField(SourcegraphServerPath.DEFAULT_HOST, 0)
private val serverTextField = ServerTextField()
private var tokenAcquisitionError: ValidationInfo? = null

private lateinit var currentUi: CodyCredentialsUi
private var tokenUi = CodyTokenCredentialsUi(serverTextField, executorFactory)

private var authUI = CodyAuthCredentialsUi(executorFactory)
private var authUi = CodyAuthCredentialsUi(executorFactory)

private val progressIcon = AnimatedIcon.Default.INSTANCE
private val progressExtension = ExtendableTextComponent.Extension { progressIcon }
Expand Down Expand Up @@ -60,11 +62,13 @@ class CodyLoginPanel(
fun doValidateAll(): List<ValidationInfo> {
val uiError =
validateCustomRequestHeaders(tokenUi.customRequestHeadersField)
?: currentUi.getValidator().invoke()
?: currentUi.getValidationInfo()

return listOfNotNull(uiError, tokenAcquisitionError)
}

fun getServerPathValidationInfo() = tokenUi.getServerPathValidationInfo()

private fun validateCustomRequestHeaders(field: JTextField): ValidationInfo? {
if (field.text.isEmpty()) {
return null
Expand Down Expand Up @@ -136,5 +140,5 @@ class CodyLoginPanel(

fun setTokenUi() = applyUi(tokenUi)

fun setAuthUI() = applyUi(authUI)
fun setAuthUI() = applyUi(authUi)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ internal class CodyLoginRequest(
val title: String? = null,
val server: SourcegraphServerPath? = null,
val login: String? = null,
val isCheckLoginUnique: Boolean = false,
val token: String? = null,
val customRequestHeaders: String? = null,
val loginButtonText: String? = null
Expand All @@ -19,10 +18,6 @@ internal fun CodyLoginRequest.loginWithToken(
project: Project,
parentComponent: Component?
): CodyAuthData? {
val isLoginUniqueChecker: UniqueLoginPredicate = { login, server ->
!isCheckLoginUnique ||
CodyAuthenticationManager.getInstance(project).isAccountUnique(login, server)
}
val dialog = SourcegraphTokenLoginDialog(project, parentComponent, SsoAuthMethod.DEFAULT)
configure(dialog)

Expand Down
Loading

0 comments on commit 0c4a398

Please sign in to comment.