Skip to content

Commit ad877d8

Browse files
authored
Add retries to InvalidGrantException (#3692)
* Add retries to InvalidGrantException The retries are added to all InvalidGrantException that makes sure that all the transient issues that are causing InvalidGrantException should be resolved by this retry. Using default retry stragegy with 2 retries(as the AWSSDK standard) that is 3 max attempts. With exponential backoff
1 parent 323a509 commit ad877d8

File tree

3 files changed

+92
-20
lines changed

3 files changed

+92
-20
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" : "CodeWhisperer: user is sometimes required to re-login before token expiration"
4+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sso/bearer/BearerTokenProvider.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ import com.intellij.util.containers.orNull
99
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
1010
import software.amazon.awssdk.auth.token.credentials.SdkToken
1111
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
12+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
13+
import software.amazon.awssdk.core.retry.conditions.OrRetryCondition
14+
import software.amazon.awssdk.core.retry.conditions.RetryOnExceptionsCondition
1215
import software.amazon.awssdk.regions.Region
1316
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
1417
import software.amazon.awssdk.services.ssooidc.SsoOidcTokenProvider
1518
import software.amazon.awssdk.services.ssooidc.internal.OnDiskTokenManager
19+
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
1620
import software.amazon.awssdk.utils.SdkAutoCloseable
1721
import software.amazon.awssdk.utils.cache.CachedSupplier
1822
import software.amazon.awssdk.utils.cache.NonBlocking
@@ -206,12 +210,31 @@ class ProfileSdkTokenProviderWrapper(private val sessionName: String, region: St
206210
internal val DEFAULT_STALE_DURATION = Duration.ofMinutes(15)
207211
internal val DEFAULT_PREFETCH_DURATION = Duration.ofMinutes(20)
208212

209-
private fun buildUnmanagedSsoOidcClient(region: String): SsoOidcClient =
213+
val ssoOidcClientConfigurationBuilder: (ClientOverrideConfiguration.Builder) -> ClientOverrideConfiguration.Builder = { configuration ->
214+
configuration.nullDefaultProfileFile()
215+
216+
// Get the existing RetryPolicy
217+
val existingRetryPolicy = configuration.retryPolicy()
218+
219+
// Add InvalidGrantException to the RetryOnExceptionsCondition
220+
val updatedRetryPolicy = existingRetryPolicy.toBuilder()
221+
.retryCondition(
222+
OrRetryCondition.create(
223+
existingRetryPolicy.retryCondition(),
224+
RetryOnExceptionsCondition.create(setOf(InvalidGrantException::class.java)),
225+
)
226+
).build()
227+
228+
// Update the RetryPolicy in the configuration
229+
configuration.retryPolicy(updatedRetryPolicy)
230+
}
231+
232+
fun buildUnmanagedSsoOidcClient(region: String): SsoOidcClient =
210233
AwsClientManager.getInstance()
211234
.createUnmanagedClient(
212235
AnonymousCredentialsProvider.create(),
213236
Region.of(region),
214237
clientCustomizer = ToolkitClientCustomizer { _, _, _, _, configuration ->
215-
configuration.nullDefaultProfileFile()
238+
ssoOidcClientConfigurationBuilder(configuration)
216239
}
217240
)

jetbrains-core/tst/software/aws/toolkits/jetbrains/core/credentials/sso/bearer/InteractiveBearerTokenProviderTest.kt

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package software.aws.toolkits.jetbrains.core.credentials.sso.bearer
55

66
import com.intellij.openapi.application.ApplicationManager
77
import com.intellij.testFramework.ApplicationRule
8+
import com.intellij.testFramework.DisposableRule
89
import com.intellij.testFramework.RuleChain
910
import org.assertj.core.api.Assertions.assertThat
11+
import org.junit.After
1012
import org.junit.Before
1113
import org.junit.Rule
1214
import org.junit.Test
@@ -15,18 +17,30 @@ import org.mockito.Mockito
1517
import org.mockito.kotlin.any
1618
import org.mockito.kotlin.argThat
1719
import org.mockito.kotlin.mock
20+
import org.mockito.kotlin.spy
1821
import org.mockito.kotlin.times
1922
import org.mockito.kotlin.verify
2023
import org.mockito.kotlin.verifyNoMoreInteractions
2124
import org.mockito.kotlin.whenever
25+
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
26+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
2227
import software.amazon.awssdk.core.exception.SdkException
28+
import software.amazon.awssdk.core.interceptor.Context
29+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes
30+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor
31+
import software.amazon.awssdk.core.internal.InternalCoreExecutionAttribute
32+
import software.amazon.awssdk.regions.Region
2333
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
2434
import software.amazon.awssdk.services.ssooidc.model.AccessDeniedException
2535
import software.amazon.awssdk.services.ssooidc.model.CreateTokenRequest
2636
import software.amazon.awssdk.services.ssooidc.model.CreateTokenResponse
37+
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
2738
import software.aws.toolkits.core.region.aRegionId
2839
import software.aws.toolkits.core.utils.test.aString
40+
import software.aws.toolkits.jetbrains.core.AwsClientManager
41+
import software.aws.toolkits.jetbrains.core.MockClientManager
2942
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
43+
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
3044
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessToken
3145
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessTokenCacheKey
3246
import software.aws.toolkits.jetbrains.core.credentials.sso.ClientRegistration
@@ -46,6 +60,10 @@ class InteractiveBearerTokenProviderTest {
4660
mockClientManager
4761
)
4862

63+
@Rule
64+
@JvmField
65+
val disposableRule = DisposableRule()
66+
4967
private lateinit var oidcClient: SsoOidcClient
5068
private val diskCache = mock<DiskCache>()
5169
private val startUrl = aString()
@@ -57,29 +75,56 @@ class InteractiveBearerTokenProviderTest {
5775
oidcClient = mockClientManager.create<SsoOidcClient>()
5876
}
5977

60-
@Test
61-
fun `reads last token from disk on initialziation`() {
62-
buildSut()
63-
verify(diskCache).loadAccessToken(
64-
argThat<AccessTokenCacheKey> {
65-
val (_, url, scopes) = this
66-
url == startUrl && scopes == this.scopes
67-
}
68-
)
78+
@After
79+
fun tearDown() {
80+
oidcClient.close()
6981
}
7082

7183
@Test
72-
fun `resolveToken refreshes from service if local token expired`() {
73-
stubClientRegistration()
74-
stubAccessToken()
75-
val sut = buildSut()
76-
sut.resolveToken()
84+
fun `oidcClient retries twice on InvalidGrantException failure`() {
85+
fun verifyRetryAttempts(configuration: ClientOverrideConfiguration.Builder) {
86+
configuration.addExecutionInterceptor(
87+
object : ExecutionInterceptor {
88+
override fun onExecutionFailure(context: Context.FailedExecution?, executionAttributes: ExecutionAttributes?) {
89+
super.onExecutionFailure(context, executionAttributes)
90+
91+
// 3 total network calls, showing 4 since the sdk increments the attempt count at the beginning
92+
// of the loop before it checks whether it's allowed to retry.
93+
assertThat(executionAttributes?.getAttribute(InternalCoreExecutionAttribute.EXECUTION_ATTEMPT)).isEqualTo(4)
94+
}
95+
}
96+
)
97+
}
98+
fun buildUnmanagedSsoOidcClientForTests(region: String): SsoOidcClient =
99+
AwsClientManager.getInstance()
100+
.createUnmanagedClient(
101+
AnonymousCredentialsProvider.create(),
102+
Region.of(region),
103+
clientCustomizer = { _, _, _, _, configuration ->
104+
verifyRetryAttempts(ssoOidcClientConfigurationBuilder(configuration))
105+
}
106+
)
77107

78-
verify(oidcClient).createToken(
79-
argThat<CreateTokenRequest> {
80-
grantType() == "refresh_token"
108+
MockClientManager.useRealImplementations(disposableRule.disposable)
109+
oidcClient = spy(buildUnmanagedSsoOidcClientForTests("us-east-1"))
110+
val registerClientResponse = oidcClient.registerClient {
111+
it.clientType("public")
112+
it.scopes(scopes)
113+
it.clientName("test")
114+
}
115+
val deviceAuthorizationResponse = oidcClient.startDeviceAuthorization {
116+
it.clientId(registerClientResponse.clientId())
117+
it.clientSecret(registerClientResponse.clientSecret())
118+
it.startUrl(SONO_URL)
119+
}
120+
assertThrows<InvalidGrantException> {
121+
oidcClient.createToken {
122+
it.clientId(registerClientResponse.clientId())
123+
it.clientSecret(registerClientResponse.clientSecret())
124+
it.deviceCode(deviceAuthorizationResponse.deviceCode() + "invalid")
125+
it.grantType("urn:ietf:params:oauth:grant-type:device_code")
81126
}
82-
)
127+
}
83128
}
84129

85130
@Test

0 commit comments

Comments
 (0)