Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

impl: support for OAuth2 [WIP]#209

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
fioan89 wants to merge13 commits intomain
base:main
Choose a base branch
Loading
fromimpl-support-for-oauth
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
13 commits
Select commitHold shift + click to select a range
8bfee5e
build: simplify install folder resolution
fioan89Oct 9, 2025
1a3415b
impl: setup auth manager with auth and token endpoints
fioan89Oct 9, 2025
7685feb
impl: retrieve supported response type and the dynamic client registr…
fioan89Oct 13, 2025
52648a0
impl: models for dynamic client registration
fioan89Oct 13, 2025
72a902f
impl: pixy secure code generator
fioan89Oct 13, 2025
0e03b03
impl: retrofit API for endpoint discovery and dynamic client registra…
fioan89Oct 13, 2025
79ba4cb
impl: factory method for the auth manager
fioan89Oct 13, 2025
59d2abd
impl: improve auth manager config
fioan89Oct 13, 2025
decb082
refactor: simplify OAuth manager architecture and improve dependency …
fioan89Oct 14, 2025
d432a76
fix: inject mocked PluginAuthManager into UTs
fioan89Oct 14, 2025
2a28cee
impl: handle the redirect URI
fioan89Oct 14, 2025
6462f14
fix: wrong client app registration endpoint
fioan89Oct 16, 2025
0e46da0
impl: simple way of triggering the OAuth flow.
fioan89Oct 16, 2025
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
12 changes: 2 additions & 10 deletionsbuild.gradle.kts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,21 +202,13 @@ tasks.register("cleanAll", Delete::class.java) {

private fun getPluginInstallDir(): Path {
val userHome = System.getProperty("user.home").let { Path.of(it) }
valtoolboxCachesDir = when {
valpluginsDir = when {
SystemInfoRt.isWindows -> System.getenv("LOCALAPPDATA")?.let { Path.of(it) } ?: (userHome / "AppData" / "Local")
// currently this is the location that TBA uses on Linux
SystemInfoRt.isLinux -> System.getenv("XDG_DATA_HOME")?.let { Path.of(it) } ?: (userHome / ".local" / "share")
SystemInfoRt.isMac -> userHome / "Library" / "Caches"
else -> error("Unknown os")
} / "JetBrains" / "Toolbox"

val pluginsDir = when {
SystemInfoRt.isWindows ||
SystemInfoRt.isLinux ||
SystemInfoRt.isMac -> toolboxCachesDir

else -> error("Unknown os")
} / "plugins"
} / "JetBrains" / "Toolbox" / "plugins"

return pluginsDir / extension.id
}
Expand Down
6 changes: 6 additions & 0 deletionssrc/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,12 @@ class CoderRemoteProvider(
*/
override suspend fun handleUri(uri: URI) {
try {

if (context.oauthManager.canHandle(uri)) {
context.oauthManager.handle(uri)
return
}

linkHandler.handle(
uri,
shouldDoAutoSetup()
Expand Down
4 changes: 4 additions & 0 deletionssrc/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
package com.coder.toolbox

import com.coder.toolbox.oauth.CoderAccount
import com.coder.toolbox.oauth.CoderOAuthCfg
import com.coder.toolbox.store.CoderSecretsStore
import com.coder.toolbox.store.CoderSettingsStore
import com.coder.toolbox.util.toURL
import com.jetbrains.toolbox.api.core.auth.PluginAuthManager
import com.jetbrains.toolbox.api.core.diagnostics.Logger
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
Expand All@@ -18,6 +21,7 @@ import java.util.UUID

@Suppress("UnstableApiUsage")
data class CoderToolboxContext(
val oauthManager: PluginAuthManager<CoderAccount, CoderOAuthCfg>,
val ui: ToolboxUi,
val envPageManager: EnvironmentUiPageManager,
val envStateColorPalette: EnvironmentStateColorPalette,
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
package com.coder.toolbox

import com.coder.toolbox.oauth.CoderAccount
import com.coder.toolbox.oauth.CoderOAuthManager
import com.coder.toolbox.settings.Environment
import com.coder.toolbox.store.CoderSecretsStore
import com.coder.toolbox.store.CoderSettingsStore
Expand DownExpand Up@@ -29,6 +31,11 @@ class CoderToolboxExtension : RemoteDevExtension {
val logger = serviceLocator.getService(Logger::class.java)
return CoderRemoteProvider(
CoderToolboxContext(
serviceLocator.getAuthManager(
CoderAccount::class.java,
"Coder OAuth2 Manager",
CoderOAuthManager()
),
serviceLocator.getService<ToolboxUi>(),
serviceLocator.getService<EnvironmentUiPageManager>(),
serviceLocator.getService<EnvironmentStateColorPalette>(),
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
package com.coder.toolbox.oauth

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class AuthorizationServer(
@field:Json(name = "authorization_endpoint") val authorizationEndpoint: String,
@field:Json(name = "token_endpoint") val tokenEndpoint: String,
@field:Json(name = "registration_endpoint") val registrationEndpoint: String,
@property:Json(name = "response_types_supported") val supportedResponseTypes: List<String>,
@property:Json(name = "token_endpoint_auth_methods_supported") val authMethodForTokenEndpoint: List<TokenEndpointAuthMethod>,
)

enum class TokenEndpointAuthMethod {
@Json(name = "none")
NONE,

@Json(name = "client_secret_post")
CLIENT_SECRET_POST,

@Json(name = "client_secret_basic")
CLIENT_SECRET_BASIC,
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package com.coder.toolbox.oauth

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ClientRegistrationRequest(
@field:Json(name = "client_name") val clientName: String,
@field:Json(name = "redirect_uris") val redirectUris: List<String>,
@field:Json(name = "grant_types") val grantTypes: List<String>,
@field:Json(name = "response_types") val responseTypes: List<String>,
@field:Json(name = "scope") val scope: String,
@field:Json(name = "token_endpoint_auth_method") val tokenEndpointAuthMethod: String? = null
)
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
package com.coder.toolbox.oauth

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* DCR response
*/
@JsonClass(generateAdapter = true)
data class ClientRegistrationResponse(
@field:Json(name = "client_id") val clientId: String,
@field:Json(name = "client_secret") val clientSecret: String,
@field:Json(name = "client_name") val clientName: String,
@field:Json(name = "redirect_uris") val redirectUris: List<String>,
@field:Json(name = "grant_types") val grantTypes: List<String>,
@field:Json(name = "response_types") val responseTypes: List<String>,
@field:Json(name = "scope") val scope: String,
@field:Json(name = "token_endpoint_auth_method") val tokenEndpointAuthMethod: String,
@field:Json(name = "client_id_issued_at") val clientIdIssuedAt: Long?,
@field:Json(name = "client_secret_expires_at") val clientSecretExpiresAt: Long?,
@field:Json(name = "registration_client_uri") val registrationClientUri: String,
@field:Json(name = "registration_access_token") val registrationAccessToken: String
)
5 changes: 5 additions & 0 deletionssrc/main/kotlin/com/coder/toolbox/oauth/CoderAccount.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
package com.coder.toolbox.oauth

import com.jetbrains.toolbox.api.core.auth.Account

data class CoderAccount(override val id: String, override val fullName: String) : Account
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
package com.coder.toolbox.oauth

import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

interface CoderAuthorizationApi {
@GET(".well-known/oauth-authorization-server")
suspend fun discoveryMetadata(): Response<AuthorizationServer>

@POST("oauth2/register")
suspend fun registerClient(
@Body request: ClientRegistrationRequest
): Response<ClientRegistrationResponse>
}
92 changes: 92 additions & 0 deletionssrc/main/kotlin/com/coder/toolbox/oauth/CoderOAuthManager.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
package com.coder.toolbox.oauth

import com.jetbrains.toolbox.api.core.auth.AuthConfiguration
import com.jetbrains.toolbox.api.core.auth.ContentType
import com.jetbrains.toolbox.api.core.auth.ContentType.FORM_URL_ENCODED
import com.jetbrains.toolbox.api.core.auth.OAuthToken
import com.jetbrains.toolbox.api.core.auth.PluginAuthInterface
import com.jetbrains.toolbox.api.core.auth.RefreshConfiguration

class CoderOAuthManager : PluginAuthInterface<CoderAccount, CoderOAuthCfg> {
private lateinit var refreshConf: CoderRefreshConfig

override fun serialize(account: CoderAccount): String = "${account.id}|${account.fullName}"

override fun deserialize(string: String): CoderAccount = CoderAccount(
string.split('|')[0],
string.split('|')[1]
)

override suspend fun createAccount(
token: OAuthToken,
config: AuthConfiguration
): CoderAccount {
TODO("Not yet implemented")
}

override suspend fun updateAccount(
token: OAuthToken,
account: CoderAccount
): CoderAccount {
TODO("Not yet implemented")
}

override fun createAuthConfig(loginConfiguration: CoderOAuthCfg): AuthConfiguration {
val codeVerifier = PKCEGenerator.generateCodeVerifier()
val codeChallenge = PKCEGenerator.generateCodeChallenge(codeVerifier)
refreshConf = loginConfiguration.toRefreshConf()

return AuthConfiguration(
authParams = mapOf(
"client_id" to loginConfiguration.clientId,
"response_type" to "code",
"code_challenge" to codeChallenge
),
tokenParams = mapOf(
"grant_type" to "authorization_code",
"client_id" to loginConfiguration.clientId,
"code_verifier" to codeVerifier
),
baseUrl = loginConfiguration.baseUrl,
authUrl = loginConfiguration.authUrl,
tokenUrl = loginConfiguration.tokenUrl,
codeChallengeParamName = "code_challenge",
codeChallengeMethod = "S256",
verifierParamName = "code_verifier",
authorization = null
)
}

override fun createRefreshConfig(account: CoderAccount): RefreshConfiguration {
return object : RefreshConfiguration {
override val refreshUrl: String = refreshConf.refreshUrl
override val parameters: Map<String, String> = mapOf(
"grant_type" to "refresh_token",
"client_id" to refreshConf.clientId,
"client_secret" to refreshConf.clientSecret
)
override val authorization: String? = null
override val contentType: ContentType = FORM_URL_ENCODED
}
}
}

data class CoderOAuthCfg(
val baseUrl: String,
val authUrl: String,
val tokenUrl: String,
val clientId: String,
val clientSecret: String,
)

private data class CoderRefreshConfig(
val refreshUrl: String,
val clientId: String,
val clientSecret: String,
)

private fun CoderOAuthCfg.toRefreshConf() = CoderRefreshConfig(
refreshUrl = this.tokenUrl,
clientId = this.clientId,
this.clientSecret
)
42 changes: 42 additions & 0 deletionssrc/main/kotlin/com/coder/toolbox/oauth/PKCEGenerator.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package com.coder.toolbox.oauth

import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64

private const val CODE_VERIFIER_LENGTH = 128

/**
* Generates OAuth2 PKCE code verifier and code challenge
*/
object PKCEGenerator {

/**
* Generates a cryptographically random code verifier 128 chars in size
* @return Base64 URL-encoded code verifier
*/
fun generateCodeVerifier(): String {
val secureRandom = SecureRandom()
val bytes = ByteArray(CODE_VERIFIER_LENGTH)
secureRandom.nextBytes(bytes)

return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(bytes)
.take(CODE_VERIFIER_LENGTH)
}

/**
* Generates code challenge from code verifier using S256 method
* @param codeVerifier The code verifier string
* @return Base64 URL-encoded SHA-256 hash of the code verifier
*/
fun generateCodeChallenge(codeVerifier: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(codeVerifier.toByteArray(Charsets.US_ASCII))

return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(hash)
}
}
9 changes: 9 additions & 0 deletionssrc/main/kotlin/com/coder/toolbox/util/URLExtensions.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,12 @@ import java.net.URL

fun String.toURL(): URL = URI.create(this).toURL()

fun String.toBaseURL(): URL {
val url = this.toURL()
val port = if (url.port != -1) ":${url.port}" else ""
return URI.create("${url.protocol}://${url.host}$port").toURL()
}

fun String.validateStrictWebUrl(): WebUrlValidationResult = try {
val uri = URI(this)

Expand All@@ -21,15 +27,18 @@ fun String.validateStrictWebUrl(): WebUrlValidationResult = try {
"The URL \"$this\" is missing a scheme (like https://). " +
"Please enter a full web address like \"https://example.com\""
)

uri.scheme?.lowercase() !in setOf("http", "https") ->
Invalid(
"The URL \"$this\" must start with http:// or https://, not \"${uri.scheme}\""
)

uri.authority.isNullOrBlank() ->
Invalid(
"The URL \"$this\" does not include a valid website name. " +
"Please enter a full web address like \"https://example.com\""
)

else -> Valid
}
} catch (_: Exception) {
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp