Skip to content

Commit 5f5ed91

Browse files
authored
fix(amazonq): add missing auth checks / invalidate token if server claims token is invalid (#5876)
we have several existing checks to guard against invocation, but do not handle the case where token is no longer valid
1 parent f9f83b2 commit 5f5ed91

File tree

1 file changed

+67
-24
lines changed
  • plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup

1 file changed

+67
-24
lines changed

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/QInlineCompletionProvider.kt

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,18 @@ import kotlinx.coroutines.channels.Channel
4242
import kotlinx.coroutines.flow.receiveAsFlow
4343
import kotlinx.coroutines.future.await
4444
import kotlinx.coroutines.launch
45+
import kotlinx.coroutines.withContext
4546
import migration.software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
47+
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
4648
import org.eclipse.lsp4j.jsonrpc.messages.Either
49+
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
4750
import software.aws.toolkits.core.utils.debug
4851
import software.aws.toolkits.core.utils.getLogger
52+
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
53+
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
54+
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
55+
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
56+
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
4957
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
5058
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
5159
import software.aws.toolkits.jetbrains.services.codewhisperer.importadder.CodeWhispererImportAdder
@@ -59,8 +67,10 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispe
5967
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
6068
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
6169
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
70+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil
6271
import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics
6372
import software.aws.toolkits.jetbrains.utils.isQConnected
73+
import software.aws.toolkits.jetbrains.utils.isQExpired
6474
import software.aws.toolkits.resources.message
6575
import software.aws.toolkits.telemetry.CodewhispererTriggerType
6676
import java.awt.Dimension
@@ -393,8 +403,22 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
393403

394404
override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion {
395405
val editor = request.editor
396-
val document = editor.document
397406
val project = editor.project ?: return InlineCompletionSuggestion.Empty
407+
408+
// try to refresh automatically if possible, otherwise ask user to login again
409+
if (isQExpired(project)) {
410+
// consider changing to only running once a ~minute since this is relatively expensive
411+
// say the connection is un-refreshable if refresh fails for 3 times
412+
val shouldReauth = withContext(getCoroutineBgContext()) {
413+
CodeWhispererUtil.promptReAuth(project)
414+
}
415+
416+
if (shouldReauth) {
417+
return InlineCompletionSuggestion.Empty
418+
}
419+
}
420+
421+
val document = editor.document
398422
val handler = InlineCompletion.getHandlerOrNull(editor) ?: return InlineCompletionSuggestion.Empty
399423
val session = InlineCompletionSession.getOrNull(editor) ?: return InlineCompletionSuggestion.Empty
400424
val triggerSessionId = triggerSessionId++
@@ -446,31 +470,28 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
446470
}
447471

448472
try {
449-
// Launch coroutine for background pagination progress
450-
cs.launch {
451-
var nextToken: Either<String, Int>? = null
452-
do {
453-
nextToken = startPaginationInBackground(
454-
project,
455-
editor,
456-
triggerTypeInfo,
457-
triggerSessionId,
458-
nextToken,
459-
sessionContext,
460-
)
461-
} while (nextToken != null && !nextToken.left.isNullOrEmpty())
473+
var nextToken: Either<String, Int>? = null
474+
do {
475+
nextToken = startPaginationInBackground(
476+
project,
477+
editor,
478+
triggerTypeInfo,
479+
triggerSessionId,
480+
nextToken,
481+
sessionContext,
482+
)
483+
} while (nextToken != null && !nextToken.left.isNullOrEmpty())
462484

463-
// closing all channels since pagination for this session has finished
485+
// closing all channels since pagination for this session has finished
486+
logInline(triggerSessionId) {
487+
"Pagination finished, closing all channels"
488+
}
489+
sessionContext.itemContexts.forEach {
490+
it.channel.close()
491+
}
492+
if (session.context.isDisposed) {
464493
logInline(triggerSessionId) {
465-
"Pagination finished, closing all channels"
466-
}
467-
sessionContext.itemContexts.forEach {
468-
it.channel.close()
469-
}
470-
if (session.context.isDisposed) {
471-
logInline(triggerSessionId) {
472-
"Current display session already disposed by a new trigger before pagination finishes, exiting"
473-
}
494+
"Current display session already disposed by a new trigger before pagination finishes, exiting"
474495
}
475496
}
476497

@@ -575,6 +596,27 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
575596
logInline(triggerSessionId, e) {
576597
"Error during pagination"
577598
}
599+
if (e is ResponseErrorException) {
600+
// convoluted but lines up with "The bearer token included in the request is invalid"
601+
// https://github.com/aws/language-servers/blob/1f3e93024eeb22186a34f0bd560f8d552f517300/server/aws-lsp-codewhisperer/src/language-server/chat/utils.ts#L22-L23
602+
// error data is nullable
603+
if (e.responseError.data?.toString()?.contains("E_AMAZON_Q_CONNECTION_EXPIRED") == true) {
604+
// kill the session if the connection is expired
605+
val connection = ToolkitConnectionManager
606+
.getInstance(project)
607+
.activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
608+
val tokenProvider = connection?.let { it.getConnectionSettings().tokenProvider.delegate as? BearerTokenProvider }
609+
tokenProvider?.let {
610+
// TODO: fragile
611+
try {
612+
it.refresh()
613+
} catch (_: InvalidGrantException) {
614+
it.invalidate()
615+
CodeWhispererUtil.reconnectCodeWhisperer(project)
616+
}
617+
}
618+
}
619+
}
578620
return null
579621
}
580622
}
@@ -594,6 +636,7 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
594636
val editor = request.editor
595637
val project = editor.project ?: return false
596638

639+
// qExpired case handled in completion handler
597640
if (!isQConnected(project)) return false
598641
if (QRegionProfileManager.getInstance().hasValidConnectionButNoActiveProfile(project)) return false
599642
if (event.isManualCall()) return true

0 commit comments

Comments
 (0)