Skip to content

Commit f675c2b

Browse files
authored
Fix case where user gets stuck while logging into Builder ID (#4923)
If user ends up in state where they have a connection, but extension doesn't think it's valid for Q, then the sign-in flow will never complete and they need to restart the IDE for the new token to be picked up
1 parent 94a5c5d commit f675c2b

File tree

4 files changed

+73
-79
lines changed

4 files changed

+73
-79
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Fix issue where a user may get stuck while attempting to login to Builder ID"
4+
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenPr
1919
// TODO: unify with AwsConnectionManager
2020
@State(name = "connectionManager", storages = [Storage("aws.xml")])
2121
class DefaultToolkitConnectionManager : ToolkitConnectionManager, PersistentStateComponent<ToolkitConnectionManagerState> {
22+
private val project: Project?
23+
24+
constructor(project: Project) {
25+
this.project = project
26+
}
27+
28+
constructor() {
29+
this.project = null
30+
}
31+
2232
init {
2333
ApplicationManager.getApplication().messageBus.connect(this).subscribe(
2434
BearerTokenProviderListener.TOPIC,
@@ -33,19 +43,10 @@ class DefaultToolkitConnectionManager : ToolkitConnectionManager, PersistentStat
3343
)
3444
}
3545

36-
private val project: Project?
37-
38-
constructor(project: Project) {
39-
this.project = project
40-
}
41-
42-
constructor() {
43-
this.project = null
44-
}
45-
4646
private var connection: ToolkitConnection? = null
4747

48-
private val pinningManager: ConnectionPinningManager = ConnectionPinningManager.getInstance()
48+
private val pinningManager
49+
get() = ConnectionPinningManager.getInstance()
4950

5051
private val defaultConnection: ToolkitConnection?
5152
get() {
@@ -60,6 +61,10 @@ class DefaultToolkitConnectionManager : ToolkitConnectionManager, PersistentStat
6061
return null
6162
}
6263

64+
@Deprecated(
65+
"Fragile API. Probably leads to unexpected behavior. Use only for toolkit explorer dropdown state.",
66+
replaceWith = ReplaceWith("activeConnectionForFeature(feature)")
67+
)
6368
@Synchronized
6469
override fun activeConnection() = connection ?: defaultConnection
6570

@@ -71,11 +76,11 @@ class DefaultToolkitConnectionManager : ToolkitConnectionManager, PersistentStat
7176
}
7277

7378
return connection?.let {
74-
if (feature.supportsConnectionType(it)) {
75-
return it
79+
return@let if (feature.supportsConnectionType(it)) {
80+
it
81+
} else {
82+
null
7683
}
77-
78-
null
7984
} ?: defaultConnection?.let {
8085
if (ApplicationInfo.getInstance().build.productCode == "GW") return null
8186
if (feature.supportsConnectionType(it)) {

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAuthManager.kt

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ interface ToolkitStartupAuthFactory {
8989
}
9090

9191
interface ToolkitConnectionManager : Disposable {
92+
@Deprecated(
93+
"Fragile API. Probably leads to unexpected behavior. Use only for toolkit explorer dropdown state.",
94+
ReplaceWith("activeConnectionForFeature(feature)")
95+
)
9296
fun activeConnection(): ToolkitConnection?
9397

9498
fun activeConnectionForFeature(feature: FeatureWithPinnedConnection): ToolkitConnection?
@@ -129,6 +133,7 @@ fun loginSso(
129133
project = project,
130134
connection = transientConnection,
131135
onPendingToken = onPendingToken,
136+
isReAuth = false,
132137
source = metadata?.sourceId,
133138
)
134139
}
@@ -149,7 +154,7 @@ fun loginSso(
149154

150155
val manager = ToolkitAuthManager.getInstance()
151156
val allScopes = requestedScopes.toMutableSet()
152-
return manager.getConnection(connectionId)?.let { connection ->
157+
val connection = manager.getConnection(connectionId)?.let { connection ->
153158
val logger = getLogger<ToolkitAuthManager>()
154159

155160
if (connection !is AwsBearerTokenConnection) {
@@ -167,32 +172,32 @@ fun loginSso(
167172
""".trimIndent()
168173
}
169174
// can't reuse since requested scopes are not in current connection. forcing reauth
170-
return createAndAuthNewConnection(
171-
ManagedSsoProfile(
172-
region,
173-
startUrl,
174-
allScopes.toList()
175-
)
176-
)
175+
return@let null
177176
}
178177

179178
// For the case when the existing connection is in invalid state, we need to re-auth
180179
reauthConnectionIfNeeded(
181180
project = project,
182181
connection = connection,
183-
isReAuth = true
182+
onPendingToken = onPendingToken,
183+
isReAuth = true,
184+
source = metadata?.sourceId,
184185
)
186+
return@let connection
187+
}
188+
189+
if (connection != null) {
185190
return connection
186-
} ?: run {
187-
// No existing connection, start from scratch
188-
createAndAuthNewConnection(
189-
ManagedSsoProfile(
190-
region,
191-
startUrl,
192-
allScopes.toList()
193-
)
194-
)
195191
}
192+
193+
// No existing connection, start from scratch
194+
return createAndAuthNewConnection(
195+
ManagedSsoProfile(
196+
region,
197+
startUrl,
198+
allScopes.toList()
199+
)
200+
)
196201
}
197202

198203
@Suppress("UnusedParameter")
@@ -242,7 +247,9 @@ fun reauthConnectionIfNeeded(
242247
}
243248

244249
val startUrl = (connection as AwsBearerTokenConnection).startUrl
250+
var didReauth = false
245251
maybeReauthProviderIfNeeded(project, tokenProvider) {
252+
didReauth = true
246253
runUnderProgressIfNeeded(project, AwsCoreBundle.message("credentials.pending.title"), true) {
247254
try {
248255
tokenProvider.reauthenticate()
@@ -283,6 +290,11 @@ fun reauthConnectionIfNeeded(
283290
}
284291
}
285292
}
293+
294+
if (!didReauth) {
295+
// webview is stuck if reauth was not needed (i.e. token on disk is valid)
296+
project?.let { ToolkitConnectionManager.getInstance(it).switchConnection(connection) }
297+
}
286298
return tokenProvider
287299
}
288300

plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitAuthManagerTest.kt

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import org.junit.jupiter.api.extension.RegisterExtension
1818
import org.mockito.Mockito.mockConstruction
1919
import org.mockito.kotlin.any
2020
import org.mockito.kotlin.argumentCaptor
21+
import org.mockito.kotlin.atLeastOnce
2122
import org.mockito.kotlin.doNothing
2223
import org.mockito.kotlin.mock
2324
import org.mockito.kotlin.spy
2425
import org.mockito.kotlin.timeout
2526
import org.mockito.kotlin.verify
2627
import org.mockito.kotlin.verifyNoMoreInteractions
2728
import org.mockito.kotlin.whenever
28-
import software.amazon.awssdk.regions.Region
2929
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
3030
import software.aws.toolkits.core.telemetry.MetricEvent
3131
import software.aws.toolkits.core.telemetry.TelemetryBatcher
@@ -36,8 +36,6 @@ import software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileSsoSessi
3636
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
3737
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
3838
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.InteractiveBearerTokenProvider
39-
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderExtension
40-
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
4139
import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher
4240
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
4341
import software.aws.toolkits.jetbrains.settings.AwsSettings
@@ -51,9 +49,6 @@ class DefaultToolkitAuthManagerTest {
5149
batcher: TelemetryBatcher
5250
) : TelemetryService(publisher, batcher)
5351

54-
@ExtendWith(MockRegionProviderExtension::class)
55-
val regionProvider = MockRegionProviderRule()
56-
5752
@JvmField
5853
@RegisterExtension
5954
val mockClientManager = MockClientManagerExtension()
@@ -67,9 +62,13 @@ class DefaultToolkitAuthManagerTest {
6762
@BeforeEach
6863
fun setUp(@TestDisposable disposable: Disposable) {
6964
mockClientManager.create<SsoOidcClient>()
65+
7066
sut = DefaultToolkitAuthManager()
7167
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
72-
connectionManager = DefaultToolkitConnectionManager()
68+
69+
connectionManager = DefaultToolkitConnectionManager(projectRule.project)
70+
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
71+
7372
batcher = mock()
7473
telemetryService = spy(TestTelemetryService(batcher = batcher))
7574
ApplicationManager.getApplication().replaceService(TelemetryService::class.java, telemetryService, disposable)
@@ -222,12 +221,7 @@ class DefaultToolkitAuthManagerTest {
222221
}
223222

224223
@Test
225-
fun `loginSso with an working existing connection`(@TestDisposable disposable: Disposable) {
226-
val connectionManager: ToolkitConnectionManager = mock()
227-
regionProvider.addRegion(Region.US_EAST_1)
228-
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
229-
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
230-
224+
fun `loginSso with an working existing connection`() {
231225
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
232226
whenever(context.state()).thenReturn(BearerTokenAuthState.AUTHORIZED)
233227
}.use {
@@ -248,12 +242,7 @@ class DefaultToolkitAuthManagerTest {
248242
}
249243

250244
@Test
251-
fun `loginSso with an existing connection but expired and refresh token is valid, should refreshToken`(@TestDisposable disposable: Disposable) {
252-
val connectionManager = ToolkitConnectionManager.getInstance(projectRule.project)
253-
regionProvider.addRegion(Region.US_EAST_1)
254-
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
255-
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
256-
245+
fun `loginSso with an existing connection but expired and refresh token is valid, should refreshToken`() {
257246
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
258247
whenever(context.id).thenReturn("id")
259248
whenever(context.state()).thenReturn(BearerTokenAuthState.NEEDS_REFRESH)
@@ -276,13 +265,7 @@ class DefaultToolkitAuthManagerTest {
276265
}
277266

278267
@Test
279-
fun `loginSso with an existing connection that token is invalid and there's no refresh token, should re-authenticate`(
280-
@TestDisposable disposable: Disposable
281-
) {
282-
val connectionManager = ToolkitConnectionManager.getInstance(projectRule.project)
283-
regionProvider.addRegion(Region.US_EAST_1)
284-
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
285-
268+
fun `loginSso with an existing connection that token is invalid and there's no refresh token, should re-authenticate`() {
286269
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
287270
whenever(context.state()).thenReturn(BearerTokenAuthState.NOT_AUTHENTICATED)
288271
}.use {
@@ -305,13 +288,12 @@ class DefaultToolkitAuthManagerTest {
305288

306289
@Test
307290
fun `loginSso reuses connection if requested scopes are subset of existing`(@TestDisposable disposable: Disposable) {
308-
val connectionManager = ToolkitConnectionManager.getInstance(projectRule.project)
309-
regionProvider.addRegion(Region.US_EAST_1)
310-
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
311-
312291
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
313292
whenever(context.state()).thenReturn(BearerTokenAuthState.AUTHORIZED)
314293
}.use {
294+
val connectionManager = spy(connectionManager)
295+
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
296+
315297
val existingConnection = sut.createConnection(
316298
ManagedSsoProfile(
317299
"us-east-1",
@@ -328,16 +310,12 @@ class DefaultToolkitAuthManagerTest {
328310
verify(tokenProvider).state()
329311
verifyNoMoreInteractions(tokenProvider)
330312
assertThat(connectionManager.activeConnection()).isEqualTo(existingConnection)
313+
verify(connectionManager, atLeastOnce()).switchConnection(existingConnection)
331314
}
332315
}
333316

334317
@Test
335-
fun `loginSso forces reauth if requested scopes are not complete subset`(@TestDisposable disposable: Disposable) {
336-
regionProvider.addRegion(Region.US_EAST_1)
337-
338-
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
339-
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
340-
318+
fun `loginSso forces reauth if requested scopes are not complete subset`() {
341319
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
342320
whenever(context.state()).thenReturn(BearerTokenAuthState.AUTHORIZED)
343321
}.use {
@@ -364,14 +342,12 @@ class DefaultToolkitAuthManagerTest {
364342

365343
@Test
366344
fun `loginSso with a new connection`(@TestDisposable disposable: Disposable) {
367-
val connectionManager: ToolkitConnectionManager = mock()
368-
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
369-
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
370-
371345
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
372346
doNothing().whenever(context).reauthenticate()
373347
whenever(context.state()).thenReturn(BearerTokenAuthState.NOT_AUTHENTICATED)
374348
}.use {
349+
val connectionManager = spy(connectionManager)
350+
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
375351
// before
376352
assertThat(sut.listConnections()).hasSize(0)
377353

@@ -399,17 +375,14 @@ class DefaultToolkitAuthManagerTest {
399375

400376
@Test
401377
fun `logoutFromConnection should invalidate the token provider and the connection and invoke callback`(@TestDisposable disposable: Disposable) {
402-
regionProvider.addRegion(Region.US_EAST_1)
403-
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
404-
405378
val profile = ManagedSsoProfile("us-east-1", "startUrl000", listOf("scopes"))
406-
val connection = ToolkitAuthManager.getInstance().createConnection(profile) as ManagedBearerSsoConnection
379+
val connection = sut.createConnection(profile) as ManagedBearerSsoConnection
407380
connectionManager.switchConnection(connection)
408381

409382
var providerInvalidatedMessageReceived = 0
410383
var connectionSwitchedMessageReceived = 0
411384
var callbackInvoked = 0
412-
ApplicationManager.getApplication().messageBus.connect().subscribe(
385+
ApplicationManager.getApplication().messageBus.connect(disposable).subscribe(
413386
BearerTokenProviderListener.TOPIC,
414387
object : BearerTokenProviderListener {
415388
override fun invalidate(providerId: String) {
@@ -419,7 +392,7 @@ class DefaultToolkitAuthManagerTest {
419392
}
420393
}
421394
)
422-
ApplicationManager.getApplication().messageBus.connect().subscribe(
395+
ApplicationManager.getApplication().messageBus.connect(disposable).subscribe(
423396
ToolkitConnectionManagerListener.TOPIC,
424397
object : ToolkitConnectionManagerListener {
425398
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {

0 commit comments

Comments
 (0)