Skip to content

Commit b009333

Browse files
adietishcursoragent
andcommitted
fix: show sequential TLS trust dialogs during OpenShift connect
Parent trust prompts to the wizard, use invokeLater instead of invokeAndWait, and split TLS setup from authentication so API and OAuth certificates can both be accepted. Add TLS trust logging and surface OAuth discovery failures. Signed-off-by: Andre Dietisheim <adietish@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5638cdf commit b009333

12 files changed

Lines changed: 368 additions & 71 deletions

File tree

src/main/kotlin/com/redhat/devtools/gateway/auth/code/OpenShiftAuthCodeFlow.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.nimbusds.oauth2.sdk.id.State
1818
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod
1919
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier
2020
import com.nimbusds.openid.connect.sdk.Nonce
21+
import com.intellij.openapi.diagnostic.logger
2122
import com.redhat.devtools.gateway.util.toServerBaseUrl
2223
import kotlinx.coroutines.future.await
2324
import kotlinx.serialization.SerialName
@@ -44,6 +45,8 @@ class OpenShiftAuthCodeFlow(
4445
private val sslContext: SSLContext
4546
) : AuthCodeFlow {
4647

48+
private val logger = logger<OpenShiftAuthCodeFlow>()
49+
4750
private lateinit var codeVerifier: CodeVerifier
4851
private lateinit var state: State
4952

@@ -75,24 +78,37 @@ class OpenShiftAuthCodeFlow(
7578
)
7679

7780
companion object {
81+
private val logger = logger<OpenShiftAuthCodeFlow>()
7882
private val json = Json { ignoreUnknownKeys = true }
7983

8084
/** OAuth HTTP endpoint base URLs discovered from the API server. */
8185
suspend fun discoverOAuthEndpointBaseUrls(
8286
apiServerUrl: String,
8387
sslContext: SSLContext,
8488
): List<String> {
89+
val discoveryUrl = "$apiServerUrl/.well-known/oauth-authorization-server"
90+
logger.info("TLS trust: discovering OAuth endpoints from $discoveryUrl")
8591
val client = HttpClient.newBuilder()
8692
.sslContext(sslContext)
8793
.version(HttpClient.Version.HTTP_1_1)
8894
.followRedirects(HttpClient.Redirect.NORMAL)
8995
.build()
9096

91-
val response = sendGetRequest(client, "$apiServerUrl/.well-known/oauth-authorization-server", "OAuth discovery failed")
97+
val response = try {
98+
sendGetRequest(client, discoveryUrl, "OAuth discovery failed")
99+
} catch (e: Exception) {
100+
logger.error("TLS trust: OAuth discovery request to $discoveryUrl failed", e)
101+
throw e
102+
}
92103
val metadata = json.decodeFromString(OAuthMetadata.serializer(), response.body())
93-
return listOf(metadata.tokenEndpoint, metadata.authorizationEndpoint)
104+
val urls = listOf(metadata.tokenEndpoint, metadata.authorizationEndpoint)
94105
.map { URI(it).toServerBaseUrl() }
95106
.distinct()
107+
logger.info(
108+
"TLS trust: OAuth discovery succeeded (issuer=${metadata.issuer}, " +
109+
"endpoints=${urls.joinToString()})"
110+
)
111+
return urls
96112
}
97113

98114
private suspend fun sendGetRequest(httpClient: HttpClient, url: String, errorPrefix: String = "Request failed"): HttpResponse<String> {
@@ -211,7 +227,12 @@ class OpenShiftAuthCodeFlow(
211227
*if (clientIdInForm) arrayOf("client_id" to clientId) else emptyArray()
212228
)
213229

214-
val token = sendPostRequest(client, metadata.tokenEndpoint, authHeader, form, errorPrefix = "Token request failed")
230+
val token = try {
231+
sendPostRequest(client, metadata.tokenEndpoint, authHeader, form, errorPrefix = "Token request failed")
232+
} catch (e: Exception) {
233+
logger.error("TLS trust: token request to ${metadata.tokenEndpoint} failed", e)
234+
throw e
235+
}
215236
val expiresAt = if (token.expiresIn > 0) System.currentTimeMillis() + token.expiresIn * 1000 else null
216237
return SSOToken(accessToken = token.accessToken, idToken = "", accountLabel = accountLabel, expiresAt = expiresAt)
217238
}

src/main/kotlin/com/redhat/devtools/gateway/auth/tls/DefaultTlsTrustManager.kt

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212
package com.redhat.devtools.gateway.auth.tls
1313

14+
import com.intellij.openapi.diagnostic.thisLogger
1415
import com.redhat.devtools.gateway.auth.code.OpenShiftAuthCodeFlow
1516
import com.redhat.devtools.gateway.kubeconfig.KubeConfigNamedCluster
1617
import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils
@@ -29,13 +30,20 @@ class DefaultTlsTrustManager(
2930
private val persistentKeyStore: PersistentKeyStore
3031
) : TlsTrustManager {
3132

33+
private val logger = thisLogger()
34+
3235
override suspend fun ensureTrusted(
3336
serverUrl: String,
3437
decisionHandler: suspend (TlsServerCertificateInfo) -> TlsTrustDecision,
3538
certificateAuthority: CertificateSource?,
39+
endpointKind: TlsEndpointKind,
3640
): TlsContext {
3741

3842
val serverUri = URI(serverUrl)
43+
logger.info(
44+
"TLS trust: probing ${endpointKind.label} at $serverUrl " +
45+
"(wizard CA=${certificateAuthority != null}, kind=$endpointKind)"
46+
)
3947

4048
val namedCluster =
4149
KubeConfigUtils.getClusterByServer(
@@ -44,22 +52,34 @@ class DefaultTlsTrustManager(
4452
)
4553

4654
if (namedCluster?.cluster?.insecureSkipTlsVerify == true) {
55+
logger.warn("TLS trust: using insecure skip for $serverUrl (kubeconfig insecure-skip-tls-verify)")
4756
return SslContextFactory.insecure()
4857
}
4958

5059
val trustedCerts = getTrustedCerts(namedCluster, serverUri.host, certificateAuthority) +
5160
sessionTrustStore.get(serverUrl)
5261

5362
if (trustedCerts.isNotEmpty()) {
63+
logger.debug(
64+
"TLS trust: trying ${trustedCerts.size} known certificate(s) for $serverUrl " +
65+
"(session=${sessionTrustStore.get(serverUrl).size}, " +
66+
"preconfigured=${trustedCerts.size - sessionTrustStore.get(serverUrl).size})"
67+
)
5468
try {
5569
val tlsContext = SslContextFactory.fromTrustedCerts(trustedCerts)
5670
withContext(Dispatchers.IO) {
5771
TlsProbe.connect(serverUri, tlsContext.sslContext)
5872
}
73+
logger.info("TLS trust: existing trust accepted for $serverUrl")
5974
return tlsContext
6075
} catch (e: SSLHandshakeException) {
61-
// Certificate changed or invalid → continue to capture
76+
logger.warn(
77+
"TLS trust: handshake failed with known certificate(s) for $serverUrl; " +
78+
"will prompt (${e.message})"
79+
)
6280
}
81+
} else {
82+
logger.info("TLS trust: no known certificate for $serverUrl; will capture server certificate")
6383
}
6484

6585
val captureContext = SslContextFactory.captureOnly()
@@ -68,6 +88,7 @@ class DefaultTlsTrustManager(
6888
withContext(Dispatchers.IO) {
6989
TlsProbe.connect(serverUri, captureContext.sslContext)
7090
}
91+
logger.warn("TLS trust: probe unexpectedly succeeded without trust for $serverUrl")
7192
return captureContext // should not normally succeed
7293
} catch (e: SSLHandshakeException) {
7394
val chain = (captureContext.trustManager as? CapturingTrustManager)
@@ -87,14 +108,26 @@ class DefaultTlsTrustManager(
87108
serverUrl = serverUrl,
88109
certificateChain = chain,
89110
fingerprintSha256 = sha256Fingerprint(trustAnchor),
90-
problem = problem
111+
problem = problem,
112+
endpointKind = endpointKind,
113+
)
114+
115+
logger.info(
116+
"TLS trust: prompting user for ${endpointKind.label} at $serverUrl " +
117+
"(problem=$problem, fingerprint=${info.fingerprintSha256})"
91118
)
92119

93120
val decision = decisionHandler(info)
94121
if (!decision.trusted) {
122+
logger.info("TLS trust: user rejected certificate for $serverUrl")
95123
throw TlsTrustRejectedException()
96124
}
97125

126+
logger.info(
127+
"TLS trust: user accepted certificate for $serverUrl " +
128+
"(scope=${decision.scope}, endpoint=${endpointKind.label})"
129+
)
130+
98131
val keyStore = persistentKeyStore.loadOrCreate()
99132
val persistentAlias = hostAlias(serverUri.host)
100133

@@ -123,7 +156,12 @@ class DefaultTlsTrustManager(
123156
val finalCerts = (trustedCerts + trustAnchor)
124157
.distinctBy { it.serialNumber }
125158

126-
return SslContextFactory.fromTrustedCerts(finalCerts)
159+
val tlsContext = SslContextFactory.fromTrustedCerts(finalCerts)
160+
withContext(Dispatchers.IO) {
161+
TlsProbe.connect(serverUri, tlsContext.sslContext)
162+
}
163+
logger.info("TLS trust: verified connection to $serverUrl after user acceptance")
164+
return tlsContext
127165
}
128166
}
129167

@@ -136,25 +174,57 @@ class DefaultTlsTrustManager(
136174
certificateAuthority: CertificateSource? = null,
137175
): TlsContext {
138176
val apiBaseUrl = URI(apiServerUrl).toServerBaseUrl()
177+
logger.info("TLS trust: establishing OpenShift TLS context for API $apiBaseUrl")
139178

140-
ensureTrusted(apiBaseUrl, decisionHandler, certificateAuthority)
179+
ensureTrusted(
180+
apiBaseUrl,
181+
decisionHandler,
182+
certificateAuthority,
183+
TlsEndpointKind.API_SERVER,
184+
)
141185

142186
val apiTls = mergedContextFor(listOf(apiBaseUrl), certificateAuthority)
143-
val oauthUrls = runCatching {
187+
val oauthUrls = try {
144188
OpenShiftAuthCodeFlow.discoverOAuthEndpointBaseUrls(
145189
apiBaseUrl,
146190
apiTls.sslContext,
147191
)
148-
}.getOrDefault(emptyList())
192+
} catch (e: Exception) {
193+
logger.error(
194+
"TLS trust: failed to discover OAuth endpoints from $apiBaseUrl. " +
195+
"Login may fail if the OAuth host uses a different certificate.",
196+
e
197+
)
198+
throw e
199+
}
200+
201+
if (oauthUrls.isEmpty()) {
202+
logger.warn(
203+
"TLS trust: OAuth discovery returned no endpoints for $apiBaseUrl. " +
204+
"Only the API server certificate will be trusted."
205+
)
206+
} else {
207+
logger.info("TLS trust: discovered OAuth endpoint host(s): ${oauthUrls.joinToString()}")
208+
}
149209

150210
val allUrls = (listOf(apiBaseUrl) + oauthUrls).distinct()
151211
for (url in allUrls) {
152212
if (url != apiBaseUrl) {
153-
ensureTrusted(url, decisionHandler, certificateAuthority)
213+
ensureTrusted(
214+
url,
215+
decisionHandler,
216+
certificateAuthority,
217+
TlsEndpointKind.OAUTH,
218+
)
154219
}
155220
}
156221

157-
return mergedContextFor(allUrls, certificateAuthority)
222+
val merged = mergedContextFor(allUrls, certificateAuthority)
223+
logger.info(
224+
"TLS trust: OpenShift TLS context ready for ${allUrls.size} endpoint(s): " +
225+
allUrls.joinToString()
226+
)
227+
return merged
158228
}
159229

160230
suspend fun mergedContextFor(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2026 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
package com.redhat.devtools.gateway.auth.tls
13+
14+
/** Identifies which cluster endpoint triggered a TLS trust prompt or handshake. */
15+
enum class TlsEndpointKind(val label: String) {
16+
UNKNOWN("server"),
17+
API_SERVER("OpenShift API server"),
18+
OAUTH("OpenShift OAuth endpoint"),
19+
}

src/main/kotlin/com/redhat/devtools/gateway/auth/tls/TlsServerCertificateInfo.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ data class TlsServerCertificateInfo(
1717
val serverUrl: String,
1818
val certificateChain: List<X509Certificate>,
1919
val fingerprintSha256: String,
20-
val problem: TlsTrustProblem
20+
val problem: TlsTrustProblem,
21+
val endpointKind: TlsEndpointKind = TlsEndpointKind.UNKNOWN,
2122
)

src/main/kotlin/com/redhat/devtools/gateway/auth/tls/TlsTrustManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ interface TlsTrustManager {
2323
serverUrl: String,
2424
decisionHandler: suspend (TlsServerCertificateInfo) -> TlsTrustDecision,
2525
certificateAuthority: CertificateSource? = null,
26+
endpointKind: TlsEndpointKind = TlsEndpointKind.UNKNOWN,
2627
): TlsContext
2728
}

src/main/kotlin/com/redhat/devtools/gateway/auth/tls/ui/TLSTrustDecisionHandler.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import com.intellij.ui.dsl.builder.BottomGap
2222
import com.intellij.ui.dsl.builder.TopGap
2323
import com.intellij.ui.dsl.builder.panel
2424
import com.intellij.util.ui.JBUI
25+
import com.redhat.devtools.gateway.auth.tls.TlsEndpointKind
2526
import java.awt.BorderLayout
27+
import java.awt.Component
2628
import java.awt.Dimension
2729
import java.awt.FlowLayout
2830
import java.awt.event.ActionEvent
@@ -38,9 +40,11 @@ import javax.swing.JPanel
3840
* @param certificateInfo PEM/text representation of the certificate.
3941
*/
4042
class TLSTrustDecisionHandler(
43+
parent: Component,
4144
private val serverUrl: String,
45+
private val endpointKind: TlsEndpointKind,
4246
private val certificateInfo: String
43-
) : DialogWrapper(true) {
47+
) : DialogWrapper(parent, true) {
4448

4549
companion object {
4650
val PREFERRED_SIZE = Dimension(500, 400)
@@ -55,7 +59,7 @@ class TLSTrustDecisionHandler(
5559
private set
5660

5761
init {
58-
title = "Untrusted TLS Certificate"
62+
title = "Untrusted TLS Certificate${endpointKind.label}"
5963
init()
6064
}
6165

@@ -75,7 +79,7 @@ class TLSTrustDecisionHandler(
7579
isOpaque = false
7680
add(JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
7781
isOpaque = false
78-
add(JBLabel("The server at "))
82+
add(JBLabel("The ${endpointKind.label} at "))
7983
add(HyperlinkLabel(serverUrl).apply { setHyperlinkTarget(serverUrl) })
8084
add(JBTextArea("presents a TLS certificate that is not trusted."))
8185
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2026 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
package com.redhat.devtools.gateway.auth.tls.ui
13+
14+
import java.awt.Component
15+
16+
/**
17+
* UI anchor for TLS trust dialogs shown during the connection wizard.
18+
*
19+
* Trust prompts must be parented to the wizard so they stack above the progress
20+
* dialog and can be shown repeatedly (API host, then OAuth host).
21+
*/
22+
object TlsDecisionUiContext {
23+
var parentComponent: Component? = null
24+
}

0 commit comments

Comments
 (0)