From ab170e6a2cfc5ade92bf8f27ac9815e5f26d457e Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 20 May 2020 22:38:34 -0700 Subject: [PATCH 01/43] Use MSAL token cache for confidential client credentials --- .../BearerTokenAuthenticationPolicy.java | 9 +- .../identity/AuthorizationCodeCredential.java | 2 +- .../identity/ClientCertificateCredential.java | 25 +- .../identity/ClientSecretCredential.java | 8 +- .../ClientSecretCredentialBuilder.java | 12 + .../azure/identity/DeviceCodeCredential.java | 2 +- .../azure/identity/IntelliJCredential.java | 2 +- .../InteractiveBrowserCredential.java | 2 +- .../identity/SharedTokenCacheCredential.java | 2 +- .../identity/UsernamePasswordCredential.java | 2 +- .../identity/VisualStudioCodeCredential.java | 2 +- .../implementation/IdentityClient.java | 255 ++++++++++-------- .../implementation/IdentityClientBuilder.java | 39 ++- .../implementation/IdentityClientOptions.java | 48 ++-- .../AuthorizationCodeCredentialTest.java | 2 +- .../ClientCertificateCredentialTest.java | 32 ++- .../identity/ClientSecretCredentialTest.java | 26 +- .../identity/DefaultAzureCredentialTest.java | 7 +- .../identity/DeviceCodeCredentialTest.java | 2 +- .../InteractiveBrowserCredentialTest.java | 2 +- .../UsernamePasswordCredentialTest.java | 6 +- .../IdentityClientIntegrationTests.java | 22 +- .../implementation/IdentityClientTests.java | 29 +- 23 files changed, 317 insertions(+), 221 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index f643b4975664..3ab432c07e3c 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -3,7 +3,6 @@ package com.azure.core.http.policy; -import com.azure.core.credential.SimpleTokenCache; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpPipelineCallContext; @@ -22,8 +21,7 @@ public class BearerTokenAuthenticationPolicy implements HttpPipelinePolicy { private static final String BEARER = "Bearer"; private final TokenCredential credential; - private final String[] scopes; - private final SimpleTokenCache cache; + private final TokenRequestContext tokenRequestContext; /** * Creates BearerTokenAuthenticationPolicy. @@ -36,8 +34,7 @@ public BearerTokenAuthenticationPolicy(TokenCredential credential, String... sco Objects.requireNonNull(scopes); assert scopes.length > 0; this.credential = credential; - this.scopes = scopes; - this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes))); + this.tokenRequestContext = new TokenRequestContext().addScopes(scopes); } @Override @@ -45,7 +42,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN if ("http".equals(context.getHttpRequest().getUrl().getProtocol())) { return Mono.error(new RuntimeException("token credentials require a URL using the HTTPS protocol scheme")); } - return cache.getToken() + return credential.getToken(tokenRequestContext) .flatMap(token -> { context.getHttpRequest().getHeaders().put(AUTHORIZATION_HEADER, BEARER + " " + token.getToken()); return next.process(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java index cd58056a485b..41be2cf7884e 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java @@ -52,7 +52,7 @@ public class AuthorizationCodeCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java index 44dfd883582c..4a6e6239ffd6 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java @@ -25,8 +25,6 @@ */ @Immutable public class ClientCertificateCredential implements TokenCredential { - private final String clientCertificate; - private final String clientCertificatePassword; private final IdentityClient identityClient; /** @@ -40,22 +38,19 @@ public class ClientCertificateCredential implements TokenCredential { ClientCertificateCredential(String tenantId, String clientId, String certificatePath, String certificatePassword, IdentityClientOptions identityClientOptions) { Objects.requireNonNull(certificatePath, "'certificatePath' cannot be null."); - this.clientCertificate = certificatePath; - this.clientCertificatePassword = certificatePassword; - identityClient = - new IdentityClientBuilder() - .tenantId(tenantId) - .clientId(clientId) - .identityClientOptions(identityClientOptions) - .build(); + identityClient = new IdentityClientBuilder() + .tenantId(tenantId) + .clientId(clientId) + .certificatePath(certificatePath) + .certificatePassword(certificatePassword) + .identityClientOptions(identityClientOptions) + .build(); } @Override public Mono getToken(TokenRequestContext request) { - if (clientCertificatePassword != null) { - return identityClient.authenticateWithPfxCertificate(clientCertificate, clientCertificatePassword, request); - } else { - return identityClient.authenticateWithPemCertificate(clientCertificate, request); - } + return identityClient.authenticateWithConfidentialClientCache(request) + .onErrorResume(t -> Mono.empty()) + .switchIfEmpty(identityClient.authenticateWithConfidentialClient(request)); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java index 28efcb6a0749..efb2783d9dc6 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java @@ -25,8 +25,6 @@ */ @Immutable public class ClientSecretCredential implements TokenCredential { - /* The client secret value. */ - private final String clientSecret; private final IdentityClient identityClient; /** @@ -44,13 +42,15 @@ public class ClientSecretCredential implements TokenCredential { identityClient = new IdentityClientBuilder() .tenantId(tenantId) .clientId(clientId) + .clientSecret(clientSecret) .identityClientOptions(identityClientOptions) .build(); - this.clientSecret = clientSecret; } @Override public Mono getToken(TokenRequestContext request) { - return identityClient.authenticateWithClientSecret(clientSecret, request); + return identityClient.authenticateWithConfidentialClientCache(request) + .onErrorResume(t -> Mono.empty()) + .switchIfEmpty(identityClient.authenticateWithConfidentialClient(request)); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredentialBuilder.java index d8061398d5e7..12b6a9915c63 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredentialBuilder.java @@ -25,6 +25,18 @@ public ClientSecretCredentialBuilder clientSecret(String clientSecret) { return this; } + /** + * Sets whether to enable using the shared token cache. This is disabled by default. + * + * @param enabled whether to enabled using the shared token cache. + * + * @return An updated instance of this builder with if the shared token cache enabled specified. + */ + public ClientSecretCredentialBuilder enablePersistentCache(boolean enabled) { + this.identityClientOptions.enablePersistentCache(enabled); + return this; + } + /** * Creates a new {@link ClientCertificateCredential} with the current configurations. * diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java index 4e2648ee77e6..81ac16498d5f 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java @@ -48,7 +48,7 @@ public class DeviceCodeCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java index ea868138423e..ca8d88e1f376 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java @@ -75,7 +75,7 @@ class IntelliJCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java index 3b66f2555fec..55f7fdf52dd6 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java @@ -54,7 +54,7 @@ public class InteractiveBrowserCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java index ecf37596b9e3..261bfb1cbdea 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java @@ -70,7 +70,7 @@ public class SharedTokenCacheCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java index 39f63b14913e..a0ed76fbb2c3 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java @@ -56,7 +56,7 @@ public class UsernamePasswordCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java index c1825bc19775..59e78fa713fb 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java @@ -67,7 +67,7 @@ class VisualStudioCodeCredential implements TokenCredential { public Mono getToken(TokenRequestContext request) { return Mono.defer(() -> { if (cachedToken.get() != null) { - return identityClient.authenticateWithMsalAccount(request, cachedToken.get().getAccount()) + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) .onErrorResume(t -> Mono.empty()); } else { return Mono.empty(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index a9f08fddf782..eef475adc85a 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -30,6 +30,7 @@ import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.DeviceCodeFlowParameters; import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IClientCredential; import com.microsoft.aad.msal4j.PublicClientApplication; import com.microsoft.aad.msal4j.RefreshTokenParameters; import com.microsoft.aad.msal4j.SilentParameters; @@ -57,6 +58,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.time.Duration; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -93,9 +99,13 @@ public class IdentityClient { private final ClientLogger logger = new ClientLogger(IdentityClient.class); private final IdentityClientOptions options; + private ConfidentialClientApplication confidentialClientApplication; private PublicClientApplication publicClientApplication; private final String tenantId; private final String clientId; + private final String clientSecret; + private final String certificatePath; + private final String certificatePassword; private HttpPipelineAdapter httpPipelineAdapter; /** @@ -103,9 +113,14 @@ public class IdentityClient { * * @param tenantId the tenant ID of the application. * @param clientId the client ID of the application. + * @param clientSecret the client secret of the application. + * @param certificatePath the path to the PKCS12 or PEM certificate of the application. + * @param certificatePassword the password protecting the PFX certificate. * @param options the options configuring the client. */ - IdentityClient(String tenantId, String clientId, IdentityClientOptions options) { + IdentityClient(String tenantId, String clientId, String clientSecret, + String certificatePath, String certificatePassword, + IdentityClientOptions options) { if (tenantId == null) { tenantId = "organizations"; } @@ -114,9 +129,91 @@ public class IdentityClient { } this.tenantId = tenantId; this.clientId = clientId; + this.clientSecret = clientSecret; + this.certificatePath = certificatePath; + this.certificatePassword = certificatePassword; this.options = options; } + private ConfidentialClientApplication getConfidentialClientApplication() { + if (confidentialClientApplication != null) { + return confidentialClientApplication; + } else if (clientId == null) { + throw logger.logExceptionAsError(new IllegalArgumentException( + "A non-null value for client ID must be provided for user authentication.")); + } else { + String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId; + IClientCredential credential; + if (clientSecret != null) { + credential = ClientCredentialFactory.createFromSecret(clientSecret); + } else if (certificatePath != null) { + try { + if (certificatePassword == null) { + byte[] pemCertificateBytes = Files.readAllBytes(Paths.get(certificatePath)); + credential = ClientCredentialFactory.createFromCertificate( + CertificateUtil.privateKeyFromPem(pemCertificateBytes), + CertificateUtil.publicKeyFromPem(pemCertificateBytes)); + } else { + credential = ClientCredentialFactory.createFromCertificate( + new FileInputStream(certificatePath), certificatePassword); + } + } catch (CertificateException + | UnrecoverableKeyException + | NoSuchAlgorithmException + | KeyStoreException + | NoSuchProviderException + | IOException e) { + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException("Must provide client secret or client certificate path"); + } + ConfidentialClientApplication.Builder applicationBuilder = + ConfidentialClientApplication.builder(clientId, credential); + try { + applicationBuilder = applicationBuilder.authority(authorityUrl); + } catch (MalformedURLException e) { + throw logger.logExceptionAsWarning(new IllegalStateException(e)); + } + + // If user supplies the pipeline, then it should override all other properties + // as they should directly be set on the pipeline. + HttpPipeline httpPipeline = options.getHttpPipeline(); + if (httpPipeline != null) { + httpPipelineAdapter = new HttpPipelineAdapter(httpPipeline); + applicationBuilder.httpClient(httpPipelineAdapter); + } else { + // If http client is set on the credential, then it should override the proxy options if any configured. + HttpClient httpClient = options.getHttpClient(); + if (httpClient != null) { + httpPipelineAdapter = new HttpPipelineAdapter(setupPipeline(httpClient)); + applicationBuilder.httpClient(httpPipelineAdapter); + } else if (options.getProxyOptions() != null) { + applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions())); + } else { + //Http Client is null, proxy options are not set, use the default client and build the pipeline. + httpPipelineAdapter = new HttpPipelineAdapter(setupPipeline(HttpClient.createDefault())); + applicationBuilder.httpClient(httpPipelineAdapter); + } + } + + if (options.getExecutorService() != null) { + applicationBuilder.executorService(options.getExecutorService()); + } + if (options.isSharedTokenCacheEnabled()) { + try { + applicationBuilder.setTokenCacheAccessAspect( + new PersistenceTokenCacheAccessAspect(options.getConfidentialClientPersistenceSettings())); + } catch (Throwable t) { + throw logger.logExceptionAsError(new ClientAuthenticationException( + "Shared token cache is unavailable in this environment.", null, t)); + } + } + this.confidentialClientApplication = applicationBuilder.build(); + return this.confidentialClientApplication; + } + } + private PublicClientApplication getPublicClientApplication(boolean sharedTokenCacheCredential) { if (publicClientApplication != null) { return publicClientApplication; @@ -159,7 +256,7 @@ private PublicClientApplication getPublicClientApplication(boolean sharedTokenCa if (options.isSharedTokenCacheEnabled()) { try { publicClientApplicationBuilder.setTokenCacheAccessAspect( - new PersistenceTokenCacheAccessAspect(options.getPersistenceSettings())); + new PersistenceTokenCacheAccessAspect(options.getPublicClientPersistenceSettings())); } catch (Throwable t) { String message = "Shared token cache is unavailable in this environment."; if (sharedTokenCacheCredential) { @@ -174,7 +271,6 @@ private PublicClientApplication getPublicClientApplication(boolean sharedTokenCa } } - public Mono authenticateWithIntelliJ(TokenRequestContext request) { try { IntelliJCacheAccessor cacheAccessor = new IntelliJCacheAccessor(options.getIntelliJKeePassDatabasePath()); @@ -337,36 +433,13 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { /** * Asynchronously acquire a token from Active Directory with a client secret. * - * @param clientSecret the client secret of the application * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithClientSecret(String clientSecret, TokenRequestContext request) { - String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId; - try { - ConfidentialClientApplication.Builder applicationBuilder = - ConfidentialClientApplication.builder(clientId, ClientCredentialFactory.createFromSecret(clientSecret)) - .authority(authorityUrl); - - // If http pipeline is available, then it should override the proxy options if any configured. - if (httpPipelineAdapter != null) { - applicationBuilder.httpClient(httpPipelineAdapter); - } else if (options.getProxyOptions() != null) { - applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions())); - } - - if (options.getExecutorService() != null) { - applicationBuilder.executorService(options.getExecutorService()); - } - - ConfidentialClientApplication application = applicationBuilder.build(); - return Mono.fromFuture(application.acquireToken( - ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) - .build())) - .map(ar -> new MsalToken(ar, options)); - } catch (MalformedURLException e) { - return Mono.error(e); - } + public Mono authenticateWithConfidentialClient(TokenRequestContext request) { + return Mono.fromFuture(() -> getConfidentialClientApplication().acquireToken( + ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build())) + .map(ar -> new MsalToken(ar, options)); } private HttpPipeline setupPipeline(HttpClient httpClient) { @@ -380,78 +453,6 @@ private HttpPipeline setupPipeline(HttpClient httpClient) { .policies(policies.toArray(new HttpPipelinePolicy[0])).build(); } - /** - * Asynchronously acquire a token from Active Directory with a PKCS12 certificate. - * - * @param pfxCertificatePath the path to the PKCS12 certificate of the application - * @param pfxCertificatePassword the password protecting the PFX certificate - * @param request the details of the token request - * @return a Publisher that emits an AccessToken - */ - public Mono authenticateWithPfxCertificate(String pfxCertificatePath, String pfxCertificatePassword, - TokenRequestContext request) { - String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId; - return Mono.fromCallable(() -> { - ConfidentialClientApplication.Builder applicationBuilder = - ConfidentialClientApplication.builder(clientId, ClientCredentialFactory.createFromCertificate( - new FileInputStream(pfxCertificatePath), pfxCertificatePassword)) - .authority(authorityUrl); - - // If http pipeline is available, then it should override the proxy options if any configured. - if (httpPipelineAdapter != null) { - applicationBuilder.httpClient(httpPipelineAdapter); - } else if (options.getProxyOptions() != null) { - applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions())); - } - - if (options.getExecutorService() != null) { - applicationBuilder.executorService(options.getExecutorService()); - } - - return applicationBuilder.build(); - }).flatMap(application -> Mono.fromFuture(application.acquireToken( - ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build()))) - .map(ar -> new MsalToken(ar, options)); - } - - /** - * Asynchronously acquire a token from Active Directory with a PEM certificate. - * - * @param pemCertificatePath the path to the PEM certificate of the application - * @param request the details of the token request - * @return a Publisher that emits an AccessToken - */ - public Mono authenticateWithPemCertificate(String pemCertificatePath, TokenRequestContext request) { - String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId; - try { - byte[] pemCertificateBytes = Files.readAllBytes(Paths.get(pemCertificatePath)); - ConfidentialClientApplication.Builder applicationBuilder = - ConfidentialClientApplication.builder(clientId, ClientCredentialFactory.createFromCertificate( - CertificateUtil.privateKeyFromPem(pemCertificateBytes), - CertificateUtil.publicKeyFromPem(pemCertificateBytes))) - .authority(authorityUrl); - - // If http pipeline is available, then it should override the proxy options if any configured. - if (httpPipelineAdapter != null) { - applicationBuilder.httpClient(httpPipelineAdapter); - } else if (options.getProxyOptions() != null) { - applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions())); - } - - if (options.getExecutorService() != null) { - applicationBuilder.executorService(options.getExecutorService()); - } - - ConfidentialClientApplication application = applicationBuilder.build(); - return Mono.fromFuture(application.acquireToken( - ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) - .build())) - .map(ar -> new MsalToken(ar, options)); - } catch (IOException e) { - return Mono.error(e); - } - } - /** * Asynchronously acquire a token from Active Directory with a username and a password. * @@ -476,7 +477,7 @@ public Mono authenticateWithUsernamePassword(TokenRequestContext requ * @param account the account used to login to acquire the last token * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithMsalAccount(TokenRequestContext request, IAccount account) { + public Mono authenticateWithPublicClientCache(TokenRequestContext request, IAccount account) { return Mono.defer(() -> Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())); @@ -490,19 +491,47 @@ public Mono authenticateWithMsalAccount(TokenRequestContext request, throw logger.logExceptionAsError(Exceptions.propagate(e)); } }).map(ar -> new MsalToken(ar, options)) - .filter(t -> !t.isExpired()) - .switchIfEmpty(Mono.fromFuture(() -> { - SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( - new HashSet<>(request.getScopes())).forceRefresh(true); - if (account != null) { - forceParametersBuilder = forceParametersBuilder.account(account); - } + .filter(t -> !t.isExpired()) + .switchIfEmpty(Mono.fromFuture(() -> { + SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( + new HashSet<>(request.getScopes())).forceRefresh(true); + if (account != null) { + forceParametersBuilder = forceParametersBuilder.account(account); + } + try { + return getPublicClientApplication(false).acquireTokenSilently(forceParametersBuilder.build()); + } catch (MalformedURLException e) { + throw logger.logExceptionAsError(Exceptions.propagate(e)); + } + }).map(result -> new MsalToken(result, options)))); + } + + /** + * Asynchronously acquire a token from the currently logged in client. + * + * @param request the details of the token request + * @return a Publisher that emits an AccessToken + */ + public Mono authenticateWithConfidentialClientCache(TokenRequestContext request) { + return Mono.defer(() -> Mono.fromFuture(() -> { + SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( + new HashSet<>(request.getScopes())); try { - return getPublicClientApplication(false).acquireTokenSilently(forceParametersBuilder.build()); + return getConfidentialClientApplication().acquireTokenSilently(parametersBuilder.build()); } catch (MalformedURLException e) { throw logger.logExceptionAsError(Exceptions.propagate(e)); } - }).map(result -> new MsalToken(result, options)))); + }).map(ar -> new MsalToken(ar, options)) + .filter(t -> !t.isExpired()) + .switchIfEmpty(Mono.fromFuture(() -> { + SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( + new HashSet<>(request.getScopes())).forceRefresh(true); + try { + return getConfidentialClientApplication().acquireTokenSilently(forceParametersBuilder.build()); + } catch (MalformedURLException e) { + throw logger.logExceptionAsError(Exceptions.propagate(e)); + } + }).map(result -> new MsalToken(result, options)))); } /** @@ -653,7 +682,7 @@ public Mono authenticateWithSharedTokenCache(TokenRequestContext requ } - return authenticateWithMsalAccount(request, requestedAccount); + return authenticateWithPublicClientCache(request, requestedAccount); }); } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBuilder.java index 9bc41c4a6801..a25acfdf8adf 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBuilder.java @@ -12,6 +12,9 @@ public final class IdentityClientBuilder { private IdentityClientOptions identityClientOptions; private String tenantId; private String clientId; + private String clientSecret; + private String certificatePath; + private String certificatePassword; /** * Sets the tenant ID for the client. @@ -33,6 +36,39 @@ public IdentityClientBuilder clientId(String clientId) { return this; } + /** + * Sets the client secret for the client. + * @param clientSecret the secret value of the AAD application. + * @return the IdentityClientBuilder itself + */ + public IdentityClientBuilder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + /** + * Sets the client certificate for the client. + * + * @param certificatePath the PEM/PFX file containing the certificate + * @return the IdentityClientBuilder itself + */ + public IdentityClientBuilder certificatePath(String certificatePath) { + this.certificatePath = certificatePath; + return this; + } + + /** + * Sets the client certificate for the client. + * + * @param certificatePassword the password protecting the PFX file + * @return the IdentityClientBuilder itself + */ + public IdentityClientBuilder certificatePassword(String certificatePassword) { + this.certificatePassword = certificatePassword; + return this; + } + + /** * Sets the options for the client. * @param identityClientOptions the options for the client. @@ -47,6 +83,7 @@ public IdentityClientBuilder identityClientOptions(IdentityClientOptions identit * @return a {@link IdentityClient} with the current configurations. */ public IdentityClient build() { - return new IdentityClient(tenantId, clientId, identityClientOptions); + return new IdentityClient(tenantId, clientId, clientSecret, certificatePath, + certificatePassword, identityClientOptions); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index 26e161fc3721..78c23ecddd4a 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java @@ -24,15 +24,18 @@ */ public final class IdentityClientOptions { private static final int MAX_RETRY_DEFAULT_LIMIT = 3; - private static final String DEFAULT_CACHE_FILE_NAME = "msal.cache"; + private static final String DEFAULT_PUBLIC_CACHE_FILE_NAME = "msal.cache"; + private static final String DEFAULT_CONFIDENTIAL_CACHE_FILE_NAME = "msal.confidential.cache"; private static final Path DEFAULT_CACHE_FILE_PATH = Platform.isWindows() ? Paths.get(System.getProperty("user.home"), "AppData", "Local", ".IdentityService") : Paths.get(System.getProperty("user.home"), ".IdentityService"); private static final String DEFAULT_KEYCHAIN_SERVICE = "Microsoft.Developer.IdentityService"; - private static final String DEFAULT_KEYCHAIN_ACCOUNT = "MSALCache"; + private static final String DEFAULT_PUBLIC_KEYCHAIN_ACCOUNT = "MSALCache"; + private static final String DEFAULT_CONFIDENTIAL_KEYCHAIN_ACCOUNT = "MSALConfidentialCache"; private static final String DEFAULT_KEYRING_NAME = "default"; private static final String DEFAULT_KEYRING_SCHEMA = "msal.cache"; - private static final String DEFAULT_KEYRING_ITEM_NAME = DEFAULT_KEYCHAIN_ACCOUNT; + private static final String DEFAULT_PUBLIC_KEYRING_ITEM_NAME = DEFAULT_PUBLIC_KEYCHAIN_ACCOUNT; + private static final String DEFAULT_CONFIDENTIAL_KEYRING_ITEM_NAME = DEFAULT_CONFIDENTIAL_KEYCHAIN_ACCOUNT; private static final String DEFAULT_KEYRING_ATTR_NAME = "MsalClientID"; private static final String DEFAULT_KEYRING_ATTR_VALUE = "Microsoft.Developer.IdentityService"; @@ -44,14 +47,6 @@ public final class IdentityClientOptions { private ExecutorService executorService; private Duration tokenRefreshOffset = Duration.ofMinutes(2); private HttpClient httpClient; - private Path cacheFileDirectory; - private String cacheFileName; - private String keychainService; - private String keychainAccount; - private String keyringName; - private String keyringItemSchema; - private String keyringItemName; - private final String[] attributes; // preserve order private boolean allowUnencryptedCache; private boolean sharedTokenCacheEnabled; private String keePassDatabasePath; @@ -64,14 +59,6 @@ public IdentityClientOptions() { authorityHost = configuration.get(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, KnownAuthorityHosts.AZURE_CLOUD); maxRetry = MAX_RETRY_DEFAULT_LIMIT; retryTimeout = i -> Duration.ofSeconds((long) Math.pow(2, i.getSeconds() - 1)); - cacheFileDirectory = DEFAULT_CACHE_FILE_PATH; - cacheFileName = DEFAULT_CACHE_FILE_NAME; - keychainService = DEFAULT_KEYCHAIN_SERVICE; - keychainAccount = DEFAULT_KEYCHAIN_ACCOUNT; - keyringName = DEFAULT_KEYRING_NAME; - keyringItemSchema = DEFAULT_KEYRING_SCHEMA; - keyringItemName = DEFAULT_KEYRING_ITEM_NAME; - attributes = new String[] { DEFAULT_KEYRING_ATTR_NAME, DEFAULT_KEYRING_ATTR_VALUE }; allowUnencryptedCache = false; sharedTokenCacheEnabled = false; } @@ -231,13 +218,22 @@ public IdentityClientOptions setHttpClient(HttpClient httpClient) { return this; } - PersistenceSettings getPersistenceSettings() { - return PersistenceSettings.builder(cacheFileName, cacheFileDirectory) - .setMacKeychain(keychainService, keychainAccount) - .setLinuxKeyring(keyringName, keyringItemSchema, keyringItemName, - attributes[0], attributes[1], null, null) - .setLinuxUseUnprotectedFileAsCacheStorage(allowUnencryptedCache) - .build(); + PersistenceSettings getPublicClientPersistenceSettings() { + return PersistenceSettings.builder(DEFAULT_PUBLIC_CACHE_FILE_NAME, DEFAULT_CACHE_FILE_PATH) + .setMacKeychain(DEFAULT_KEYCHAIN_SERVICE, DEFAULT_PUBLIC_KEYCHAIN_ACCOUNT) + .setLinuxKeyring(DEFAULT_KEYRING_NAME, DEFAULT_KEYRING_SCHEMA, DEFAULT_PUBLIC_KEYRING_ITEM_NAME, + DEFAULT_KEYRING_ATTR_NAME, DEFAULT_KEYRING_ATTR_VALUE, null, null) + .setLinuxUseUnprotectedFileAsCacheStorage(allowUnencryptedCache) + .build(); + } + + PersistenceSettings getConfidentialClientPersistenceSettings() { + return PersistenceSettings.builder(DEFAULT_CONFIDENTIAL_CACHE_FILE_NAME, DEFAULT_CACHE_FILE_PATH) + .setMacKeychain(DEFAULT_KEYCHAIN_SERVICE, DEFAULT_CONFIDENTIAL_KEYCHAIN_ACCOUNT) + .setLinuxKeyring(DEFAULT_KEYRING_NAME, DEFAULT_KEYRING_SCHEMA, DEFAULT_CONFIDENTIAL_KEYRING_ITEM_NAME, + DEFAULT_KEYRING_ATTR_NAME, DEFAULT_KEYRING_ATTR_VALUE, null, null) + .setLinuxUseUnprotectedFileAsCacheStorage(allowUnencryptedCache) + .build(); } /** diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/AuthorizationCodeCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/AuthorizationCodeCredentialTest.java index b15a28f51478..511f9705dc27 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/AuthorizationCodeCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/AuthorizationCodeCredentialTest.java @@ -47,7 +47,7 @@ public void testValidAuthorizationCode() throws Exception { IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithAuthorizationCode(eq(request1), eq(authCode1), eq(redirectUri))) .thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); - when(identityClient.authenticateWithMsalAccount(any(), any())) + when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> { TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; if (argument.getScopes().size() == 1 && argument.getScopes().get(0).equals(request2.getScopes().get(0))) { diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientCertificateCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientCertificateCredentialTest.java index 9ab7dcfe867d..c2e88775cbd1 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientCertificateCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientCertificateCredentialTest.java @@ -22,6 +22,9 @@ import java.util.UUID; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; @RunWith(PowerMockRunner.class) @@ -45,10 +48,14 @@ public void testValidCertificates() throws Exception { OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); // mock - IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithPemCertificate(pemPath, request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - when(identityClient.authenticateWithPfxCertificate(pfxPath, pfxPassword, request2)).thenReturn(TestUtils.getMockAccessToken(token2, expiresAt)); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + IdentityClient pemIdentityClient = PowerMockito.mock(IdentityClient.class); + IdentityClient pfxIdentityClient = PowerMockito.mock(IdentityClient.class); + when(pemIdentityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(pfxIdentityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(pemIdentityClient.authenticateWithConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + when(pfxIdentityClient.authenticateWithConfidentialClient(request2)).thenReturn(TestUtils.getMockAccessToken(token2, expiresAt)); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), isNull(), eq(pemPath), isNull(), any()).thenReturn(pemIdentityClient); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), isNull(), eq(pfxPath), eq(pfxPassword), any()).thenReturn(pfxIdentityClient); // test ClientCertificateCredential credential = @@ -76,10 +83,14 @@ public void testInvalidCertificates() throws Exception { OffsetDateTime expiresOn = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); // mock - IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithPemCertificate(pemPath, request1)).thenReturn(Mono.error(new MsalServiceException("bad pem", "BadPem"))); - when(identityClient.authenticateWithPfxCertificate(pfxPath, pfxPassword, request2)).thenReturn(Mono.error(new MsalServiceException("bad pfx", "BadPfx"))); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + IdentityClient pemIdentityClient = PowerMockito.mock(IdentityClient.class); + IdentityClient pfxIdentityClient = PowerMockito.mock(IdentityClient.class); + when(pemIdentityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(pfxIdentityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(pemIdentityClient.authenticateWithConfidentialClient(request1)).thenReturn(Mono.error(new MsalServiceException("bad pem", "BadPem"))); + when(pfxIdentityClient.authenticateWithConfidentialClient(request2)).thenReturn(Mono.error(new MsalServiceException("bad pfx", "BadPfx"))); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), isNull(), eq(pemPath), isNull(), any()).thenReturn(pemIdentityClient); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), isNull(), eq(pfxPath), eq(pfxPassword), any()).thenReturn(pfxIdentityClient); // test ClientCertificateCredential credential = @@ -105,8 +116,9 @@ public void testInvalidParameters() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithPemCertificate(pemPath, request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), isNull(), eq(pemPath), isNull(), any()).thenReturn(identityClient); // test try { diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientSecretCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientSecretCredentialTest.java index 3cb4359cb004..cc692863a865 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientSecretCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ClientSecretCredentialTest.java @@ -23,6 +23,9 @@ import java.util.UUID; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; @RunWith(PowerMockRunner.class) @@ -45,8 +48,9 @@ public void testValidSecrets() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithClientSecret(secret, request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - when(identityClient.authenticateWithClientSecret(secret, request2)).thenReturn(TestUtils.getMockAccessToken(token2, expiresAt)); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + when(identityClient.authenticateWithConfidentialClient(request2)).thenReturn(TestUtils.getMockAccessToken(token2, expiresAt)); PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test @@ -75,8 +79,9 @@ public void testValidSecretsWithTokenRefreshOffset() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithClientSecret(secret, request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt, offset)); - when(identityClient.authenticateWithClientSecret(secret, request2)).thenReturn(TestUtils.getMockAccessToken(token2, expiresAt, offset)); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt, offset)); + when(identityClient.authenticateWithConfidentialClient(request2)).thenReturn(TestUtils.getMockAccessToken(token2, expiresAt, offset)); PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test @@ -107,9 +112,13 @@ public void testInvalidSecrets() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithClientSecret(secret, request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); - when(identityClient.authenticateWithClientSecret(badSecret, request)).thenReturn(Mono.error(new MsalServiceException("bad secret", "BadSecret"))); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + IdentityClient badIdentityClient = PowerMockito.mock(IdentityClient.class); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(badIdentityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); + when(badIdentityClient.authenticateWithConfidentialClient(request)).thenReturn(Mono.error(new MsalServiceException("bad secret", "BadSecret"))); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), eq(secret), isNull(), isNull(), any()).thenReturn(identityClient); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), eq(badSecret), isNull(), isNull(), any()).thenReturn(badIdentityClient); // test ClientSecretCredential credential = @@ -135,7 +144,8 @@ public void testInvalidParameters() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithClientSecret(secret, request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java index a01add160fb3..f57efbe5be80 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java @@ -21,6 +21,8 @@ import java.util.UUID; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; @RunWith(PowerMockRunner.class) @@ -48,8 +50,9 @@ public void testUseEnvironmentCredential() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithClientSecret(secret, request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(tenantId), eq(clientId), eq(secret), isNull(), isNull(), any()).thenReturn(identityClient); IntelliJCredential intelliJCredential = PowerMockito.mock(IntelliJCredential.class); when(intelliJCredential.getToken(request1)) diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DeviceCodeCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DeviceCodeCredentialTest.java index 8660390ff28d..adca2257f306 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DeviceCodeCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DeviceCodeCredentialTest.java @@ -46,7 +46,7 @@ public void testValidDeviceCode() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithDeviceCode(eq(request1), eq(consumer))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); - when(identityClient.authenticateWithMsalAccount(any(), any())) + when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> { TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; if (argument.getScopes().size() == 1 && argument.getScopes().get(0).equals(request2.getScopes().get(0))) { diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java index 1b5e1e2a94a8..7145a5086b4f 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java @@ -48,7 +48,7 @@ public void testValidInteractive() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(port))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); - when(identityClient.authenticateWithMsalAccount(any(), any())) + when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> { TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; if (argument.getScopes().size() == 1 && argument.getScopes().get(0).equals(request2.getScopes().get(0))) { diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/UsernamePasswordCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/UsernamePasswordCredentialTest.java index bd75c2307e03..73c27fd63708 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/UsernamePasswordCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/UsernamePasswordCredentialTest.java @@ -47,7 +47,7 @@ public void testValidUserCredential() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithUsernamePassword(request1, username, password)).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); - when(identityClient.authenticateWithMsalAccount(any(), any())) + when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> { TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; if (argument.getScopes().size() == 1 && argument.getScopes().get(0).equals(request2.getScopes().get(0))) { @@ -83,7 +83,7 @@ public void testInvalidUserCredential() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithUsernamePassword(request, username, badPassword)).thenThrow(new MsalServiceException("bad credential", "BadCredential")); - when(identityClient.authenticateWithMsalAccount(any(), any())) + when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> Mono.error(new UnsupportedOperationException("nothing cached"))); PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); @@ -107,7 +107,7 @@ public void testInvalidParameters() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithUsernamePassword(request, username, password)).thenReturn(TestUtils.getMockMsalToken(token1, expiresOn)); - when(identityClient.authenticateWithMsalAccount(any(), any())) + when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> Mono.error(new UnsupportedOperationException("nothing cached"))); PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java index 72adcc2c2f8d..e049c1f4e9bb 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java @@ -23,13 +23,13 @@ public class IdentityClientIntegrationTests { @Ignore("Integration tests") public void clientSecretCanGetToken() { - IdentityClient client = new IdentityClient(System.getenv(AZURE_TENANT_ID), System.getenv(AZURE_CLIENT_ID), new IdentityClientOptions()); - StepVerifier.create(client.authenticateWithClientSecret(System.getenv(AZURE_CLIENT_SECRET), request)) + IdentityClient client = new IdentityClient(System.getenv(AZURE_TENANT_ID), System.getenv(AZURE_CLIENT_ID), System.getenv(AZURE_CLIENT_SECRET), null, null, new IdentityClientOptions()); + StepVerifier.create(client.authenticateWithConfidentialClient(request)) .expectNextMatches(token -> token.getToken() != null && token.getExpiresAt() != null && !token.isExpired()) .verifyComplete(); - StepVerifier.create(client.authenticateWithClientSecret(System.getenv(AZURE_CLIENT_SECRET), new TokenRequestContext().addScopes("https://vault.azure.net/.default"))) + StepVerifier.create(client.authenticateWithConfidentialClient(new TokenRequestContext().addScopes("https://vault.azure.net/.default"))) .expectNextMatches(token -> token.getToken() != null && token.getExpiresAt() != null && !token.isExpired()) @@ -38,7 +38,7 @@ public void clientSecretCanGetToken() { @Ignore("Integration tests") public void deviceCodeCanGetToken() { - IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); + IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), null, null, null, new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); MsalToken token = client.authenticateWithDeviceCode(request, deviceCode -> { System.out.println(deviceCode.getMessage()); try { @@ -51,7 +51,7 @@ public void deviceCodeCanGetToken() { Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); Assert.assertFalse(token.isExpired()); - token = client.authenticateWithMsalAccount(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); + token = client.authenticateWithPublicClientCache(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); @@ -60,13 +60,13 @@ public void deviceCodeCanGetToken() { @Ignore("Integration tests") public void browserCanGetToken() { - IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); + IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), null, null, null, new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); MsalToken token = client.authenticateWithBrowserInteraction(request, 8765).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); Assert.assertFalse(token.isExpired()); - token = client.authenticateWithMsalAccount(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); + token = client.authenticateWithPublicClientCache(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); @@ -75,13 +75,13 @@ public void browserCanGetToken() { @Ignore("Integration tests") public void usernamePasswordCanGetToken() { - IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); + IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), null, null, null, new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); MsalToken token = client.authenticateWithUsernamePassword(request, System.getenv("username"), System.getenv("password")).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); Assert.assertFalse(token.isExpired()); - token = client.authenticateWithMsalAccount(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); + token = client.authenticateWithPublicClientCache(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); @@ -90,13 +90,13 @@ public void usernamePasswordCanGetToken() { @Ignore("Integration tests") public void authCodeCanGetToken() throws Exception { - IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), new IdentityClientOptions()); + IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), null, null, null, new IdentityClientOptions()); MsalToken token = client.authenticateWithAuthorizationCode(request, System.getenv("AZURE_AUTH_CODE"), new URI("http://localhost:8000")).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); Assert.assertFalse(token.isExpired()); - token = client.authenticateWithMsalAccount(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); + token = client.authenticateWithPublicClientCache(new TokenRequestContext().addScopes("https://vault.azure.net/.default"), token.getAccount()).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java index 72fe05076ecc..afec805e3c62 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java @@ -72,8 +72,9 @@ public void testValidSecret() throws Exception { mockForClientSecret(secret, request, accessToken, expiresOn); // test - IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId).build(); - AccessToken token = client.authenticateWithClientSecret(secret, request).block(); + IdentityClient client = new IdentityClientBuilder() + .tenantId(tenantId).clientId(clientId).clientSecret(secret).build(); + AccessToken token = client.authenticateWithConfidentialClient(request).block(); Assert.assertEquals(accessToken, token.getToken()); Assert.assertEquals(expiresOn.getSecond(), token.getExpiresAt().getSecond()); } @@ -91,8 +92,9 @@ public void testInvalidSecret() throws Exception { // test try { - IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId).build(); - client.authenticateWithClientSecret("bad secret", request).block(); + IdentityClient client = new IdentityClientBuilder() + .tenantId(tenantId).clientId(clientId).clientSecret("bad secret").build(); + client.authenticateWithConfidentialClient(request).block(); fail(); } catch (MsalServiceException e) { Assert.assertEquals("Invalid clientSecret", e.getMessage()); @@ -111,8 +113,9 @@ public void testValidCertificate() throws Exception { mockForClientCertificate(request, accessToken, expiresOn); // test - IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId).build(); - AccessToken token = client.authenticateWithPfxCertificate(pfxPath, "StrongPass!123", request).block(); + IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId) + .certificatePath(pfxPath).certificatePassword("StrongPass!123").build(); + AccessToken token = client.authenticateWithConfidentialClient(request).block(); Assert.assertEquals(accessToken, token.getToken()); Assert.assertEquals(expiresOn.getSecond(), token.getExpiresAt().getSecond()); } @@ -131,8 +134,9 @@ public void testPemCertificate() throws Exception { // mock mockForClientPemCertificate(accessToken, request, expiresOn); // test - IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId).build(); - AccessToken token = client.authenticateWithPemCertificate(pemPath, request).block(); + IdentityClient client = new IdentityClientBuilder() + .tenantId(tenantId).clientId(clientId).certificatePath(pemPath).build(); + AccessToken token = client.authenticateWithConfidentialClient(request).block(); Assert.assertEquals(accessToken, token.getToken()); Assert.assertEquals(expiresOn.getSecond(), token.getExpiresAt().getSecond()); } @@ -150,8 +154,9 @@ public void testInvalidCertificatePassword() throws Exception { // test try { - IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId).build(); - client.authenticateWithPfxCertificate(pfxPath, "BadPassword", request).block(); + IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId) + .certificatePath(pfxPath).certificatePassword("BadPassword").build(); + client.authenticateWithConfidentialClient(request).block(); fail(); } catch (Exception e) { Assert.assertTrue(e.getMessage().contains("password was incorrect")); @@ -260,7 +265,7 @@ public void testUserRefreshTokenflow() throws Exception { // test IdentityClientOptions options = new IdentityClientOptions(); IdentityClient client = new IdentityClientBuilder().tenantId(tenantId).clientId(clientId).identityClientOptions(options).build(); - StepVerifier.create(client.authenticateWithMsalAccount(request2, TestUtils.getMockMsalAccount(token1, expiresAt).block())) + StepVerifier.create(client.authenticateWithPublicClientCache(request2, TestUtils.getMockMsalAccount(token1, expiresAt).block())) .expectNextMatches(accessToken -> token2.equals(accessToken.getToken()) && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) .verifyComplete(); @@ -320,7 +325,7 @@ public void testOpenUrl() throws Exception { when(rt.exec(anyString())).thenReturn(a); // test - IdentityClient client = new IdentityClientBuilder().build(); + IdentityClient client = new IdentityClientBuilder().clientId("dummy").build(); client.openUrl("https://localhost.com"); } From 5939411318bfdf625b8410cc3f43ab2345d0654c Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 20 May 2020 23:15:18 -0700 Subject: [PATCH 02/43] Checkstyle: log exception --- .../com/azure/identity/implementation/IdentityClient.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index eef475adc85a..052b7d5dfec4 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -163,10 +163,12 @@ private ConfidentialClientApplication getConfidentialClientApplication() { | KeyStoreException | NoSuchProviderException | IOException e) { - throw new RuntimeException(e); + throw logger.logExceptionAsError( + new RuntimeException("Failed to parse the certificate for the credential.", e)); } } else { - throw new IllegalArgumentException("Must provide client secret or client certificate path"); + throw logger.logExceptionAsError( + new IllegalArgumentException("Must provide client secret or client certificate path")); } ConfidentialClientApplication.Builder applicationBuilder = ConfidentialClientApplication.builder(clientId, credential); From 9a75ef91f4cf345892f19fe9bb6195769ed690c1 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 21 May 2020 00:30:08 -0700 Subject: [PATCH 03/43] improve cert parse error --- .../com/azure/identity/implementation/IdentityClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 052b7d5dfec4..5f9da0d8fe80 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -163,8 +163,8 @@ private ConfidentialClientApplication getConfidentialClientApplication() { | KeyStoreException | NoSuchProviderException | IOException e) { - throw logger.logExceptionAsError( - new RuntimeException("Failed to parse the certificate for the credential.", e)); + throw logger.logExceptionAsError(new RuntimeException( + "Failed to parse the certificate for the credential: " + e.getMessage(), e)); } } else { throw logger.logExceptionAsError( From 4d99147f0fbf0e00df43089a36000261f7acec70 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 26 May 2020 15:55:07 -0700 Subject: [PATCH 04/43] Check token refresh offset after read from cache --- .../azure/core/credential/AccessToken.java | 2 +- .../implementation/IdentityClient.java | 10 ++++---- .../implementation/IdentityToken.java | 24 ------------------- .../identity/implementation/MsalToken.java | 6 ++--- 4 files changed, 9 insertions(+), 33 deletions(-) delete mode 100644 sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java index b8673a9fc960..b12996a55f37 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java @@ -19,7 +19,7 @@ public class AccessToken { */ public AccessToken(String token, OffsetDateTime expiresAt) { this.token = token; - this.expiresAt = expiresAt.minusMinutes(2); // 2 minutes before token expires + this.expiresAt = expiresAt; } /** diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 5f9da0d8fe80..b00bec7e75a6 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -415,7 +415,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { OffsetDateTime expiresOn = LocalDateTime.parse(timeJoinedWithT, DateTimeFormatter.ISO_LOCAL_DATE_TIME) .atZone(ZoneId.systemDefault()) .toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC); - token = new IdentityToken(accessToken, expiresOn, options); + token = new AccessToken(accessToken, expiresOn); } catch (IOException | InterruptedException e) { throw logger.logExceptionAsError(new IllegalStateException(e)); } catch (RuntimeException e) { @@ -493,7 +493,7 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req throw logger.logExceptionAsError(Exceptions.propagate(e)); } }).map(ar -> new MsalToken(ar, options)) - .filter(t -> !t.isExpired()) + .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))) .switchIfEmpty(Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())).forceRefresh(true); @@ -524,7 +524,7 @@ public Mono authenticateWithConfidentialClientCache(TokenRequestCon throw logger.logExceptionAsError(Exceptions.propagate(e)); } }).map(ar -> new MsalToken(ar, options)) - .filter(t -> !t.isExpired()) + .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))) .switchIfEmpty(Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())).forceRefresh(true); @@ -728,7 +728,7 @@ public Mono authenticateToManagedIdentityEndpoint(String msiEndpoin String result = s.hasNext() ? s.next() : ""; MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new IdentityToken(msiToken.getToken(), msiToken.getExpiresAt(), options); + return new AccessToken(msiToken.getToken(), msiToken.getExpiresAt()); } finally { if (connection != null) { connection.disconnect(); @@ -782,7 +782,7 @@ public Mono authenticateToIMDSEndpoint(TokenRequestContext request) MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new IdentityToken(msiToken.getToken(), msiToken.getExpiresAt(), options); + return new AccessToken(msiToken.getToken(), msiToken.getExpiresAt()); } catch (IOException exception) { if (connection == null) { throw logger.logExceptionAsError(new RuntimeException( diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java deleted file mode 100644 index c6973b3d99a8..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation; - -import com.azure.core.credential.AccessToken; - -import java.time.OffsetDateTime; - -/** - * Type representing authentication result from the azure-identity client. - */ -public class IdentityToken extends AccessToken { - /** - * Creates an identity token instance. - * - * @param token the token string. - * @param expiresAt the expiration time. - * @param options the identity client options. - */ - public IdentityToken(String token, OffsetDateTime expiresAt, IdentityClientOptions options) { - super(token, expiresAt.plusMinutes(2).minus(options.getTokenRefreshOffset())); - } -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java index 5ccc94892a49..41e4a6a316fa 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java @@ -3,6 +3,7 @@ package com.azure.identity.implementation; +import com.azure.core.credential.AccessToken; import com.microsoft.aad.msal4j.IAccount; import com.microsoft.aad.msal4j.IAuthenticationResult; @@ -12,7 +13,7 @@ /** * Type representing authentication result from the MSAL (Microsoft Authentication Library). */ -public final class MsalToken extends IdentityToken { +public final class MsalToken extends AccessToken { private IAccount account; @@ -23,8 +24,7 @@ public final class MsalToken extends IdentityToken { */ public MsalToken(IAuthenticationResult msalResult, IdentityClientOptions options) { super(msalResult.accessToken(), - OffsetDateTime.ofInstant(msalResult.expiresOnDate().toInstant(), ZoneOffset.UTC), - options); + OffsetDateTime.ofInstant(msalResult.expiresOnDate().toInstant(), ZoneOffset.UTC)); this.account = msalResult.account(); } From 354bc074b6aa9e25e8b06119a2dfa0811f6a3350 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 2 Jun 2020 18:07:58 -0700 Subject: [PATCH 05/43] Revert changes in azure-core for backward compatibility --- .../azure/core/credential/AccessToken.java | 2 +- .../BearerTokenAuthenticationPolicy.java | 9 ++++-- .../implementation/IdentityClient.java | 28 +++++++++---------- .../implementation/IdentityToken.java | 23 +++++++++++++++ .../identity/implementation/MSIToken.java | 3 +- .../identity/implementation/MsalToken.java | 5 ++-- 6 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java index b12996a55f37..b8673a9fc960 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java @@ -19,7 +19,7 @@ public class AccessToken { */ public AccessToken(String token, OffsetDateTime expiresAt) { this.token = token; - this.expiresAt = expiresAt; + this.expiresAt = expiresAt.minusMinutes(2); // 2 minutes before token expires } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index 3ab432c07e3c..f643b4975664 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -3,6 +3,7 @@ package com.azure.core.http.policy; +import com.azure.core.credential.SimpleTokenCache; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpPipelineCallContext; @@ -21,7 +22,8 @@ public class BearerTokenAuthenticationPolicy implements HttpPipelinePolicy { private static final String BEARER = "Bearer"; private final TokenCredential credential; - private final TokenRequestContext tokenRequestContext; + private final String[] scopes; + private final SimpleTokenCache cache; /** * Creates BearerTokenAuthenticationPolicy. @@ -34,7 +36,8 @@ public BearerTokenAuthenticationPolicy(TokenCredential credential, String... sco Objects.requireNonNull(scopes); assert scopes.length > 0; this.credential = credential; - this.tokenRequestContext = new TokenRequestContext().addScopes(scopes); + this.scopes = scopes; + this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes))); } @Override @@ -42,7 +45,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN if ("http".equals(context.getHttpRequest().getUrl().getProtocol())) { return Mono.error(new RuntimeException("token credentials require a URL using the HTTPS protocol scheme")); } - return credential.getToken(tokenRequestContext) + return cache.getToken() .flatMap(token -> { context.getHttpRequest().getHeaders().put(AUTHORIZATION_HEADER, BEARER + " " + token.getToken()); return next.process(); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 375e0e0008a8..10cf51837e1e 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -265,7 +265,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { return Mono.fromFuture(application.acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) .build())) - .map(ar -> new MsalToken(ar, options)); + .map(MsalToken::new); } catch (MalformedURLException e) { return Mono.error(e); } @@ -279,7 +279,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { .build(); return Mono.defer(() -> Mono.fromFuture(getPublicClientApplication(false).acquireToken(parameters)) - .map(ar -> new MsalToken(ar, options))); + .map(MsalToken::new)); } else { throw logger.logExceptionAsError(new CredentialUnavailableException( @@ -377,7 +377,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { OffsetDateTime expiresOn = LocalDateTime.parse(timeJoinedWithT, DateTimeFormatter.ISO_LOCAL_DATE_TIME) .atZone(ZoneId.systemDefault()) .toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC); - token = new AccessToken(accessToken, expiresOn); + token = new IdentityToken(accessToken, expiresOn); } catch (IOException | InterruptedException e) { throw logger.logExceptionAsError(new IllegalStateException(e)); } catch (RuntimeException e) { @@ -403,7 +403,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { public Mono authenticateWithConfidentialClient(TokenRequestContext request) { return Mono.fromFuture(() -> getConfidentialClientApplication().acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build())) - .map(ar -> new MsalToken(ar, options)); + .map(MsalToken::new); } private HttpPipeline setupPipeline(HttpClient httpClient) { @@ -431,7 +431,7 @@ public Mono authenticateWithUsernamePassword(TokenRequestContext requ UserNamePasswordParameters.builder(new HashSet<>(request.getScopes()), username, password.toCharArray()) .build())) .onErrorMap(t -> new ClientAuthenticationException("Failed to acquire token with username and password", - null, t)).map(ar -> new MsalToken(ar, options)); + null, t)).map(MsalToken::new); } /** @@ -454,7 +454,7 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req } catch (MalformedURLException e) { return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } - }).map(ar -> new MsalToken(ar, options)) + }).map(MsalToken::new) .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))) .switchIfEmpty(Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( @@ -467,7 +467,7 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req } catch (MalformedURLException e) { return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } - }).map(result -> new MsalToken(result, options))); + }).map(MsalToken::new)); } /** @@ -485,7 +485,7 @@ public Mono authenticateWithConfidentialClientCache(TokenRequestCon } catch (MalformedURLException e) { return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } - }).map(ar -> (AccessToken) new MsalToken(ar, options)) + }).map(ar -> (AccessToken) new MsalToken(ar)) .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))); } @@ -507,7 +507,7 @@ public Mono authenticateWithDeviceCode(TokenRequestContext request, dc.verificationUri(), OffsetDateTime.now().plusSeconds(dc.expiresIn()), dc.message()))).build(); return getPublicClientApplication(false).acquireToken(parameters); }).onErrorMap(t -> new ClientAuthenticationException("Failed to acquire token with device code", null, t)) - .map(ar -> new MsalToken(ar, options)); + .map(MsalToken::new); } /** @@ -527,7 +527,7 @@ public Mono authenticateWithVsCodeCredential(TokenRequestContext requ .build(); return Mono.defer(() -> Mono.fromFuture(getPublicClientApplication(false).acquireToken(parameters)) - .map(ar -> new MsalToken(ar, options))); + .map(MsalToken::new)); } /** @@ -545,7 +545,7 @@ public Mono authenticateWithAuthorizationCode(TokenRequestContext req .scopes(new HashSet<>(request.getScopes())) .build())) .onErrorMap(t -> new ClientAuthenticationException("Failed to acquire token with authorization code", - null, t)).map(ar -> new MsalToken(ar, options)); + null, t)).map(MsalToken::new); } /** @@ -680,8 +680,7 @@ public Mono authenticateToManagedIdentityEndpoint(String msiEndpoin .useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; - MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new AccessToken(msiToken.getToken(), msiToken.getExpiresAt()); + return SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); } finally { if (connection != null) { connection.disconnect(); @@ -733,9 +732,8 @@ public Mono authenticateToIMDSEndpoint(TokenRequestContext request) .useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; - MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, + return SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new AccessToken(msiToken.getToken(), msiToken.getExpiresAt()); } catch (IOException exception) { if (connection == null) { throw logger.logExceptionAsError(new RuntimeException( diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java new file mode 100644 index 000000000000..adebac2dbe3f --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.implementation; + +import com.azure.core.credential.AccessToken; + +import java.time.OffsetDateTime; + +/** + * Type representing authentication result from the azure-identity client. + */ +public class IdentityToken extends AccessToken { + /** + * Creates an identity token instance. + * + * @param token the token string. + * @param expiresAt the expiration time. + */ + public IdentityToken(String token, OffsetDateTime expiresAt) { + super(token, expiresAt.plusMinutes(2)); + } +} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java index 91424f509cc0..bdf353c4cf32 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java @@ -3,7 +3,6 @@ package com.azure.identity.implementation; -import com.azure.core.credential.AccessToken; import com.azure.core.util.logging.ClientLogger; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -17,7 +16,7 @@ /** * Type representing response from the local MSI token provider. */ -public final class MSIToken extends AccessToken { +public final class MSIToken extends IdentityToken { private static final OffsetDateTime EPOCH = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); @JsonProperty(value = "token_type") diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java index 41e4a6a316fa..4e6369e2911a 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java @@ -3,7 +3,6 @@ package com.azure.identity.implementation; -import com.azure.core.credential.AccessToken; import com.microsoft.aad.msal4j.IAccount; import com.microsoft.aad.msal4j.IAuthenticationResult; @@ -13,7 +12,7 @@ /** * Type representing authentication result from the MSAL (Microsoft Authentication Library). */ -public final class MsalToken extends AccessToken { +public final class MsalToken extends IdentityToken { private IAccount account; @@ -22,7 +21,7 @@ public final class MsalToken extends AccessToken { * * @param msalResult the raw authentication result returned by MSAL */ - public MsalToken(IAuthenticationResult msalResult, IdentityClientOptions options) { + public MsalToken(IAuthenticationResult msalResult) { super(msalResult.accessToken(), OffsetDateTime.ofInstant(msalResult.expiresOnDate().toInstant(), ZoneOffset.UTC)); this.account = msalResult.account(); From 0f53d6dfde7ac099326efb6bd1aab291d20088b6 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 17:16:48 -0700 Subject: [PATCH 06/43] Add getTokenRefreshOffset() to TokenCredential --- .../azure/core/credential/AccessToken.java | 2 +- .../core/credential/SimpleTokenCache.java | 17 ++++++++++- .../core/credential/TokenCredential.java | 14 +++++++++ .../BearerTokenAuthenticationPolicy.java | 4 ++- sdk/identity/azure-identity/pom.xml | 4 +-- .../azure/identity/AzureCliCredential.java | 9 ++++++ .../identity/ChainedTokenCredential.java | 10 ++++++- .../identity/ClientCertificateCredential.java | 8 +++++ .../identity/ClientSecretCredential.java | 8 +++++ .../identity/DefaultAzureCredential.java | 12 +++++++- .../DefaultAzureCredentialBuilder.java | 2 +- .../azure/identity/DeviceCodeCredential.java | 8 +++++ .../azure/identity/EnvironmentCredential.java | 7 +++++ .../azure/identity/IntelliJCredential.java | 8 +++++ .../identity/ManagedIdentityCredential.java | 9 ++++++ .../identity/SharedTokenCacheCredential.java | 8 +++++ .../identity/UsernamePasswordCredential.java | 8 +++++ .../identity/VisualStudioCodeCredential.java | 8 +++++ .../implementation/IdentityClient.java | 29 ++++++++++--------- .../implementation/IdentityClientOptions.java | 3 +- .../implementation/IdentityToken.java | 23 --------------- .../identity/implementation/MSIToken.java | 3 +- .../identity/implementation/MsalToken.java | 3 +- .../com/azure/identity/util/TestUtils.java | 3 +- 24 files changed, 161 insertions(+), 49 deletions(-) delete mode 100644 sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java index b8673a9fc960..b12996a55f37 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java @@ -19,7 +19,7 @@ public class AccessToken { */ public AccessToken(String token, OffsetDateTime expiresAt) { this.token = token; - this.expiresAt = expiresAt.minusMinutes(2); // 2 minutes before token expires + this.expiresAt = expiresAt; } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index b80416ca9952..d2cb192f8855 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -8,7 +8,9 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; +import java.time.OffsetDateTime; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -22,6 +24,7 @@ public class SimpleTokenCache { private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; + private final Function tokenExpired; /** * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". @@ -29,8 +32,20 @@ public class SimpleTokenCache { * @param tokenSupplier a method to get a new token */ public SimpleTokenCache(Supplier> tokenSupplier) { + this(tokenSupplier, + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET))); + } + + /** + * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". + * + * @param tokenSupplier a method to get a new token + * @param tokenExpired a method to check if the cached token is expired + */ + public SimpleTokenCache(Supplier> tokenSupplier, Function tokenExpired) { this.wip = new AtomicBoolean(false); this.tokenSupplier = tokenSupplier; + this.tokenExpired = tokenExpired; } /** @@ -38,7 +53,7 @@ public SimpleTokenCache(Supplier> tokenSupplier) { * @return a Publisher that emits an AccessToken */ public Mono getToken() { - if (cache != null && !cache.isExpired()) { + if (cache != null && !tokenExpired.apply(cache)) { return Mono.just(cache); } return Mono.defer(() -> { diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java index 79c29d6e0416..62b00338f176 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java @@ -5,15 +5,29 @@ import reactor.core.publisher.Mono; +import java.time.Duration; + /** * The interface for credentials that can provide a token. */ @FunctionalInterface public interface TokenCredential { + /** + * The default duration before the actual token expiry to refresh the token. The value is 2 minutes. + */ + Duration DEFAULT_TOKEN_REFRESH_OFFSET = Duration.ofMinutes(2); + /** * Asynchronously get a token for a given resource/audience. * @param request the details of the token request * @return a Publisher that emits a single access token */ Mono getToken(TokenRequestContext request); + + /** + * The duration before the actual token expiry to refresh the token. Default is 2 minutes. + */ + default Duration getTokenRefreshOffset() { + return DEFAULT_TOKEN_REFRESH_OFFSET; + } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index f643b4975664..ad184e6841de 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -11,6 +11,7 @@ import com.azure.core.http.HttpResponse; import reactor.core.publisher.Mono; +import java.time.OffsetDateTime; import java.util.Objects; /** @@ -37,7 +38,8 @@ public BearerTokenAuthenticationPolicy(TokenCredential credential, String... sco assert scopes.length > 0; this.credential = credential; this.scopes = scopes; - this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes))); + this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes)), + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(credential.getTokenRefreshOffset()))); } @Override diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index e2e86ccccc69..2931e6047a18 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -27,7 +27,7 @@ com.azure azure-core - 1.5.0 + 1.6.0-beta.1 com.microsoft.azure @@ -88,7 +88,7 @@ com.azure azure-core-http-netty - 1.5.1 + 1.6.0-beta.1 test diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java index 0e329129d74c..046278d8875e 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java @@ -14,6 +14,8 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import java.time.Duration; + /** * A credential provider that provides token credentials based on Azure CLI * command. @@ -21,6 +23,7 @@ @Immutable class AzureCliCredential implements TokenCredential { private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; /** * Creates an AzureCliSecretCredential with default identity client options. @@ -28,10 +31,16 @@ class AzureCliCredential implements TokenCredential { */ AzureCliCredential(IdentityClientOptions identityClientOptions) { identityClient = new IdentityClientBuilder().identityClientOptions(identityClientOptions).build(); + this.identityClientOptions = identityClientOptions; } @Override public Mono getToken(TokenRequestContext request) { return identityClient.authenticateWithAzureCli(request); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java index 88dbb58a1f93..f7852f4869b1 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java @@ -10,6 +10,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.ArrayList; import java.util.Deque; import java.util.List; @@ -25,6 +26,7 @@ @Immutable public class ChainedTokenCredential implements TokenCredential { private final Deque credentials; + private Duration tokenRefreshOffset; /** * Create an instance of chained token credential that aggregates a list of token @@ -32,6 +34,7 @@ public class ChainedTokenCredential implements TokenCredential { */ ChainedTokenCredential(Deque credentials) { this.credentials = credentials; + tokenRefreshOffset = TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET; } @Override @@ -41,7 +44,7 @@ public Mono getToken(TokenRequestContext request) { .flatMap(p -> p.getToken(request).onErrorResume(CredentialUnavailableException.class, t -> { exceptions.add(t); return Mono.empty(); - }), 1) + }).doOnNext(t -> tokenRefreshOffset = p.getTokenRefreshOffset()), 1) .next() .switchIfEmpty(Mono.defer(() -> { @@ -61,4 +64,9 @@ public Mono getToken(TokenRequestContext request) { return Mono.error(new CredentialUnavailableException(message.toString(), last)); })); } + + @Override + public Duration getTokenRefreshOffset() { + return tokenRefreshOffset; + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java index e3929c58f0a1..20ebba2d8110 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java @@ -12,6 +12,7 @@ import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.Objects; /** @@ -26,6 +27,7 @@ @Immutable public class ClientCertificateCredential implements TokenCredential { private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; /** * Creates a ClientSecretCredential with default identity client options. @@ -45,6 +47,7 @@ public class ClientCertificateCredential implements TokenCredential { .certificatePassword(certificatePassword) .identityClientOptions(identityClientOptions) .build(); + this.identityClientOptions = identityClientOptions; } @Override @@ -53,4 +56,9 @@ public Mono getToken(TokenRequestContext request) { .onErrorResume(t -> Mono.empty()) .switchIfEmpty(Mono.defer(() -> identityClient.authenticateWithConfidentialClient(request))); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java index c9ee963dd452..77c911402a98 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java @@ -12,6 +12,7 @@ import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.Objects; /** @@ -26,6 +27,7 @@ @Immutable public class ClientSecretCredential implements TokenCredential { private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; /** * Creates a ClientSecretCredential with the given identity client options. @@ -45,6 +47,7 @@ public class ClientSecretCredential implements TokenCredential { .clientSecret(clientSecret) .identityClientOptions(identityClientOptions) .build(); + this.identityClientOptions = identityClientOptions; } @Override @@ -53,4 +56,9 @@ public Mono getToken(TokenRequestContext request) { .onErrorResume(t -> Mono.empty()) .switchIfEmpty(Mono.defer(() -> identityClient.authenticateWithConfidentialClient(request))); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java index e8275e8ede9b..89a8e66ea4bf 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java @@ -5,7 +5,9 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.TokenCredential; +import com.azure.identity.implementation.IdentityClientOptions; +import java.time.Duration; import java.util.ArrayDeque; /** @@ -22,6 +24,7 @@ */ @Immutable public final class DefaultAzureCredential extends ChainedTokenCredential { + private final IdentityClientOptions identityClientOptions; /** * Creates default DefaultAzureCredential instance to use. This will use AZURE_CLIENT_ID, @@ -32,8 +35,15 @@ public final class DefaultAzureCredential extends ChainedTokenCredential { * token cache. * * @param tokenCredentials the list of credentials to execute for authentication. + * @param identityClientOptions the options for configuring the identity client. */ - DefaultAzureCredential(ArrayDeque tokenCredentials) { + DefaultAzureCredential(ArrayDeque tokenCredentials, IdentityClientOptions identityClientOptions) { super(tokenCredentials); + this.identityClientOptions = identityClientOptions; + } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java index 5a51f5b3e407..3543db6393e4 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java @@ -166,7 +166,7 @@ public DefaultAzureCredentialBuilder executorService(ExecutorService executorSer * @return a {@link DefaultAzureCredential} with the current configurations. */ public DefaultAzureCredential build() { - return new DefaultAzureCredential(getCredentialsChain()); + return new DefaultAzureCredential(getCredentialsChain(), identityClientOptions); } private ArrayDeque getCredentialsChain() { diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java index 81ac16498d5f..d206fe57b90c 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java @@ -13,6 +13,7 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -23,6 +24,7 @@ public class DeviceCodeCredential implements TokenCredential { private final Consumer challengeConsumer; private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; private final AtomicReference cachedToken; /** @@ -42,6 +44,7 @@ public class DeviceCodeCredential implements TokenCredential { .identityClientOptions(identityClientOptions) .build(); this.cachedToken = new AtomicReference<>(); + this.identityClientOptions = identityClientOptions; } @Override @@ -60,4 +63,9 @@ public Mono getToken(TokenRequestContext request) { return msalToken; }); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java index c333e2118d9c..aefe398a619c 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java @@ -12,6 +12,8 @@ import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; +import java.time.Duration; + /** * A credential provider that provides token credentials based on environment variables. The environment variables * expected are: @@ -83,6 +85,11 @@ public Mono getToken(TokenRequestContext request) { } } + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } + private boolean verifyNotNull(String... configs) { for (String config: configs) { if (config == null) { diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java index ca8d88e1f376..49bf96dd4132 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java @@ -16,6 +16,7 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; /** @@ -28,6 +29,7 @@ class IntelliJCredential implements TokenCredential { private static final String AZURE_TOOLS_FOR_INTELLIJ_CLIENT_ID = "61d65f5a-6e3b-468b-af73-a033f5098c5c"; private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; private final AtomicReference cachedToken; /** @@ -69,6 +71,7 @@ class IntelliJCredential implements TokenCredential { .build(); this.cachedToken = new AtomicReference<>(); + this.identityClientOptions = identityClientOptions; } @Override @@ -87,4 +90,9 @@ public Mono getToken(TokenRequestContext request) { return msalToken; }); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java index 8ccd89fa21ea..6d9570c92b4b 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java @@ -13,6 +13,8 @@ import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; +import java.time.Duration; + /** * The base class for Managed Service Identity token based credentials. */ @@ -20,6 +22,7 @@ public final class ManagedIdentityCredential implements TokenCredential { private final AppServiceMsiCredential appServiceMSICredential; private final VirtualMachineMsiCredential virtualMachineMSICredential; + private final IdentityClientOptions identityClientOptions; /** * Creates an instance of the ManagedIdentityCredential. @@ -39,6 +42,7 @@ public final class ManagedIdentityCredential implements TokenCredential { virtualMachineMSICredential = new VirtualMachineMsiCredential(clientId, identityClient); appServiceMSICredential = null; } + this.identityClientOptions = identityClientOptions; } /** @@ -57,4 +61,9 @@ public Mono getToken(TokenRequestContext request) { ? appServiceMSICredential.authenticate(request) : virtualMachineMSICredential.authenticate(request)); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java index 261bfb1cbdea..5a1a484cb6c4 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java @@ -13,6 +13,7 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; /** @@ -27,6 +28,7 @@ public class SharedTokenCacheCredential implements TokenCredential { private final AtomicReference cachedToken; private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; /** * Creates an instance of the Shared Token Cache Credential Provider. @@ -61,6 +63,7 @@ public class SharedTokenCacheCredential implements TokenCredential { .identityClientOptions(identityClientOptions) .build(); this.cachedToken = new AtomicReference<>(); + this.identityClientOptions = identityClientOptions; } /** @@ -82,4 +85,9 @@ public Mono getToken(TokenRequestContext request) { return msalToken; }); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java index a0ed76fbb2c3..2c0f522043a6 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java @@ -13,6 +13,7 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -26,6 +27,7 @@ public class UsernamePasswordCredential implements TokenCredential { private final String username; private final String password; private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; private final AtomicReference cachedToken; /** @@ -50,6 +52,7 @@ public class UsernamePasswordCredential implements TokenCredential { .identityClientOptions(identityClientOptions) .build(); cachedToken = new AtomicReference<>(); + this.identityClientOptions = identityClientOptions; } @Override @@ -67,4 +70,9 @@ public Mono getToken(TokenRequestContext request) { return msalToken; }); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java index 59e78fa713fb..53403f3bc3f9 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java @@ -14,6 +14,7 @@ import com.azure.identity.implementation.VisualStudioCacheAccessor; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -22,6 +23,7 @@ */ class VisualStudioCodeCredential implements TokenCredential { private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; private final AtomicReference cachedToken; private final String cloudInstance; @@ -61,6 +63,7 @@ class VisualStudioCodeCredential implements TokenCredential { .build(); this.cachedToken = new AtomicReference<>(); + this.identityClientOptions = identityClientOptions; } @Override @@ -79,4 +82,9 @@ public Mono getToken(TokenRequestContext request) { return msalToken; }); } + + @Override + public Duration getTokenRefreshOffset() { + return identityClientOptions.getTokenRefreshOffset(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 10cf51837e1e..7a1652af23fa 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -264,8 +264,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { ConfidentialClientApplication application = applicationBuilder.build(); return Mono.fromFuture(application.acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) - .build())) - .map(MsalToken::new); + .build())).map(MsalToken::new); } catch (MalformedURLException e) { return Mono.error(e); } @@ -297,7 +296,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithAzureCli(TokenRequestContext request) { + public Mono authenticateWithAzureCli(TokenRequestContext request) { String azCommand = "az account get-access-token --output json --resource "; StringBuilder command = new StringBuilder(); @@ -313,7 +312,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { command.append(scopes); - AccessToken token = null; + com.azure.core.credential.AccessToken token = null; BufferedReader reader = null; try { String starter; @@ -377,7 +376,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { OffsetDateTime expiresOn = LocalDateTime.parse(timeJoinedWithT, DateTimeFormatter.ISO_LOCAL_DATE_TIME) .atZone(ZoneId.systemDefault()) .toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC); - token = new IdentityToken(accessToken, expiresOn); + token = new AccessToken(accessToken, expiresOn); } catch (IOException | InterruptedException e) { throw logger.logExceptionAsError(new IllegalStateException(e)); } catch (RuntimeException e) { @@ -400,7 +399,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithConfidentialClient(TokenRequestContext request) { + public Mono authenticateWithConfidentialClient(TokenRequestContext request) { return Mono.fromFuture(() -> getConfidentialClientApplication().acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build())) .map(MsalToken::new); @@ -476,7 +475,7 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithConfidentialClientCache(TokenRequestContext request) { + public Mono authenticateWithConfidentialClientCache(TokenRequestContext request) { return Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())); @@ -485,7 +484,7 @@ public Mono authenticateWithConfidentialClientCache(TokenRequestCon } catch (MalformedURLException e) { return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } - }).map(ar -> (AccessToken) new MsalToken(ar)) + }).map(ar -> (com.azure.core.credential.AccessToken) new MsalToken(ar)) .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))); } @@ -649,8 +648,8 @@ public Mono authenticateWithSharedTokenCache(TokenRequestContext requ * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateToManagedIdentityEndpoint(String msiEndpoint, String msiSecret, - TokenRequestContext request) { + public Mono authenticateToManagedIdentityEndpoint(String msiEndpoint, String msiSecret, + TokenRequestContext request) { return Mono.fromCallable(() -> { String resource = ScopeUtil.scopesToResource(request.getScopes()); HttpURLConnection connection = null; @@ -680,7 +679,9 @@ public Mono authenticateToManagedIdentityEndpoint(String msiEndpoin .useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; - return SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); + MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); + return new AccessToken(msiToken.getToken(), + msiToken.getExpiresAt().minus(options.getTokenRefreshOffset())); } finally { if (connection != null) { connection.disconnect(); @@ -695,7 +696,7 @@ public Mono authenticateToManagedIdentityEndpoint(String msiEndpoin * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateToIMDSEndpoint(TokenRequestContext request) { + public Mono authenticateToIMDSEndpoint(TokenRequestContext request) { String resource = ScopeUtil.scopesToResource(request.getScopes()); StringBuilder payload = new StringBuilder(); final int imdsUpgradeTimeInMs = 70 * 1000; @@ -732,8 +733,10 @@ public Mono authenticateToIMDSEndpoint(TokenRequestContext request) .useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; - return SERIALIZER_ADAPTER.deserialize(result, + MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); + return new AccessToken(msiToken.getToken(), + msiToken.getExpiresAt().minus(options.getTokenRefreshOffset())); } catch (IOException exception) { if (connection == null) { throw logger.logExceptionAsError(new RuntimeException( diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index 78c23ecddd4a..cf3685422e54 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java @@ -3,6 +3,7 @@ package com.azure.identity.implementation; +import com.azure.core.credential.TokenCredential; import com.azure.core.http.HttpClient; import com.azure.core.http.HttpPipeline; import com.azure.core.http.ProxyOptions; @@ -45,7 +46,7 @@ public final class IdentityClientOptions { private ProxyOptions proxyOptions; private HttpPipeline httpPipeline; private ExecutorService executorService; - private Duration tokenRefreshOffset = Duration.ofMinutes(2); + private Duration tokenRefreshOffset = TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET; private HttpClient httpClient; private boolean allowUnencryptedCache; private boolean sharedTokenCacheEnabled; diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java deleted file mode 100644 index adebac2dbe3f..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation; - -import com.azure.core.credential.AccessToken; - -import java.time.OffsetDateTime; - -/** - * Type representing authentication result from the azure-identity client. - */ -public class IdentityToken extends AccessToken { - /** - * Creates an identity token instance. - * - * @param token the token string. - * @param expiresAt the expiration time. - */ - public IdentityToken(String token, OffsetDateTime expiresAt) { - super(token, expiresAt.plusMinutes(2)); - } -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java index bdf353c4cf32..91424f509cc0 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java @@ -3,6 +3,7 @@ package com.azure.identity.implementation; +import com.azure.core.credential.AccessToken; import com.azure.core.util.logging.ClientLogger; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -16,7 +17,7 @@ /** * Type representing response from the local MSI token provider. */ -public final class MSIToken extends IdentityToken { +public final class MSIToken extends AccessToken { private static final OffsetDateTime EPOCH = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); @JsonProperty(value = "token_type") diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java index 4e6369e2911a..ba7d0fb1ca41 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java @@ -3,6 +3,7 @@ package com.azure.identity.implementation; +import com.azure.core.credential.AccessToken; import com.microsoft.aad.msal4j.IAccount; import com.microsoft.aad.msal4j.IAuthenticationResult; @@ -12,7 +13,7 @@ /** * Type representing authentication result from the MSAL (Microsoft Authentication Library). */ -public final class MsalToken extends IdentityToken { +public final class MsalToken extends AccessToken { private IAccount account; diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/util/TestUtils.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/util/TestUtils.java index 896d47c37bcd..661299e2be6e 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/util/TestUtils.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/util/TestUtils.java @@ -4,7 +4,6 @@ package com.azure.identity.util; import com.azure.core.credential.AccessToken; -import com.azure.identity.implementation.IdentityClientOptions; import com.azure.identity.implementation.MsalToken; import com.microsoft.aad.msal4j.IAccount; import com.microsoft.aad.msal4j.IAuthenticationResult; @@ -84,7 +83,7 @@ public Date expiresOnDate() { */ public static Mono getMockMsalToken(String accessToken, OffsetDateTime expiresOn) { return Mono.fromFuture(getMockAuthenticationResult(accessToken, expiresOn)) - .map(ar -> new MsalToken(ar, new IdentityClientOptions())); + .map(MsalToken::new); } /** From ca8b3a1777c51ced4556427fe11d31c2d77ef169 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 18:07:11 -0700 Subject: [PATCH 07/43] add a test for custom expiry --- .../core/credential/TokenCacheTests.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 1aa0689b8d3c..89a44c1a0231 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -79,6 +79,36 @@ public void testLongRunningWontOverflow() throws Exception { Assertions.assertTrue(refreshes.get() <= 11); } + @Test + public void testCustomExpiry() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + SimpleTokenCache cache = new SimpleTokenCache( + () -> incrementalRemoteGetTokenAsync(latency), + // 2s offset: token needs refreshing after 3s after 1st token comes back (which is just past second 1), + // so 5th token acquisition will require another call + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(2000)))); + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + + Flux.interval(Duration.ofSeconds(1)) + .take(5) + .flatMap(i -> Mono.just(OffsetDateTime.now())) + .flatMap(start -> cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + })) + .doOnComplete(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() > 2000); + Assertions.assertTrue(maxMillis.get() < 3000); // Big enough for any latency, small enough to make sure no get token is called twice + } + private Mono remoteGetTokenAsync(long delayInMillis) { return Mono.delay(Duration.ofMillis(delayInMillis)) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); From b61e2f4dd9ecb73c09611dd018a2724a64825c8d Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 18:09:47 -0700 Subject: [PATCH 08/43] Fix javadoc --- .../main/java/com/azure/core/credential/TokenCredential.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java index 62b00338f176..2ca883885c4c 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java @@ -26,6 +26,8 @@ public interface TokenCredential { /** * The duration before the actual token expiry to refresh the token. Default is 2 minutes. + * + * @return the current offset for token refresh */ default Duration getTokenRefreshOffset() { return DEFAULT_TOKEN_REFRESH_OFFSET; From 48083bc5707f6ba809ea6bb26bfd6fc431cdb4fe Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 18:27:14 -0700 Subject: [PATCH 09/43] Fix amqp test --- .../amqp/implementation/ClaimsBasedSecurityChannelTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java index 5a847b48b063..685de4005242 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java @@ -107,8 +107,7 @@ public void teardown() { @Test public void authorizesSasToken() { // Arrange - // Subtracting two minutes because the AccessToken does this internally. - final Date expectedDate = Date.from(validUntil.minusMinutes(2).toInstant()); + final Date expectedDate = Date.from(validUntil.toInstant()); final ClaimsBasedSecurityChannel cbsChannel = new ClaimsBasedSecurityChannel(Mono.just(requestResponseChannel), tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, options); From 2e57b827376c4e8f1874724f9eb4af7369179c9a Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 18:52:19 -0700 Subject: [PATCH 10/43] Use predicate over function --- .../java/com/azure/core/credential/SimpleTokenCache.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index d2cb192f8855..254b231c9369 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -10,7 +10,7 @@ import java.time.OffsetDateTime; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; /** @@ -24,7 +24,7 @@ public class SimpleTokenCache { private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; - private final Function tokenExpired; + private final Predicate tokenExpired; /** * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". @@ -42,7 +42,7 @@ public SimpleTokenCache(Supplier> tokenSupplier) { * @param tokenSupplier a method to get a new token * @param tokenExpired a method to check if the cached token is expired */ - public SimpleTokenCache(Supplier> tokenSupplier, Function tokenExpired) { + public SimpleTokenCache(Supplier> tokenSupplier, Predicate tokenExpired) { this.wip = new AtomicBoolean(false); this.tokenSupplier = tokenSupplier; this.tokenExpired = tokenExpired; @@ -53,7 +53,7 @@ public SimpleTokenCache(Supplier> tokenSupplier, Function getToken() { - if (cache != null && !tokenExpired.apply(cache)) { + if (cache != null && !tokenExpired.test(cache)) { return Mono.just(cache); } return Mono.defer(() -> { From ef0869a67343f2bb7fb78fc3a3ccf05fa26a3e15 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 19:55:23 -0700 Subject: [PATCH 11/43] Fix new test --- .../core/credential/TokenCacheTests.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 89a44c1a0231..0cb5179296d8 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -86,22 +86,25 @@ public void testCustomExpiry() throws Exception { () -> incrementalRemoteGetTokenAsync(latency), // 2s offset: token needs refreshing after 3s after 1st token comes back (which is just past second 1), // so 5th token acquisition will require another call - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(2000)))); + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(2)))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.interval(Duration.ofSeconds(1)) - .take(5) - .flatMap(i -> Mono.just(OffsetDateTime.now())) - .flatMap(start -> cache.getToken() - .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) - .doOnNext(millis -> { - if (millis > maxMillis.get()) { - maxMillis.set(millis); - } - })) - .doOnComplete(latch::countDown) + Flux.range(1, 5) + .concatMap(i -> Mono.delay(Duration.ofSeconds(1))) + .concatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + System.out.format("Thread: %s\tDuration: %smillis%n", + Thread.currentThread().getName(), Duration.between(start, OffsetDateTime.now()).toMillis()); + }); + }).doOnComplete(latch::countDown) .subscribe(); latch.await(); From a4d383e5880231b0fefc679c19dc1fbed9e4dca6 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 3 Jun 2020 21:59:35 -0700 Subject: [PATCH 12/43] Allow equals 2000 in new test --- .../test/java/com/azure/core/credential/TokenCacheTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 0cb5179296d8..1756f5d27686 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -101,14 +101,12 @@ public void testCustomExpiry() throws Exception { if (millis > maxMillis.get()) { maxMillis.set(millis); } - System.out.format("Thread: %s\tDuration: %smillis%n", - Thread.currentThread().getName(), Duration.between(start, OffsetDateTime.now()).toMillis()); }); }).doOnComplete(latch::countDown) .subscribe(); latch.await(); - Assertions.assertTrue(maxMillis.get() > 2000); + Assertions.assertTrue(maxMillis.get() >= 2000); Assertions.assertTrue(maxMillis.get() < 3000); // Big enough for any latency, small enough to make sure no get token is called twice } From 616d4b9e5d782e7a1f9ff227aba575de37785647 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 4 Jun 2020 00:58:35 -0700 Subject: [PATCH 13/43] make test safer --- .../test/java/com/azure/core/credential/TokenCacheTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 1756f5d27686..4bd76d0ed0b1 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -91,7 +91,7 @@ public void testCustomExpiry() throws Exception { CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.range(1, 5) + Flux.range(1, 6) .concatMap(i -> Mono.delay(Duration.ofSeconds(1))) .concatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); From a9f4089f55444c7a0b837c537d9b727075b8c7df Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 4 Jun 2020 10:13:16 -0700 Subject: [PATCH 14/43] Fix 2 minute offset in MSITests --- .../com/azure/identity/implementation/MSITokenTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java index 3284fc197e46..ec8e6a590ba6 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java @@ -10,7 +10,7 @@ import java.time.ZoneOffset; public class MSITokenTests { - private OffsetDateTime expected = OffsetDateTime.of(2020, 1, 10, 15, 1, 28, 0, ZoneOffset.UTC); + private OffsetDateTime expected = OffsetDateTime.of(2020, 1, 10, 15, 3, 28, 0, ZoneOffset.UTC); @Test public void canParseLong() { @@ -30,11 +30,11 @@ public void canParseDateTime12Hr() { Assert.assertEquals(expected.toEpochSecond(), token.getExpiresAt().toEpochSecond()); token = new MSIToken("fake_token", "12/20/2019 4:58:20 AM +00:00"); - expected = OffsetDateTime.of(2019, 12, 20, 4, 56, 20, 0, ZoneOffset.UTC); + expected = OffsetDateTime.of(2019, 12, 20, 4, 58, 20, 0, ZoneOffset.UTC); Assert.assertEquals(expected.toEpochSecond(), token.getExpiresAt().toEpochSecond()); token = new MSIToken("fake_token", "1/1/2020 0:00:00 PM +00:00"); - expected = OffsetDateTime.of(2020, 1, 1, 11, 58, 0, 0, ZoneOffset.UTC); + expected = OffsetDateTime.of(2020, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC); Assert.assertEquals(expected.toEpochSecond(), token.getExpiresAt().toEpochSecond()); } } From 268de3b3b9720265016a3a359415b6ca8d2b9025 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 4 Jun 2020 10:33:57 -0700 Subject: [PATCH 15/43] Checkstyle --- .../azure/identity/ChainedTokenCredential.java | 9 +++++---- .../identity/implementation/IdentityClient.java | 16 ++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java index f7852f4869b1..87cd747a6c54 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; /** @@ -26,7 +27,7 @@ @Immutable public class ChainedTokenCredential implements TokenCredential { private final Deque credentials; - private Duration tokenRefreshOffset; + private final AtomicReference tokenRefreshOffset; /** * Create an instance of chained token credential that aggregates a list of token @@ -34,7 +35,7 @@ public class ChainedTokenCredential implements TokenCredential { */ ChainedTokenCredential(Deque credentials) { this.credentials = credentials; - tokenRefreshOffset = TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET; + tokenRefreshOffset = new AtomicReference<>(TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET); } @Override @@ -44,7 +45,7 @@ public Mono getToken(TokenRequestContext request) { .flatMap(p -> p.getToken(request).onErrorResume(CredentialUnavailableException.class, t -> { exceptions.add(t); return Mono.empty(); - }).doOnNext(t -> tokenRefreshOffset = p.getTokenRefreshOffset()), 1) + }).doOnNext(t -> tokenRefreshOffset.set(p.getTokenRefreshOffset())), 1) .next() .switchIfEmpty(Mono.defer(() -> { @@ -67,6 +68,6 @@ public Mono getToken(TokenRequestContext request) { @Override public Duration getTokenRefreshOffset() { - return tokenRefreshOffset; + return tokenRefreshOffset.get(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 7a1652af23fa..1d30455a8a23 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -296,7 +296,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithAzureCli(TokenRequestContext request) { + public Mono authenticateWithAzureCli(TokenRequestContext request) { String azCommand = "az account get-access-token --output json --resource "; StringBuilder command = new StringBuilder(); @@ -312,7 +312,7 @@ public Mono authenticateWithAzureCli(Toke command.append(scopes); - com.azure.core.credential.AccessToken token = null; + AccessToken token = null; BufferedReader reader = null; try { String starter; @@ -399,7 +399,7 @@ public Mono authenticateWithAzureCli(Toke * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithConfidentialClient(TokenRequestContext request) { + public Mono authenticateWithConfidentialClient(TokenRequestContext request) { return Mono.fromFuture(() -> getConfidentialClientApplication().acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build())) .map(MsalToken::new); @@ -475,7 +475,7 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateWithConfidentialClientCache(TokenRequestContext request) { + public Mono authenticateWithConfidentialClientCache(TokenRequestContext request) { return Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())); @@ -484,7 +484,7 @@ public Mono authenticateWithConfidentialC } catch (MalformedURLException e) { return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } - }).map(ar -> (com.azure.core.credential.AccessToken) new MsalToken(ar)) + }).map(ar -> (AccessToken) new MsalToken(ar)) .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))); } @@ -648,8 +648,8 @@ public Mono authenticateWithSharedTokenCache(TokenRequestContext requ * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateToManagedIdentityEndpoint(String msiEndpoint, String msiSecret, - TokenRequestContext request) { + public Mono authenticateToManagedIdentityEndpoint(String msiEndpoint, String msiSecret, + TokenRequestContext request) { return Mono.fromCallable(() -> { String resource = ScopeUtil.scopesToResource(request.getScopes()); HttpURLConnection connection = null; @@ -696,7 +696,7 @@ public Mono authenticateToManagedIdentity * @param request the details of the token request * @return a Publisher that emits an AccessToken */ - public Mono authenticateToIMDSEndpoint(TokenRequestContext request) { + public Mono authenticateToIMDSEndpoint(TokenRequestContext request) { String resource = ScopeUtil.scopesToResource(request.getScopes()); StringBuilder payload = new StringBuilder(); final int imdsUpgradeTimeInMs = 70 * 1000; From fb8809d1b3f2964898e6013c6e023a9325f3334e Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 4 Jun 2020 15:36:53 -0700 Subject: [PATCH 16/43] Use unreleased_ prefix for azure core dependencies --- eng/versioning/version_client.txt | 6 ++++-- sdk/identity/azure-identity/pom.xml | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 9d54b988c055..a5e3d0f3739f 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -16,13 +16,13 @@ com.azure:azure-core-serializer-json-gson;1.0.0-beta.1;1.0.0-beta.2 com.azure:azure-core-serializer-json-jackson;1.0.0-beta.1;1.0.0-beta.2 com.azure:azure-core-test;1.2.1;1.3.0-beta.1 com.azure:azure-core-tracing-opentelemetry;1.0.0-beta.4;1.0.0-beta.5 -com.azure:azure-cosmos;4.0.1-beta.3;4.0.1-beta.4 +com.azure:azure-cosmos;4.0.1-beta.4;4.0.1-beta.5 com.azure:azure-cosmos-examples;4.0.1-beta.1;4.0.1-beta.1 com.azure:azure-cosmos-benchmark;4.0.1-beta.1;4.0.1-beta.1 -com.azure:azure-cosmos-table;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-data-appconfiguration;1.1.1;1.2.0-beta.1 com.azure:azure-data-schemaregistry;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-data-schemaregistry-avro;1.0.0-beta.1;1.0.0-beta.1 +com.azure:azure-data-tables;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-e2e;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-identity;1.0.6;1.1.0-beta.5 com.azure:azure-identity-perf;1.0.0-beta.1;1.0.0-beta.1 @@ -60,6 +60,8 @@ com.microsoft.azure:azure-spring-boot-metrics-starter;2.2.4;2.2.5-beta.1 # Format; # unreleased_:;dependency-version # note: The unreleased dependencies will not be manipulated with the automatic PR creation code. +unreleased_com.azure:azure-core;1.6.0-beta.1 +unreleased_com.azure:azure-core-http-netty;1.6.0-beta.1 # Released Beta dependencies: Copy the entry from above, prepend "beta_", remove the current # version and set the version to the released beta. Released beta dependencies are only valid diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 2931e6047a18..0e60b50634fe 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -27,7 +27,7 @@ com.azure azure-core - 1.6.0-beta.1 + 1.6.0-beta.1 com.microsoft.azure @@ -88,7 +88,7 @@ com.azure azure-core-http-netty - 1.6.0-beta.1 + 1.6.0-beta.1 test From 8632a8aa3baebccc18cf3ebf6a26819f902fa48f Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 11 Jun 2020 16:30:09 -0700 Subject: [PATCH 17/43] Use new logic for proactive token refresh --- .../core/credential/SimpleTokenCache.java | 93 ++++++++++-- .../core/credential/TokenCacheTests.java | 132 ++++++++++++++++-- 2 files changed, 197 insertions(+), 28 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 254b231c9369..6b6f4dfd11b6 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -3,11 +3,13 @@ package com.azure.core.credential; +import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.FluxSink; import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; +import java.time.Duration; import java.time.OffsetDateTime; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; @@ -17,14 +19,16 @@ * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { - private static final int REFRESH_TIMEOUT_SECONDS = 30; + private static final Duration REFRESH_TIMEOUT = Duration.ofSeconds(30); private final AtomicBoolean wip; - private AccessToken cache; + private volatile AccessToken cache; + private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; - private final Predicate tokenExpired; + private final Predicate shouldRefresh; + private final ClientLogger logger = new ClientLogger(SimpleTokenCache.class); /** * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". @@ -40,12 +44,12 @@ public SimpleTokenCache(Supplier> tokenSupplier) { * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". * * @param tokenSupplier a method to get a new token - * @param tokenExpired a method to check if the cached token is expired + * @param shouldRefresh a method to check if the cached token is expired */ - public SimpleTokenCache(Supplier> tokenSupplier, Predicate tokenExpired) { + public SimpleTokenCache(Supplier> tokenSupplier, Predicate shouldRefresh) { this.wip = new AtomicBoolean(false); this.tokenSupplier = tokenSupplier; - this.tokenExpired = tokenExpired; + this.shouldRefresh = shouldRefresh; } /** @@ -53,18 +57,77 @@ public SimpleTokenCache(Supplier> tokenSupplier, Predicate getToken() { - if (cache != null && !tokenExpired.test(cache)) { - return Mono.just(cache); - } - return Mono.defer(() -> { + try { if (!wip.getAndSet(true)) { - return tokenSupplier.get().doOnNext(ac -> cache = ac) - .doOnNext(sink::next) + OffsetDateTime now = OffsetDateTime.now(); + Mono tokenRefresh; + Mono fallback; + if (cache != null && !shouldRefresh.test(cache)) { + // fresh cache & no need to refresh + tokenRefresh = Mono.empty(); + fallback = Mono.just(cache); + } else if (cache == null || cache.isExpired()) { + // no token to use + if (now.isAfter(nextTokenRefresh)) { + // refresh immediately + tokenRefresh = Mono.defer(tokenSupplier); + } else { + // wait for timeout, then refresh + tokenRefresh = Mono.delay(Duration.between(now, nextTokenRefresh)) + .then(Mono.defer(tokenSupplier)); + } + // cache doesn't exist or expired, no fallback + fallback = Mono.empty(); + } else { + // token available, but close to expiry + if (now.isAfter(nextTokenRefresh)) { + // refresh immediately + tokenRefresh = Mono.defer(tokenSupplier); + } else { + // still in timeout, do not refresh + tokenRefresh = Mono.empty(); + } + // cache hasn't expired, ignore refresh error this time + fallback = Mono.just(cache); + } + return tokenRefresh + .doOnNext(accessToken -> { + logger.info(refreshLog(cache, now, "Acquired a new access token")); + sink.next(accessToken); + cache = accessToken; + nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); + }) + .onErrorResume(err -> { + logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); + nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); + return fallback.switchIfEmpty(Mono.error(err)); + }) + .switchIfEmpty(fallback) .doOnError(sink::error) - .doOnTerminate(() -> wip.set(false)); + .doOnTerminate(() -> { + wip.set(false); + }); } else { - return emitterProcessor.next(); + return emitterProcessor.next().filter(t -> !t.isExpired()); + } + } catch (Throwable t) { + return Mono.error(t); + } + } + + private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { + StringBuilder info = new StringBuilder(log); + if (cache == null) { + info.append("."); + } else { + Duration tte = Duration.between(now, cache.getExpiresAt()); + info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") + .append(tte.isNegative() ? "after" : "before").append(" expiry. ") + .append("Retry may be attempted after ").append(REFRESH_TIMEOUT.getSeconds()).append(" seconds."); + if (!tte.isNegative()) { + info.append(" The token currently cached will be used."); } - }); + } + return info.toString(); } } diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 4bd76d0ed0b1..b7b72dd8f2e9 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -49,7 +49,7 @@ public void testOnlyOneThreadRefreshesToken() throws Exception { } @Test - public void testLongRunningWontOverflow() throws Exception { + public void testMultipleThreadsWaitForTimeout() throws Exception { AtomicLong refreshes = new AtomicLong(0); // token expires on creation. Run this 100 times to simulate running the application a long time @@ -68,32 +68,27 @@ public void testLongRunningWontOverflow() throws Exception { .flatMap(start -> cache.getToken() .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) .doOnNext(millis -> { -// System.out.format("Thread: %s\tDuration: %smillis%n", -// Thread.currentThread().getName(), Duration.between(start, OffsetDateTime.now()).toMillis()); }))) .doOnComplete(latch::countDown) .subscribe(); latch.await(); - // At most 10 requests should do actual token acquisition, use 11 for safe - Assertions.assertTrue(refreshes.get() <= 11); + Assertions.assertEquals(2, refreshes.get()); } @Test - public void testCustomExpiry() throws Exception { + public void testProactiveRefreshBeforeExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( - () -> incrementalRemoteGetTokenAsync(latency), - // 2s offset: token needs refreshing after 3s after 1st token comes back (which is just past second 1), - // so 5th token acquisition will require another call - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(2)))); + () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(28)))); // refresh at second 32, just past REFRESH_TIMEOUT CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.range(1, 6) - .concatMap(i -> Mono.delay(Duration.ofSeconds(1))) - .concatMap(i -> { + Flux.interval(Duration.ofSeconds(2)) + .take(20) // 38 seconds after first token, making sure of a refresh + .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) @@ -110,6 +105,99 @@ public void testCustomExpiry() throws Exception { Assertions.assertTrue(maxMillis.get() < 3000); // Big enough for any latency, small enough to make sure no get token is called twice } + @Test + public void testRefreshAfterExpiry() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 15 * 1000), + t -> OffsetDateTime.now().isAfter(t.getExpiresAt())); // refresh at second 30 because of REFRESH_TIMEOUT + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + + Flux.interval(Duration.ofSeconds(2)) + .take(10) // 38 seconds after first token, making sure of a refresh + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }); + }).doOnComplete(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() >= 15000); + } + + @Test + public void testProactiveRefreshError() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + AtomicInteger tryCount = new AtomicInteger(0); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(28)))); // refresh at second 32, just past REFRESH_TIMEOUT + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + AtomicInteger errorCount = new AtomicInteger(0); + + Flux.interval(Duration.ofSeconds(2)) + .take(32) // 64 seconds after first token, making sure of a refresh + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }) + .doOnError(t -> errorCount.incrementAndGet()); + }).doOnTerminate(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() >= 1000); + Assertions.assertTrue(maxMillis.get() < 2000); // Big enough for any latency, small enough to make sure no get token is called twice + Assertions.assertEquals(1, errorCount.get()); // Only the error after expiresAt will be propagated + } + + @Test + public void testProactiveRefreshErrorTimeout() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + AtomicInteger tryCount = new AtomicInteger(0); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), + t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(28)))); // refresh at second 32, just past REFRESH_TIMEOUT + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + AtomicInteger errorCount = new AtomicInteger(0); + + Flux.interval(Duration.ofSeconds(2)) + .take(32) // 64 seconds after first token, making sure of a refresh + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }) + .doOnError(t -> errorCount.incrementAndGet()); + }).doOnTerminate(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() >= 3000); + Assertions.assertEquals(0, errorCount.get()); // Only the error after expiresAt will be propagated + } + private Mono remoteGetTokenAsync(long delayInMillis) { return Mono.delay(Duration.ofMillis(delayInMillis)) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); @@ -126,6 +214,24 @@ private Mono incrementalRemoteGetTokenAsync(AtomicInteger latency) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); } + private Mono remoteGetTokenWithTemporaryError(long delayInMillis, long validityInMillis, int errorAt, AtomicInteger tryCount) { + if (tryCount.incrementAndGet() == errorAt) { + return Mono.error(new RuntimeException("Expected error")); + } else { + return Mono.delay(Duration.ofMillis(delayInMillis)) + .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)), validityInMillis)); + } + } + + private Mono remoteGetTokenWithPersistentError(long delayInMillis, long validityInMillis, int errorAfter, AtomicInteger tryCount) { + if (tryCount.incrementAndGet() >= errorAfter) { + return Mono.error(new RuntimeException("Expected error")); + } else { + return Mono.delay(Duration.ofMillis(delayInMillis)) + .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)), validityInMillis)); + } + } + private static class Token extends AccessToken { private String token; private OffsetDateTime expiry; From 54033d14865f423e70224bd9be36669ff423ac16 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 17 Jun 2020 15:33:41 -0700 Subject: [PATCH 18/43] Use unreleased azure-core versions in identity --- sdk/identity/azure-identity/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 5fc18abee1d8..6ada6d7f6630 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -88,7 +88,7 @@ com.azure azure-core-http-netty - 1.5.2 + 1.6.0-beta.1 test From ec48f39a5406ca5900ddd21438413cbdb6a559ab Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 18 Jun 2020 16:41:53 -0700 Subject: [PATCH 19/43] Use TokenRefreshOptions for TokenCredential --- .../core/credential/TokenCredential.java | 13 +--- .../core/credential/TokenRefreshOptions.java | 63 +++++++++++++++++++ .../identity/AuthorizationCodeCredential.java | 8 +++ .../azure/identity/AzureCliCredential.java | 17 +++-- .../identity/ChainedTokenCredential.java | 13 ++-- .../identity/ClientCertificateCredential.java | 6 +- .../identity/ClientSecretCredential.java | 6 +- .../azure/identity/CredentialBuilderBase.java | 13 ++++ .../identity/DefaultAzureCredential.java | 6 +- .../azure/identity/DeviceCodeCredential.java | 6 +- .../azure/identity/EnvironmentCredential.java | 7 +-- .../azure/identity/IntelliJCredential.java | 6 +- .../InteractiveBrowserCredential.java | 8 +++ .../identity/ManagedIdentityCredential.java | 7 +-- .../identity/SharedTokenCacheCredential.java | 6 +- .../identity/UsernamePasswordCredential.java | 6 +- .../identity/VisualStudioCodeCredential.java | 6 +- .../implementation/IdentityClientOptions.java | 26 ++++++-- 18 files changed, 158 insertions(+), 65 deletions(-) create mode 100644 sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java index 2ca883885c4c..0408548dec65 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java @@ -5,18 +5,11 @@ import reactor.core.publisher.Mono; -import java.time.Duration; - /** * The interface for credentials that can provide a token. */ @FunctionalInterface public interface TokenCredential { - /** - * The default duration before the actual token expiry to refresh the token. The value is 2 minutes. - */ - Duration DEFAULT_TOKEN_REFRESH_OFFSET = Duration.ofMinutes(2); - /** * Asynchronously get a token for a given resource/audience. * @param request the details of the token request @@ -25,11 +18,11 @@ public interface TokenCredential { Mono getToken(TokenRequestContext request); /** - * The duration before the actual token expiry to refresh the token. Default is 2 minutes. + * The options to configure the token refresh behavior. * * @return the current offset for token refresh */ - default Duration getTokenRefreshOffset() { - return DEFAULT_TOKEN_REFRESH_OFFSET; + default TokenRefreshOptions getTokenRefreshOptions() { + return new TokenRefreshOptions(); } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java new file mode 100644 index 000000000000..452f08d7f3b0 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.credential; + +import java.time.Duration; + +/** + * The options to configure the token refresh behavior. + */ +public class TokenRefreshOptions { + private static final Duration DEFAULT_REFRESH_TIMEOUT = Duration.ofSeconds(30); + private static final Duration DEFAULT_REFRESH_OFFSET = Duration.ofMinutes(2); + + private Duration tokenRefreshTimeout = DEFAULT_REFRESH_TIMEOUT; + private Duration tokenRefreshOffset = DEFAULT_REFRESH_OFFSET; + + /** + * Returns a Duration value representing the amount of time to wait between token refreshes. This is to prevent + * sending too many requests to the authentication service. + * + * @return the duration value representing the amount of time to wait between token refreshes + */ + public Duration getTokenRefreshTimeout() { + return tokenRefreshTimeout; + } + + /** + * Specifies a Duration value representing the amount of time to wait between token refreshes. This is to prevent + * sending too many requests to the authentication service. + * + * @param tokenRefreshTimeout the duration value representing the amount of time to wait between token refreshes + * @return TokenRefreshOptions + */ + public TokenRefreshOptions setTokenRefreshTimeout(Duration tokenRefreshTimeout) { + this.tokenRefreshTimeout = tokenRefreshTimeout; + return this; + } + + /** + * Returns a Duration value representing the amount of time to subtract from the token expiry time, whereupon + * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the + * token. + * + * @return the duration value representing the amount of time to subtract from the token expiry time + */ + public Duration getTokenRefreshOffset() { + return tokenRefreshOffset; + } + + /** + * Specifies the Duration value representing the amount of time to subtract from the token expiry time, whereupon + * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the + * token. + * + * @param tokenRefreshOffset the duration representing the amount of time to subtract from the token expiry time + * @return TokenRefreshOptions + */ + public TokenRefreshOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { + this.tokenRefreshOffset = tokenRefreshOffset; + return this; + } +} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java index b5b77b895e83..b8da00e3c491 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.identity.implementation.IdentityClient; import com.azure.identity.implementation.IdentityClientBuilder; @@ -25,6 +26,7 @@ public class AuthorizationCodeCredential implements TokenCredential { private final String authCode; private final URI redirectUri; private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; private final AtomicReference cachedToken; /** @@ -43,6 +45,7 @@ public class AuthorizationCodeCredential implements TokenCredential { .clientId(clientId) .identityClientOptions(identityClientOptions) .build(); + this.identityClientOptions = identityClientOptions; this.cachedToken = new AtomicReference<>(); this.authCode = authCode; this.redirectUri = redirectUri; @@ -66,4 +69,9 @@ public Mono getToken(TokenRequestContext request) { return msalToken; }); } + + @Override + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java index 7568f497e958..9143096a465d 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureCliCredential.java @@ -3,18 +3,15 @@ package com.azure.identity; -import com.azure.identity.implementation.IdentityClient; -import com.azure.identity.implementation.IdentityClientBuilder; -import com.azure.identity.implementation.IdentityClientOptions; - -import reactor.core.publisher.Mono; - import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; - -import java.time.Duration; +import com.azure.identity.implementation.IdentityClient; +import com.azure.identity.implementation.IdentityClientBuilder; +import com.azure.identity.implementation.IdentityClientOptions; +import reactor.core.publisher.Mono; /** * A credential provider that provides token credentials based on Azure CLI @@ -40,7 +37,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java index 87cd747a6c54..5eb5755450f9 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java @@ -6,15 +6,14 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.ArrayList; import java.util.Deque; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; /** @@ -27,7 +26,7 @@ @Immutable public class ChainedTokenCredential implements TokenCredential { private final Deque credentials; - private final AtomicReference tokenRefreshOffset; + private volatile TokenRefreshOptions tokenRefreshOptions; /** * Create an instance of chained token credential that aggregates a list of token @@ -35,7 +34,7 @@ public class ChainedTokenCredential implements TokenCredential { */ ChainedTokenCredential(Deque credentials) { this.credentials = credentials; - tokenRefreshOffset = new AtomicReference<>(TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET); + tokenRefreshOptions = new TokenRefreshOptions(); } @Override @@ -45,7 +44,7 @@ public Mono getToken(TokenRequestContext request) { .flatMap(p -> p.getToken(request).onErrorResume(CredentialUnavailableException.class, t -> { exceptions.add(t); return Mono.empty(); - }).doOnNext(t -> tokenRefreshOffset.set(p.getTokenRefreshOffset())), 1) + }).doOnNext(t -> tokenRefreshOptions = p.getTokenRefreshOptions()), 1) .next() .switchIfEmpty(Mono.defer(() -> { @@ -67,7 +66,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return tokenRefreshOffset.get(); + public TokenRefreshOptions getTokenRefreshOptions() { + return tokenRefreshOptions; } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java index 20ebba2d8110..620a69e04a34 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientCertificateCredential.java @@ -6,13 +6,13 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.identity.implementation.IdentityClient; import com.azure.identity.implementation.IdentityClientBuilder; import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.Objects; /** @@ -58,7 +58,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java index 77c911402a98..26240a20d40c 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ClientSecretCredential.java @@ -6,13 +6,13 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.identity.implementation.IdentityClient; import com.azure.identity.implementation.IdentityClientBuilder; import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.Objects; /** @@ -58,7 +58,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java index 13c76e29c1be..b3fc1671c9e3 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java @@ -106,4 +106,17 @@ public T tokenRefreshOffset(Duration tokenRefreshOffset) { this.identityClientOptions.setTokenRefreshOffset(tokenRefreshOffset); return (T) this; } + + /** + * Specifies a Duration value representing the amount of time to wait between token refreshes. This is to prevent + * sending too many requests to the authentication service. + * + * @param tokenRefreshTimeout the duration value representing the amount of time to wait between token refreshes + * @return An updated instance of this builder with the token refresh offset set as specified. + */ + @SuppressWarnings("unchecked") + public T setTokenRefreshTimeout(Duration tokenRefreshTimeout) { + this.identityClientOptions.setTokenRefreshTimeout(tokenRefreshTimeout); + return (T) this; + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java index 89a8e66ea4bf..f92e4c1cfea1 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredential.java @@ -5,9 +5,9 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.identity.implementation.IdentityClientOptions; -import java.time.Duration; import java.util.ArrayDeque; /** @@ -43,7 +43,7 @@ public final class DefaultAzureCredential extends ChainedTokenCredential { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java index f999bf8b2f2f..7e0806562a23 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.logging.ClientLogger; import com.azure.identity.implementation.IdentityClient; @@ -15,7 +16,6 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -128,7 +128,7 @@ private MsalToken updateCache(MsalToken msalToken) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java index aefe398a619c..d35ff0334ab9 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java @@ -6,14 +6,13 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.Configuration; import com.azure.core.util.logging.ClientLogger; import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; -import java.time.Duration; - /** * A credential provider that provides token credentials based on environment variables. The environment variables * expected are: @@ -86,8 +85,8 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } private boolean verifyNotNull(String... configs) { diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java index edc5f9d8a86e..13fc5d5cf042 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/IntelliJCredential.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.CoreUtils; import com.azure.identity.implementation.IdentityClient; @@ -16,7 +17,6 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; /** @@ -92,7 +92,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java index 5a7b2e643292..d341891c1403 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.logging.ClientLogger; import com.azure.identity.implementation.IdentityClient; @@ -30,6 +31,7 @@ public class InteractiveBrowserCredential implements TokenCredential { private final int port; private final IdentityClient identityClient; + private final IdentityClientOptions identityClientOptions; private final AtomicReference cachedToken; private final boolean automaticAuthentication; private final String authorityHost; @@ -54,6 +56,7 @@ public class InteractiveBrowserCredential implements TokenCredential { .clientId(clientId) .identityClientOptions(identityClientOptions) .build(); + this.identityClientOptions = identityClientOptions; cachedToken = new AtomicReference<>(); this.authorityHost = identityClientOptions.getAuthorityHost(); this.automaticAuthentication = automaticAuthentication; @@ -112,6 +115,11 @@ public Mono authenticate() { return authenticate(new TokenRequestContext().addScopes(defaultScope)); } + @Override + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); + } + private MsalToken updateCache(MsalToken msalToken) { cachedToken.set( new MsalAuthenticationAccount( diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java index 6d9570c92b4b..61e782a9b10f 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.Configuration; import com.azure.identity.implementation.IdentityClient; @@ -13,8 +14,6 @@ import com.azure.identity.implementation.IdentityClientOptions; import reactor.core.publisher.Mono; -import java.time.Duration; - /** * The base class for Managed Service Identity token based credentials. */ @@ -63,7 +62,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java index 9b972eafcd4f..5b96542edc4d 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/SharedTokenCacheCredential.java @@ -5,6 +5,7 @@ import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.Configuration; import com.azure.identity.implementation.IdentityClient; @@ -13,7 +14,6 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; /** @@ -88,7 +88,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java index d2b4267a8596..db06f8757c21 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.logging.ClientLogger; import com.azure.identity.implementation.IdentityClient; @@ -15,7 +16,6 @@ import com.azure.identity.implementation.MsalToken; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -109,7 +109,7 @@ private MsalToken updateCache(MsalToken msalToken) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java index 7620efdb0f27..db76b87e4ae4 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java @@ -5,6 +5,7 @@ import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.credential.TokenRequestContext; import com.azure.core.util.CoreUtils; import com.azure.identity.implementation.IdentityClient; @@ -14,7 +15,6 @@ import com.azure.identity.implementation.VisualStudioCacheAccessor; import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -84,7 +84,7 @@ public Mono getToken(TokenRequestContext request) { } @Override - public Duration getTokenRefreshOffset() { - return identityClientOptions.getTokenRefreshOffset(); + public TokenRefreshOptions getTokenRefreshOptions() { + return identityClientOptions.getTokenRefreshOptions(); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index 2587c2371d3c..960633e3dd08 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java @@ -3,7 +3,7 @@ package com.azure.identity.implementation; -import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRefreshOptions; import com.azure.core.http.HttpClient; import com.azure.core.http.HttpPipeline; import com.azure.core.http.ProxyOptions; @@ -47,7 +47,7 @@ public final class IdentityClientOptions { private ProxyOptions proxyOptions; private HttpPipeline httpPipeline; private ExecutorService executorService; - private Duration tokenRefreshOffset = TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET; + private TokenRefreshOptions tokenRefreshOptions = new TokenRefreshOptions(); private HttpClient httpClient; private boolean allowUnencryptedCache; private boolean sharedTokenCacheEnabled; @@ -187,10 +187,10 @@ public ExecutorService getExecutorService() { } /** - * @return how long before the actual token expiry to refresh the token. + * @return the options to configure the token refresh behavior. */ - public Duration getTokenRefreshOffset() { - return tokenRefreshOffset; + public TokenRefreshOptions getTokenRefreshOptions() { + return tokenRefreshOptions; } /** @@ -207,7 +207,21 @@ public Duration getTokenRefreshOffset() { */ public IdentityClientOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { Objects.requireNonNull(tokenRefreshOffset, "The token refresh offset cannot be null."); - this.tokenRefreshOffset = tokenRefreshOffset; + this.tokenRefreshOptions.setTokenRefreshOffset(tokenRefreshOffset); + return this; + } + + /** + * Specifies a Duration value representing the amount of time to wait between token refreshes. This is to prevent + * sending too many requests to the authentication service. + * + * @param tokenRefreshTimeout the duration value representing the amount of time to wait between token refreshes + * @return IdentityClientOptions + * @throws NullPointerException If {@code tokenRefreshOffset} is null. + */ + public IdentityClientOptions setTokenRefreshTimeout(Duration tokenRefreshTimeout) { + Objects.requireNonNull(tokenRefreshTimeout, "The token refresh timeout cannot be null."); + this.tokenRefreshOptions.setTokenRefreshTimeout(tokenRefreshTimeout); return this; } From e58d48545740504d664b477db97dd29973870849 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 18 Jun 2020 16:51:06 -0700 Subject: [PATCH 20/43] Use token refresh options in simple token cache --- .../core/credential/SimpleTokenCache.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 6b6f4dfd11b6..fded44171fa6 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -19,8 +19,6 @@ * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { - private static final Duration REFRESH_TIMEOUT = Duration.ofSeconds(30); - private final AtomicBoolean wip; private volatile AccessToken cache; private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); @@ -28,6 +26,7 @@ public class SimpleTokenCache { private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; private final Predicate shouldRefresh; + private final Duration refreshTimeout; private final ClientLogger logger = new ClientLogger(SimpleTokenCache.class); /** @@ -36,20 +35,21 @@ public class SimpleTokenCache { * @param tokenSupplier a method to get a new token */ public SimpleTokenCache(Supplier> tokenSupplier) { - this(tokenSupplier, - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(TokenCredential.DEFAULT_TOKEN_REFRESH_OFFSET))); + this(tokenSupplier, new TokenRefreshOptions()); } /** * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". * * @param tokenSupplier a method to get a new token - * @param shouldRefresh a method to check if the cached token is expired + * @param tokenRefreshOptions the options to configure the token refresh behavior */ - public SimpleTokenCache(Supplier> tokenSupplier, Predicate shouldRefresh) { + public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshOptions tokenRefreshOptions) { this.wip = new AtomicBoolean(false); this.tokenSupplier = tokenSupplier; - this.shouldRefresh = shouldRefresh; + this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() + .minus(tokenRefreshOptions.getTokenRefreshOffset())); + this.refreshTimeout = tokenRefreshOptions.getTokenRefreshTimeout(); } /** @@ -95,11 +95,11 @@ public Mono getToken() { logger.info(refreshLog(cache, now, "Acquired a new access token")); sink.next(accessToken); cache = accessToken; - nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); + nextTokenRefresh = OffsetDateTime.now().plus(refreshTimeout); }) .onErrorResume(err -> { logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); - nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); + nextTokenRefresh = OffsetDateTime.now().plus(refreshTimeout); return fallback.switchIfEmpty(Mono.error(err)); }) .switchIfEmpty(fallback) @@ -123,7 +123,7 @@ private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { Duration tte = Duration.between(now, cache.getExpiresAt()); info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") .append(tte.isNegative() ? "after" : "before").append(" expiry. ") - .append("Retry may be attempted after ").append(REFRESH_TIMEOUT.getSeconds()).append(" seconds."); + .append("Retry may be attempted after ").append(refreshTimeout.getSeconds()).append(" seconds."); if (!tte.isNegative()) { info.append(" The token currently cached will be used."); } From 8ebb8294970f2d55961e3fccf7ee6b9f80f0ca31 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 18 Jun 2020 23:29:17 -0700 Subject: [PATCH 21/43] Make tests run faster with configurable refresh timeout --- .../BearerTokenAuthenticationPolicy.java | 3 +- .../core/credential/TokenCacheTests.java | 34 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index ad184e6841de..ae9a4616c975 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -11,7 +11,6 @@ import com.azure.core.http.HttpResponse; import reactor.core.publisher.Mono; -import java.time.OffsetDateTime; import java.util.Objects; /** @@ -39,7 +38,7 @@ public BearerTokenAuthenticationPolicy(TokenCredential credential, String... sco this.credential = credential; this.scopes = scopes; this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes)), - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(credential.getTokenRefreshOffset()))); + credential.getTokenRefreshOptions()); } @Override diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index b7b72dd8f2e9..3376f6a2f7e3 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -56,7 +56,7 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { SimpleTokenCache cache = new SimpleTokenCache(() -> { refreshes.incrementAndGet(); return remoteGetTokenThatExpiresSoonAsync(1000, 0); - }); + }, new TokenRefreshOptions().setTokenRefreshTimeout(Duration.ZERO)); CountDownLatch latch = new CountDownLatch(1); @@ -73,7 +73,7 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { .subscribe(); latch.await(); - Assertions.assertEquals(2, refreshes.get()); + Assertions.assertTrue(refreshes.get() <= 11); } @Test @@ -81,13 +81,13 @@ public void testProactiveRefreshBeforeExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(28)))); // refresh at second 32, just past REFRESH_TIMEOUT + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(58)).setTokenRefreshTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.interval(Duration.ofSeconds(2)) - .take(20) // 38 seconds after first token, making sure of a refresh + Flux.interval(Duration.ofSeconds(1)) + .take(5) .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -109,14 +109,14 @@ public void testProactiveRefreshBeforeExpiry() throws Exception { public void testRefreshAfterExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 15 * 1000), - t -> OffsetDateTime.now().isAfter(t.getExpiresAt())); // refresh at second 30 because of REFRESH_TIMEOUT + () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 1000), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshTimeout(Duration.ofSeconds(5))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.interval(Duration.ofSeconds(2)) - .take(10) // 38 seconds after first token, making sure of a refresh + Flux.interval(Duration.ofSeconds(1)) + .take(4) // 38 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -130,7 +130,7 @@ public void testRefreshAfterExpiry() throws Exception { .subscribe(); latch.await(); - Assertions.assertTrue(maxMillis.get() >= 15000); + Assertions.assertTrue(maxMillis.get() >= 5000); } @Test @@ -138,15 +138,15 @@ public void testProactiveRefreshError() throws Exception { AtomicInteger latency = new AtomicInteger(1); AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(28)))); // refresh at second 32, just past REFRESH_TIMEOUT + () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); AtomicInteger errorCount = new AtomicInteger(0); - Flux.interval(Duration.ofSeconds(2)) - .take(32) // 64 seconds after first token, making sure of a refresh + Flux.interval(Duration.ofSeconds(1)) + .take(6) // 64 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -171,15 +171,15 @@ public void testProactiveRefreshErrorTimeout() throws Exception { AtomicInteger latency = new AtomicInteger(1); AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), - t -> OffsetDateTime.now().isAfter(t.getExpiresAt().minus(Duration.ofSeconds(28)))); // refresh at second 32, just past REFRESH_TIMEOUT + () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); AtomicInteger errorCount = new AtomicInteger(0); Flux.interval(Duration.ofSeconds(2)) - .take(32) // 64 seconds after first token, making sure of a refresh + .take(6) // 64 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() From f9d64bda9391f8cf85dd5e7ad0d9c84c8f905f8a Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 19 Jun 2020 11:09:04 -0700 Subject: [PATCH 22/43] Fix build issues in IdentityClient --- .../identity/implementation/IdentityClient.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 965c79a7add7..8d21ac5649f4 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -456,7 +456,8 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } }).map(MsalToken::new) - .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset()))) + .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus( + options.getTokenRefreshOptions().getTokenRefreshOffset()))) .switchIfEmpty(Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())).forceRefresh(true); @@ -488,7 +489,8 @@ public Mono authenticateWithConfidentialClientCache(TokenRequestCon return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); } }).map(ar -> (AccessToken) new MsalToken(ar)) - .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(options.getTokenRefreshOffset())))); + .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus( + options.getTokenRefreshOptions().getTokenRefreshOffset())))); } /** @@ -688,8 +690,7 @@ public Mono authenticateToManagedIdentityEndpoint(String msiEndpoin String result = s.hasNext() ? s.next() : ""; MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new AccessToken(msiToken.getToken(), - msiToken.getExpiresAt().minus(options.getTokenRefreshOffset())); + return new AccessToken(msiToken.getToken(), msiToken.getExpiresAt()); } finally { if (connection != null) { connection.disconnect(); @@ -743,8 +744,7 @@ public Mono authenticateToIMDSEndpoint(TokenRequestContext request) MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new AccessToken(msiToken.getToken(), - msiToken.getExpiresAt().minus(options.getTokenRefreshOffset())); + return new AccessToken(msiToken.getToken(), msiToken.getExpiresAt()); } catch (IOException exception) { if (connection == null) { throw logger.logExceptionAsError(new RuntimeException( From 3013a21b7bd89f0135d901d4d83c53caada7ac22 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 19 Jun 2020 14:28:36 -0700 Subject: [PATCH 23/43] Fix token cache test --- .../java/com/azure/core/credential/TokenCacheTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 3376f6a2f7e3..c3379ffa8559 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -171,15 +171,15 @@ public void testProactiveRefreshErrorTimeout() throws Exception { AtomicInteger latency = new AtomicInteger(1); AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), + () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 4 * 1000, 2, tryCount), new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); AtomicInteger errorCount = new AtomicInteger(0); - Flux.interval(Duration.ofSeconds(2)) - .take(6) // 64 seconds after first token, making sure of a refresh + Flux.interval(Duration.ofSeconds(1)) + .take(8) // 64 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() From 3741805f8ee422601ddf54f26e312f475d0e18b1 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 19 Jun 2020 15:35:04 -0700 Subject: [PATCH 24/43] Refname tokenRefreshTimeout to tokenRefreshRetryTimeout --- .../core/credential/SimpleTokenCache.java | 10 ++++---- .../core/credential/TokenRefreshOptions.java | 24 +++++++++---------- .../core/credential/TokenCacheTests.java | 10 ++++---- .../azure/identity/CredentialBuilderBase.java | 10 ++++---- .../implementation/IdentityClientOptions.java | 14 +++++------ 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index fded44171fa6..2da9f0845006 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -26,7 +26,7 @@ public class SimpleTokenCache { private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; private final Predicate shouldRefresh; - private final Duration refreshTimeout; + private final Duration refreshRetryTimeout; private final ClientLogger logger = new ClientLogger(SimpleTokenCache.class); /** @@ -49,7 +49,7 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO this.tokenSupplier = tokenSupplier; this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() .minus(tokenRefreshOptions.getTokenRefreshOffset())); - this.refreshTimeout = tokenRefreshOptions.getTokenRefreshTimeout(); + this.refreshRetryTimeout = tokenRefreshOptions.getTokenRefreshRetryTimeout(); } /** @@ -95,11 +95,11 @@ public Mono getToken() { logger.info(refreshLog(cache, now, "Acquired a new access token")); sink.next(accessToken); cache = accessToken; - nextTokenRefresh = OffsetDateTime.now().plus(refreshTimeout); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); }) .onErrorResume(err -> { logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); - nextTokenRefresh = OffsetDateTime.now().plus(refreshTimeout); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); return fallback.switchIfEmpty(Mono.error(err)); }) .switchIfEmpty(fallback) @@ -123,7 +123,7 @@ private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { Duration tte = Duration.between(now, cache.getExpiresAt()); info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") .append(tte.isNegative() ? "after" : "before").append(" expiry. ") - .append("Retry may be attempted after ").append(refreshTimeout.getSeconds()).append(" seconds."); + .append("Retry may be attempted after ").append(refreshRetryTimeout.getSeconds()).append(" seconds."); if (!tte.isNegative()) { info.append(" The token currently cached will be used."); } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java index 452f08d7f3b0..fd566916d351 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -9,31 +9,31 @@ * The options to configure the token refresh behavior. */ public class TokenRefreshOptions { - private static final Duration DEFAULT_REFRESH_TIMEOUT = Duration.ofSeconds(30); + private static final Duration DEFAULT_REFRESH_RETRY_TIMEOUT = Duration.ofSeconds(30); private static final Duration DEFAULT_REFRESH_OFFSET = Duration.ofMinutes(2); - private Duration tokenRefreshTimeout = DEFAULT_REFRESH_TIMEOUT; + private Duration tokenRefreshRetryTimeout = DEFAULT_REFRESH_RETRY_TIMEOUT; private Duration tokenRefreshOffset = DEFAULT_REFRESH_OFFSET; /** - * Returns a Duration value representing the amount of time to wait between token refreshes. This is to prevent - * sending too many requests to the authentication service. + * Returns a Duration value representing the amount of time to wait before retrying a token refresh. This is to + * prevent sending too many requests to the authentication service. * - * @return the duration value representing the amount of time to wait between token refreshes + * @return the duration value representing the amount of time to wait before retrying a token refresh */ - public Duration getTokenRefreshTimeout() { - return tokenRefreshTimeout; + public Duration getTokenRefreshRetryTimeout() { + return tokenRefreshRetryTimeout; } /** - * Specifies a Duration value representing the amount of time to wait between token refreshes. This is to prevent - * sending too many requests to the authentication service. + * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to + * prevent sending too many requests to the authentication service. * - * @param tokenRefreshTimeout the duration value representing the amount of time to wait between token refreshes + * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh * @return TokenRefreshOptions */ - public TokenRefreshOptions setTokenRefreshTimeout(Duration tokenRefreshTimeout) { - this.tokenRefreshTimeout = tokenRefreshTimeout; + public TokenRefreshOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { + this.tokenRefreshRetryTimeout = tokenRefreshRetryTimeout; return this; } diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index c3379ffa8559..a2f3edaeb1b6 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -56,7 +56,7 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { SimpleTokenCache cache = new SimpleTokenCache(() -> { refreshes.incrementAndGet(); return remoteGetTokenThatExpiresSoonAsync(1000, 0); - }, new TokenRefreshOptions().setTokenRefreshTimeout(Duration.ZERO)); + }, new TokenRefreshOptions().setTokenRefreshRetryTimeout(Duration.ZERO)); CountDownLatch latch = new CountDownLatch(1); @@ -81,7 +81,7 @@ public void testProactiveRefreshBeforeExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(58)).setTokenRefreshTimeout(Duration.ofSeconds(1))); + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(58)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -110,7 +110,7 @@ public void testRefreshAfterExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 1000), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshTimeout(Duration.ofSeconds(5))); + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshRetryTimeout(Duration.ofSeconds(5))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -139,7 +139,7 @@ public void testProactiveRefreshError() throws Exception { AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshTimeout(Duration.ofSeconds(1))); + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -172,7 +172,7 @@ public void testProactiveRefreshErrorTimeout() throws Exception { AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 4 * 1000, 2, tryCount), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshTimeout(Duration.ofSeconds(1))); + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java index b3fc1671c9e3..b76772ab86c7 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java @@ -108,15 +108,15 @@ public T tokenRefreshOffset(Duration tokenRefreshOffset) { } /** - * Specifies a Duration value representing the amount of time to wait between token refreshes. This is to prevent - * sending too many requests to the authentication service. + * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to + * prevent sending too many requests to the authentication service. * - * @param tokenRefreshTimeout the duration value representing the amount of time to wait between token refreshes + * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh * @return An updated instance of this builder with the token refresh offset set as specified. */ @SuppressWarnings("unchecked") - public T setTokenRefreshTimeout(Duration tokenRefreshTimeout) { - this.identityClientOptions.setTokenRefreshTimeout(tokenRefreshTimeout); + public T setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { + this.identityClientOptions.setTokenRefreshRetryTimeout(tokenRefreshRetryTimeout); return (T) this; } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index 960633e3dd08..8d2fc948e20c 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java @@ -212,16 +212,16 @@ public IdentityClientOptions setTokenRefreshOffset(Duration tokenRefreshOffset) } /** - * Specifies a Duration value representing the amount of time to wait between token refreshes. This is to prevent - * sending too many requests to the authentication service. + * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to + * prevent sending too many requests to the authentication service. * - * @param tokenRefreshTimeout the duration value representing the amount of time to wait between token refreshes + * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh * @return IdentityClientOptions - * @throws NullPointerException If {@code tokenRefreshOffset} is null. + * @throws NullPointerException If {@code tokenRefreshRetryTimeout} is null. */ - public IdentityClientOptions setTokenRefreshTimeout(Duration tokenRefreshTimeout) { - Objects.requireNonNull(tokenRefreshTimeout, "The token refresh timeout cannot be null."); - this.tokenRefreshOptions.setTokenRefreshTimeout(tokenRefreshTimeout); + public IdentityClientOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { + Objects.requireNonNull(tokenRefreshRetryTimeout, "The token refresh retry timeout cannot be null."); + this.tokenRefreshOptions.setTokenRefreshRetryTimeout(tokenRefreshRetryTimeout); return this; } From c877d9c407c793752210ab3e159ae70566e14d80 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 19 Jun 2020 16:32:12 -0700 Subject: [PATCH 25/43] Increase test stability --- .../com/azure/core/credential/SimpleTokenCache.java | 2 ++ .../com/azure/core/credential/TokenCacheTests.java | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 2da9f0845006..00bafd98c468 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -96,10 +96,12 @@ public Mono getToken() { sink.next(accessToken); cache = accessToken; nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + System.out.println("Getting a token. Next refresh after " + refreshRetryTimeout.toSeconds()); }) .onErrorResume(err -> { logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + System.out.println("Failing to get a token. Next refresh after " + refreshRetryTimeout.toSeconds()); return fallback.switchIfEmpty(Mono.error(err)); }) .switchIfEmpty(fallback) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index a2f3edaeb1b6..093cf7eaf897 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -110,13 +110,13 @@ public void testRefreshAfterExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 1000), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshRetryTimeout(Duration.ofSeconds(5))); + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(1)).setTokenRefreshRetryTimeout(Duration.ofSeconds(5))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); Flux.interval(Duration.ofSeconds(1)) - .take(4) // 38 seconds after first token, making sure of a refresh + .take(5) .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -130,7 +130,7 @@ public void testRefreshAfterExpiry() throws Exception { .subscribe(); latch.await(); - Assertions.assertTrue(maxMillis.get() >= 5000); + Assertions.assertTrue(maxMillis.get() >= 4000); } @Test @@ -146,7 +146,7 @@ public void testProactiveRefreshError() throws Exception { AtomicInteger errorCount = new AtomicInteger(0); Flux.interval(Duration.ofSeconds(1)) - .take(6) // 64 seconds after first token, making sure of a refresh + .take(6) .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -179,7 +179,7 @@ public void testProactiveRefreshErrorTimeout() throws Exception { AtomicInteger errorCount = new AtomicInteger(0); Flux.interval(Duration.ofSeconds(1)) - .take(8) // 64 seconds after first token, making sure of a refresh + .take(8) .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() From b89655463955c32aa9902ec0d8f6e6add61322b6 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 19 Jun 2020 23:17:18 -0700 Subject: [PATCH 26/43] Remove println in token cache --- .../java/com/azure/core/credential/SimpleTokenCache.java | 2 -- .../test/java/com/azure/core/credential/TokenCacheTests.java | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 00bafd98c468..2da9f0845006 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -96,12 +96,10 @@ public Mono getToken() { sink.next(accessToken); cache = accessToken; nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - System.out.println("Getting a token. Next refresh after " + refreshRetryTimeout.toSeconds()); }) .onErrorResume(err -> { logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - System.out.println("Failing to get a token. Next refresh after " + refreshRetryTimeout.toSeconds()); return fallback.switchIfEmpty(Mono.error(err)); }) .switchIfEmpty(fallback) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 093cf7eaf897..2ef21b7a7a92 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -116,7 +116,7 @@ public void testRefreshAfterExpiry() throws Exception { AtomicLong maxMillis = new AtomicLong(0); Flux.interval(Duration.ofSeconds(1)) - .take(5) + .take(4) .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -130,7 +130,8 @@ public void testRefreshAfterExpiry() throws Exception { .subscribe(); latch.await(); - Assertions.assertTrue(maxMillis.get() >= 4000); + System.out.println("MaxMillis:" + maxMillis.get()); + Assertions.assertTrue(maxMillis.get() >= 5000); } @Test From 49628e36d52d122af663842e4bb88c2f51bd19a3 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 22 Jun 2020 15:01:03 -0700 Subject: [PATCH 27/43] Simplify fallback logic --- .../core/credential/SimpleTokenCache.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 2da9f0845006..0800680046d2 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -91,22 +91,24 @@ public Mono getToken() { fallback = Mono.just(cache); } return tokenRefresh - .doOnNext(accessToken -> { - logger.info(refreshLog(cache, now, "Acquired a new access token")); - sink.next(accessToken); - cache = accessToken; - nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + .materialize() + .flatMap(signal -> { + if (signal.isOnNext() && signal.get() != null) { + logger.info(refreshLog(cache, now, "Acquired a new access token")); + sink.next(signal.get()); + cache = signal.get(); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + return Mono.just(signal.get()); + } else if (signal.isOnError() && signal.getThrowable() != null) { + logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + return fallback.switchIfEmpty(Mono.error(signal.getThrowable())); + } else { + return fallback; + } }) - .onErrorResume(err -> { - logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); - nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - return fallback.switchIfEmpty(Mono.error(err)); - }) - .switchIfEmpty(fallback) .doOnError(sink::error) - .doOnTerminate(() -> { - wip.set(false); - }); + .doOnTerminate(() -> wip.set(false)); } else { return emitterProcessor.next().filter(t -> !t.isExpired()); } From 8ebadeba08a0480b8d980d0501ef5fd66f61c0aa Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 22 Jun 2020 15:28:08 -0700 Subject: [PATCH 28/43] Help spotbugs understand the code --- .../azure/core/credential/SimpleTokenCache.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 0800680046d2..9d9480b2207c 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -93,16 +93,18 @@ public Mono getToken() { return tokenRefresh .materialize() .flatMap(signal -> { - if (signal.isOnNext() && signal.get() != null) { + AccessToken accessToken = signal.get(); + Throwable error = signal.getThrowable(); + if (signal.isOnNext() && accessToken != null) { logger.info(refreshLog(cache, now, "Acquired a new access token")); - sink.next(signal.get()); - cache = signal.get(); + sink.next(accessToken); + cache = accessToken; nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - return Mono.just(signal.get()); - } else if (signal.isOnError() && signal.getThrowable() != null) { + return Mono.just(accessToken); + } else if (signal.isOnError() && error != null) { logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - return fallback.switchIfEmpty(Mono.error(signal.getThrowable())); + return fallback.switchIfEmpty(Mono.error(error)); } else { return fallback; } From 649593f8f3d2c9be4b2235805844f5fb220c275e Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 22 Jun 2020 16:02:17 -0700 Subject: [PATCH 29/43] Add token refresh options to simple token cache --- .../azure/core/credential/AccessToken.java | 2 +- .../core/credential/SimpleTokenCache.java | 104 ++++++++++-- .../core/credential/TokenCredential.java | 9 ++ .../core/credential/TokenRefreshOptions.java | 63 ++++++++ .../BearerTokenAuthenticationPolicy.java | 3 +- .../core/credential/TokenCacheTests.java | 148 +++++++++++++++++- 6 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java index b8673a9fc960..b12996a55f37 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java @@ -19,7 +19,7 @@ public class AccessToken { */ public AccessToken(String token, OffsetDateTime expiresAt) { this.token = token; - this.expiresAt = expiresAt.minusMinutes(2); // 2 minutes before token expires + this.expiresAt = expiresAt; } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index b80416ca9952..9d9480b2207c 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -3,25 +3,31 @@ package com.azure.core.credential; +import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.FluxSink; import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; +import java.time.Duration; +import java.time.OffsetDateTime; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.function.Supplier; /** * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { - private static final int REFRESH_TIMEOUT_SECONDS = 30; - private final AtomicBoolean wip; - private AccessToken cache; + private volatile AccessToken cache; + private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; + private final Predicate shouldRefresh; + private final Duration refreshRetryTimeout; + private final ClientLogger logger = new ClientLogger(SimpleTokenCache.class); /** * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". @@ -29,8 +35,21 @@ public class SimpleTokenCache { * @param tokenSupplier a method to get a new token */ public SimpleTokenCache(Supplier> tokenSupplier) { + this(tokenSupplier, new TokenRefreshOptions()); + } + + /** + * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". + * + * @param tokenSupplier a method to get a new token + * @param tokenRefreshOptions the options to configure the token refresh behavior + */ + public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshOptions tokenRefreshOptions) { this.wip = new AtomicBoolean(false); this.tokenSupplier = tokenSupplier; + this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() + .minus(tokenRefreshOptions.getTokenRefreshOffset())); + this.refreshRetryTimeout = tokenRefreshOptions.getTokenRefreshRetryTimeout(); } /** @@ -38,18 +57,81 @@ public SimpleTokenCache(Supplier> tokenSupplier) { * @return a Publisher that emits an AccessToken */ public Mono getToken() { - if (cache != null && !cache.isExpired()) { - return Mono.just(cache); - } - return Mono.defer(() -> { + try { if (!wip.getAndSet(true)) { - return tokenSupplier.get().doOnNext(ac -> cache = ac) - .doOnNext(sink::next) + OffsetDateTime now = OffsetDateTime.now(); + Mono tokenRefresh; + Mono fallback; + if (cache != null && !shouldRefresh.test(cache)) { + // fresh cache & no need to refresh + tokenRefresh = Mono.empty(); + fallback = Mono.just(cache); + } else if (cache == null || cache.isExpired()) { + // no token to use + if (now.isAfter(nextTokenRefresh)) { + // refresh immediately + tokenRefresh = Mono.defer(tokenSupplier); + } else { + // wait for timeout, then refresh + tokenRefresh = Mono.delay(Duration.between(now, nextTokenRefresh)) + .then(Mono.defer(tokenSupplier)); + } + // cache doesn't exist or expired, no fallback + fallback = Mono.empty(); + } else { + // token available, but close to expiry + if (now.isAfter(nextTokenRefresh)) { + // refresh immediately + tokenRefresh = Mono.defer(tokenSupplier); + } else { + // still in timeout, do not refresh + tokenRefresh = Mono.empty(); + } + // cache hasn't expired, ignore refresh error this time + fallback = Mono.just(cache); + } + return tokenRefresh + .materialize() + .flatMap(signal -> { + AccessToken accessToken = signal.get(); + Throwable error = signal.getThrowable(); + if (signal.isOnNext() && accessToken != null) { + logger.info(refreshLog(cache, now, "Acquired a new access token")); + sink.next(accessToken); + cache = accessToken; + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + return Mono.just(accessToken); + } else if (signal.isOnError() && error != null) { + logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + return fallback.switchIfEmpty(Mono.error(error)); + } else { + return fallback; + } + }) .doOnError(sink::error) .doOnTerminate(() -> wip.set(false)); } else { - return emitterProcessor.next(); + return emitterProcessor.next().filter(t -> !t.isExpired()); } - }); + } catch (Throwable t) { + return Mono.error(t); + } + } + + private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { + StringBuilder info = new StringBuilder(log); + if (cache == null) { + info.append("."); + } else { + Duration tte = Duration.between(now, cache.getExpiresAt()); + info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") + .append(tte.isNegative() ? "after" : "before").append(" expiry. ") + .append("Retry may be attempted after ").append(refreshRetryTimeout.getSeconds()).append(" seconds."); + if (!tte.isNegative()) { + info.append(" The token currently cached will be used."); + } + } + return info.toString(); } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java index 79c29d6e0416..0408548dec65 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java @@ -16,4 +16,13 @@ public interface TokenCredential { * @return a Publisher that emits a single access token */ Mono getToken(TokenRequestContext request); + + /** + * The options to configure the token refresh behavior. + * + * @return the current offset for token refresh + */ + default TokenRefreshOptions getTokenRefreshOptions() { + return new TokenRefreshOptions(); + } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java new file mode 100644 index 000000000000..fd566916d351 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.credential; + +import java.time.Duration; + +/** + * The options to configure the token refresh behavior. + */ +public class TokenRefreshOptions { + private static final Duration DEFAULT_REFRESH_RETRY_TIMEOUT = Duration.ofSeconds(30); + private static final Duration DEFAULT_REFRESH_OFFSET = Duration.ofMinutes(2); + + private Duration tokenRefreshRetryTimeout = DEFAULT_REFRESH_RETRY_TIMEOUT; + private Duration tokenRefreshOffset = DEFAULT_REFRESH_OFFSET; + + /** + * Returns a Duration value representing the amount of time to wait before retrying a token refresh. This is to + * prevent sending too many requests to the authentication service. + * + * @return the duration value representing the amount of time to wait before retrying a token refresh + */ + public Duration getTokenRefreshRetryTimeout() { + return tokenRefreshRetryTimeout; + } + + /** + * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to + * prevent sending too many requests to the authentication service. + * + * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh + * @return TokenRefreshOptions + */ + public TokenRefreshOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { + this.tokenRefreshRetryTimeout = tokenRefreshRetryTimeout; + return this; + } + + /** + * Returns a Duration value representing the amount of time to subtract from the token expiry time, whereupon + * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the + * token. + * + * @return the duration value representing the amount of time to subtract from the token expiry time + */ + public Duration getTokenRefreshOffset() { + return tokenRefreshOffset; + } + + /** + * Specifies the Duration value representing the amount of time to subtract from the token expiry time, whereupon + * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the + * token. + * + * @param tokenRefreshOffset the duration representing the amount of time to subtract from the token expiry time + * @return TokenRefreshOptions + */ + public TokenRefreshOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { + this.tokenRefreshOffset = tokenRefreshOffset; + return this; + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index f643b4975664..ae9a4616c975 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -37,7 +37,8 @@ public BearerTokenAuthenticationPolicy(TokenCredential credential, String... sco assert scopes.length > 0; this.credential = credential; this.scopes = scopes; - this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes))); + this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes)), + credential.getTokenRefreshOptions()); } @Override diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 1aa0689b8d3c..2ef21b7a7a92 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -49,14 +49,14 @@ public void testOnlyOneThreadRefreshesToken() throws Exception { } @Test - public void testLongRunningWontOverflow() throws Exception { + public void testMultipleThreadsWaitForTimeout() throws Exception { AtomicLong refreshes = new AtomicLong(0); // token expires on creation. Run this 100 times to simulate running the application a long time SimpleTokenCache cache = new SimpleTokenCache(() -> { refreshes.incrementAndGet(); return remoteGetTokenThatExpiresSoonAsync(1000, 0); - }); + }, new TokenRefreshOptions().setTokenRefreshRetryTimeout(Duration.ZERO)); CountDownLatch latch = new CountDownLatch(1); @@ -68,17 +68,137 @@ public void testLongRunningWontOverflow() throws Exception { .flatMap(start -> cache.getToken() .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) .doOnNext(millis -> { -// System.out.format("Thread: %s\tDuration: %smillis%n", -// Thread.currentThread().getName(), Duration.between(start, OffsetDateTime.now()).toMillis()); }))) .doOnComplete(latch::countDown) .subscribe(); latch.await(); - // At most 10 requests should do actual token acquisition, use 11 for safe Assertions.assertTrue(refreshes.get() <= 11); } + @Test + public void testProactiveRefreshBeforeExpiry() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(58)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + + Flux.interval(Duration.ofSeconds(1)) + .take(5) + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }); + }).doOnComplete(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() >= 2000); + Assertions.assertTrue(maxMillis.get() < 3000); // Big enough for any latency, small enough to make sure no get token is called twice + } + + @Test + public void testRefreshAfterExpiry() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 1000), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(1)).setTokenRefreshRetryTimeout(Duration.ofSeconds(5))); + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + + Flux.interval(Duration.ofSeconds(1)) + .take(4) + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }); + }).doOnComplete(latch::countDown) + .subscribe(); + + latch.await(); + System.out.println("MaxMillis:" + maxMillis.get()); + Assertions.assertTrue(maxMillis.get() >= 5000); + } + + @Test + public void testProactiveRefreshError() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + AtomicInteger tryCount = new AtomicInteger(0); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + AtomicInteger errorCount = new AtomicInteger(0); + + Flux.interval(Duration.ofSeconds(1)) + .take(6) + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }) + .doOnError(t -> errorCount.incrementAndGet()); + }).doOnTerminate(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() >= 1000); + Assertions.assertTrue(maxMillis.get() < 2000); // Big enough for any latency, small enough to make sure no get token is called twice + Assertions.assertEquals(1, errorCount.get()); // Only the error after expiresAt will be propagated + } + + @Test + public void testProactiveRefreshErrorTimeout() throws Exception { + AtomicInteger latency = new AtomicInteger(1); + AtomicInteger tryCount = new AtomicInteger(0); + SimpleTokenCache cache = new SimpleTokenCache( + () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 4 * 1000, 2, tryCount), + new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong maxMillis = new AtomicLong(0); + AtomicInteger errorCount = new AtomicInteger(0); + + Flux.interval(Duration.ofSeconds(1)) + .take(8) + .flatMap(i -> { + OffsetDateTime start = OffsetDateTime.now(); + return cache.getToken() + .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) + .doOnNext(millis -> { + if (millis > maxMillis.get()) { + maxMillis.set(millis); + } + }) + .doOnError(t -> errorCount.incrementAndGet()); + }).doOnTerminate(latch::countDown) + .subscribe(); + + latch.await(); + Assertions.assertTrue(maxMillis.get() >= 3000); + Assertions.assertEquals(0, errorCount.get()); // Only the error after expiresAt will be propagated + } + private Mono remoteGetTokenAsync(long delayInMillis) { return Mono.delay(Duration.ofMillis(delayInMillis)) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); @@ -95,6 +215,24 @@ private Mono incrementalRemoteGetTokenAsync(AtomicInteger latency) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); } + private Mono remoteGetTokenWithTemporaryError(long delayInMillis, long validityInMillis, int errorAt, AtomicInteger tryCount) { + if (tryCount.incrementAndGet() == errorAt) { + return Mono.error(new RuntimeException("Expected error")); + } else { + return Mono.delay(Duration.ofMillis(delayInMillis)) + .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)), validityInMillis)); + } + } + + private Mono remoteGetTokenWithPersistentError(long delayInMillis, long validityInMillis, int errorAfter, AtomicInteger tryCount) { + if (tryCount.incrementAndGet() >= errorAfter) { + return Mono.error(new RuntimeException("Expected error")); + } else { + return Mono.delay(Duration.ofMillis(delayInMillis)) + .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)), validityInMillis)); + } + } + private static class Token extends AccessToken { private String token; private OffsetDateTime expiry; From 8fc55084414d29b6fcc79904990cc4f7e2c67b53 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 22 Jun 2020 16:18:59 -0700 Subject: [PATCH 30/43] Fix amqp test --- .../amqp/implementation/ClaimsBasedSecurityChannelTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java index 5a847b48b063..685de4005242 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java @@ -107,8 +107,7 @@ public void teardown() { @Test public void authorizesSasToken() { // Arrange - // Subtracting two minutes because the AccessToken does this internally. - final Date expectedDate = Date.from(validUntil.minusMinutes(2).toInstant()); + final Date expectedDate = Date.from(validUntil.toInstant()); final ClaimsBasedSecurityChannel cbsChannel = new ClaimsBasedSecurityChannel(Mono.just(requestResponseChannel), tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, options); From 7d4c095dc103f03ea446a1d7299fa0685faaef96 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 24 Jun 2020 14:03:43 -0700 Subject: [PATCH 31/43] Improve token refresh --- .../core/credential/SimpleTokenCache.java | 46 +++++++++++-------- .../core/credential/TokenCacheTests.java | 31 +++++++++++++ 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 9d9480b2207c..83a041c207a6 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -4,14 +4,12 @@ package com.azure.core.credential; import com.azure.core.util.logging.ClientLogger; -import reactor.core.publisher.FluxSink; -import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; +import reactor.core.publisher.MonoProcessor; import java.time.Duration; import java.time.OffsetDateTime; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.function.Supplier; @@ -19,11 +17,9 @@ * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { - private final AtomicBoolean wip; + private final AtomicReference> wip; private volatile AccessToken cache; private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); - private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); - private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; private final Predicate shouldRefresh; private final Duration refreshRetryTimeout; @@ -45,7 +41,7 @@ public SimpleTokenCache(Supplier> tokenSupplier) { * @param tokenRefreshOptions the options to configure the token refresh behavior */ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshOptions tokenRefreshOptions) { - this.wip = new AtomicBoolean(false); + this.wip = new AtomicReference<>(); this.tokenSupplier = tokenSupplier; this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() .minus(tokenRefreshOptions.getTokenRefreshOffset())); @@ -58,7 +54,8 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO */ public Mono getToken() { try { - if (!wip.getAndSet(true)) { + if (wip.compareAndSet(null, MonoProcessor.create())) { + final MonoProcessor monoProcessor = wip.get(); OffsetDateTime now = OffsetDateTime.now(); Mono tokenRefresh; Mono fallback; @@ -73,8 +70,8 @@ public Mono getToken() { tokenRefresh = Mono.defer(tokenSupplier); } else { // wait for timeout, then refresh - tokenRefresh = Mono.delay(Duration.between(now, nextTokenRefresh)) - .then(Mono.defer(tokenSupplier)); + tokenRefresh = Mono.defer(tokenSupplier) + .delaySubscription(Duration.between(now, nextTokenRefresh)); } // cache doesn't exist or expired, no fallback fallback = Mono.empty(); @@ -95,24 +92,37 @@ public Mono getToken() { .flatMap(signal -> { AccessToken accessToken = signal.get(); Throwable error = signal.getThrowable(); - if (signal.isOnNext() && accessToken != null) { + if (signal.isOnNext() && accessToken != null) { // SUCCESS logger.info(refreshLog(cache, now, "Acquired a new access token")); - sink.next(accessToken); cache = accessToken; + monoProcessor.onNext(accessToken); + monoProcessor.onComplete(); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); return Mono.just(accessToken); - } else if (signal.isOnError() && error != null) { + } else if (signal.isOnError() && error != null) { // ERROR logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); return fallback.switchIfEmpty(Mono.error(error)); - } else { + } else { // NO REFRESH + monoProcessor.onComplete(); return fallback; } }) - .doOnError(sink::error) - .doOnTerminate(() -> wip.set(false)); + .doOnError(monoProcessor::onError) + .doOnTerminate(() -> wip.set(null)); + } else if (cache != null && !cache.isExpired()) { + // another thread might be refreshing the token proactively, but the current token is still valid + return Mono.just(cache); } else { - return emitterProcessor.next().filter(t -> !t.isExpired()); + // another thread is definitely refreshing the expired token + MonoProcessor monoProcessor = wip.get(); + if (monoProcessor == null) { + // the refreshing thread has finished + return Mono.just(cache); + } else { + // wait for refreshing thread to finish but defer to updated cache in case just missed onNext() + return monoProcessor.switchIfEmpty(Mono.defer(() -> Mono.just(cache))); + } } } catch (Throwable t) { return Mono.error(t); diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 2ef21b7a7a92..bc311cc86ac0 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -13,6 +13,7 @@ import java.time.OffsetDateTime; import java.util.Random; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -199,6 +200,36 @@ public void testProactiveRefreshErrorTimeout() throws Exception { Assertions.assertEquals(0, errorCount.get()); // Only the error after expiresAt will be propagated } + @Test + public void testMultipleThreadsAfterExpiry() throws Exception { + AtomicLong refreshes = new AtomicLong(0); + + // token expires on creation. Run this 100 times to simulate running the application a long time + SimpleTokenCache cache = new SimpleTokenCache(() -> { + refreshes.incrementAndGet(); + return remoteGetTokenThatExpiresSoonAsync(1000, 3 * 1000); + }, new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshRetryTimeout(Duration.ZERO)); + + CountDownLatch latch = new CountDownLatch(100); + + cache.getToken().then(Mono.delay(Duration.ofSeconds(5))).block(); + + Flux.range(1, 100) + .flatMap(i -> Mono.just(OffsetDateTime.now()) + .subscribeOn(Schedulers.newParallel("pool", 100)) + .flatMap(start -> cache.getToken() + .doOnNext(t -> { + if (!t.isExpired()) { + latch.countDown(); + } + }))) // expect all getToken() calls to return a token + .subscribe(); + + latch.await(20, TimeUnit.SECONDS); + Assertions.assertEquals(2 , refreshes.get()); + Assertions.assertEquals(0, latch.getCount()); + } + private Mono remoteGetTokenAsync(long delayInMillis) { return Mono.delay(Duration.ofMillis(delayInMillis)) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); From 4eff3c1e0ae0768dcec305a2d521cfad5bb7b175 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 24 Jun 2020 14:23:16 -0700 Subject: [PATCH 32/43] Address Alan & CI's feedback --- eng/versioning/version_client.txt | 4 +- .../core/credential/SimpleTokenCache.java | 46 +++++++++++-------- .../core/credential/TokenCacheTests.java | 31 +++++++++++++ sdk/identity/azure-identity/pom.xml | 4 +- .../implementation/MSITokenTests.java | 6 +-- 5 files changed, 67 insertions(+), 24 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 39f82e3969b6..a497b7e7f8b7 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -62,6 +62,8 @@ com.microsoft.azure:azure-spring-boot-test-core;2.3.2;2.3.3-beta.1 # Format; # unreleased_:;dependency-version # note: The unreleased dependencies will not be manipulated with the automatic PR creation code. +unreleased_com.azure:azure-core;1.6.0-beta.1 +unreleased_com.azure:azure-core-http-netty;1.6.0-beta.1 # Released Beta dependencies: Copy the entry from above, prepend "beta_", remove the current @@ -70,4 +72,4 @@ com.microsoft.azure:azure-spring-boot-test-core;2.3.2;2.3.3-beta.1 # a library but need to keep the dependency version at the latest released GA. # Format; # beta_:;dependency-version -# note: Released beta versions will not be manipulated with the automatic PR creation code. +# note: Released beta versions will not be manipulated with the automatic PR creation code. \ No newline at end of file diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 9d9480b2207c..83a041c207a6 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -4,14 +4,12 @@ package com.azure.core.credential; import com.azure.core.util.logging.ClientLogger; -import reactor.core.publisher.FluxSink; -import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; +import reactor.core.publisher.MonoProcessor; import java.time.Duration; import java.time.OffsetDateTime; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.function.Supplier; @@ -19,11 +17,9 @@ * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { - private final AtomicBoolean wip; + private final AtomicReference> wip; private volatile AccessToken cache; private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); - private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); - private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; private final Predicate shouldRefresh; private final Duration refreshRetryTimeout; @@ -45,7 +41,7 @@ public SimpleTokenCache(Supplier> tokenSupplier) { * @param tokenRefreshOptions the options to configure the token refresh behavior */ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshOptions tokenRefreshOptions) { - this.wip = new AtomicBoolean(false); + this.wip = new AtomicReference<>(); this.tokenSupplier = tokenSupplier; this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() .minus(tokenRefreshOptions.getTokenRefreshOffset())); @@ -58,7 +54,8 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO */ public Mono getToken() { try { - if (!wip.getAndSet(true)) { + if (wip.compareAndSet(null, MonoProcessor.create())) { + final MonoProcessor monoProcessor = wip.get(); OffsetDateTime now = OffsetDateTime.now(); Mono tokenRefresh; Mono fallback; @@ -73,8 +70,8 @@ public Mono getToken() { tokenRefresh = Mono.defer(tokenSupplier); } else { // wait for timeout, then refresh - tokenRefresh = Mono.delay(Duration.between(now, nextTokenRefresh)) - .then(Mono.defer(tokenSupplier)); + tokenRefresh = Mono.defer(tokenSupplier) + .delaySubscription(Duration.between(now, nextTokenRefresh)); } // cache doesn't exist or expired, no fallback fallback = Mono.empty(); @@ -95,24 +92,37 @@ public Mono getToken() { .flatMap(signal -> { AccessToken accessToken = signal.get(); Throwable error = signal.getThrowable(); - if (signal.isOnNext() && accessToken != null) { + if (signal.isOnNext() && accessToken != null) { // SUCCESS logger.info(refreshLog(cache, now, "Acquired a new access token")); - sink.next(accessToken); cache = accessToken; + monoProcessor.onNext(accessToken); + monoProcessor.onComplete(); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); return Mono.just(accessToken); - } else if (signal.isOnError() && error != null) { + } else if (signal.isOnError() && error != null) { // ERROR logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); return fallback.switchIfEmpty(Mono.error(error)); - } else { + } else { // NO REFRESH + monoProcessor.onComplete(); return fallback; } }) - .doOnError(sink::error) - .doOnTerminate(() -> wip.set(false)); + .doOnError(monoProcessor::onError) + .doOnTerminate(() -> wip.set(null)); + } else if (cache != null && !cache.isExpired()) { + // another thread might be refreshing the token proactively, but the current token is still valid + return Mono.just(cache); } else { - return emitterProcessor.next().filter(t -> !t.isExpired()); + // another thread is definitely refreshing the expired token + MonoProcessor monoProcessor = wip.get(); + if (monoProcessor == null) { + // the refreshing thread has finished + return Mono.just(cache); + } else { + // wait for refreshing thread to finish but defer to updated cache in case just missed onNext() + return monoProcessor.switchIfEmpty(Mono.defer(() -> Mono.just(cache))); + } } } catch (Throwable t) { return Mono.error(t); diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 2ef21b7a7a92..bc311cc86ac0 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -13,6 +13,7 @@ import java.time.OffsetDateTime; import java.util.Random; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -199,6 +200,36 @@ public void testProactiveRefreshErrorTimeout() throws Exception { Assertions.assertEquals(0, errorCount.get()); // Only the error after expiresAt will be propagated } + @Test + public void testMultipleThreadsAfterExpiry() throws Exception { + AtomicLong refreshes = new AtomicLong(0); + + // token expires on creation. Run this 100 times to simulate running the application a long time + SimpleTokenCache cache = new SimpleTokenCache(() -> { + refreshes.incrementAndGet(); + return remoteGetTokenThatExpiresSoonAsync(1000, 3 * 1000); + }, new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshRetryTimeout(Duration.ZERO)); + + CountDownLatch latch = new CountDownLatch(100); + + cache.getToken().then(Mono.delay(Duration.ofSeconds(5))).block(); + + Flux.range(1, 100) + .flatMap(i -> Mono.just(OffsetDateTime.now()) + .subscribeOn(Schedulers.newParallel("pool", 100)) + .flatMap(start -> cache.getToken() + .doOnNext(t -> { + if (!t.isExpired()) { + latch.countDown(); + } + }))) // expect all getToken() calls to return a token + .subscribe(); + + latch.await(20, TimeUnit.SECONDS); + Assertions.assertEquals(2 , refreshes.get()); + Assertions.assertEquals(0, latch.getCount()); + } + private Mono remoteGetTokenAsync(long delayInMillis) { return Mono.delay(Duration.ofMillis(delayInMillis)) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 3525b441d290..6ada6d7f6630 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -27,7 +27,7 @@ com.azure azure-core - 1.5.1 + 1.6.0-beta.1 com.microsoft.azure @@ -88,7 +88,7 @@ com.azure azure-core-http-netty - 1.5.2 + 1.6.0-beta.1 test diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java index 3284fc197e46..ec8e6a590ba6 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/MSITokenTests.java @@ -10,7 +10,7 @@ import java.time.ZoneOffset; public class MSITokenTests { - private OffsetDateTime expected = OffsetDateTime.of(2020, 1, 10, 15, 1, 28, 0, ZoneOffset.UTC); + private OffsetDateTime expected = OffsetDateTime.of(2020, 1, 10, 15, 3, 28, 0, ZoneOffset.UTC); @Test public void canParseLong() { @@ -30,11 +30,11 @@ public void canParseDateTime12Hr() { Assert.assertEquals(expected.toEpochSecond(), token.getExpiresAt().toEpochSecond()); token = new MSIToken("fake_token", "12/20/2019 4:58:20 AM +00:00"); - expected = OffsetDateTime.of(2019, 12, 20, 4, 56, 20, 0, ZoneOffset.UTC); + expected = OffsetDateTime.of(2019, 12, 20, 4, 58, 20, 0, ZoneOffset.UTC); Assert.assertEquals(expected.toEpochSecond(), token.getExpiresAt().toEpochSecond()); token = new MSIToken("fake_token", "1/1/2020 0:00:00 PM +00:00"); - expected = OffsetDateTime.of(2020, 1, 1, 11, 58, 0, 0, ZoneOffset.UTC); + expected = OffsetDateTime.of(2020, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC); Assert.assertEquals(expected.toEpochSecond(), token.getExpiresAt().toEpochSecond()); } } From 400396b13b3cabd07e793a8523ebd3948a09bedd Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 24 Jun 2020 14:35:53 -0700 Subject: [PATCH 33/43] migrate more from identity --- .../implementation/IdentityClient.java | 9 +++---- .../implementation/IdentityToken.java | 24 ------------------- .../identity/implementation/MsalToken.java | 6 ++--- 3 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 78da0dc1ddf2..c1865388bc18 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -380,7 +380,7 @@ public Mono authenticateWithAzureCli(TokenRequestContext request) { OffsetDateTime expiresOn = LocalDateTime.parse(timeJoinedWithT, DateTimeFormatter.ISO_LOCAL_DATE_TIME) .atZone(ZoneId.systemDefault()) .toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC); - token = new IdentityToken(accessToken, expiresOn, options); + token = new AccessToken(accessToken, expiresOn); } catch (IOException | InterruptedException e) { throw logger.logExceptionAsError(new IllegalStateException(e)); } catch (RuntimeException e) { @@ -692,8 +692,7 @@ public Mono authenticateToManagedIdentityEndpoint(String msiEndpoin .useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; - MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); - return new IdentityToken(msiToken.getToken(), msiToken.getExpiresAt(), options); + return SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); } finally { if (connection != null) { connection.disconnect(); @@ -745,9 +744,7 @@ public Mono authenticateToIMDSEndpoint(TokenRequestContext request) .useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; - MSIToken msiToken = SERIALIZER_ADAPTER.deserialize(result, - MSIToken.class, SerializerEncoding.JSON); - return new IdentityToken(msiToken.getToken(), msiToken.getExpiresAt(), options); + return SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON); } catch (IOException exception) { if (connection == null) { throw logger.logExceptionAsError(new RuntimeException( diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java deleted file mode 100644 index c6973b3d99a8..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation; - -import com.azure.core.credential.AccessToken; - -import java.time.OffsetDateTime; - -/** - * Type representing authentication result from the azure-identity client. - */ -public class IdentityToken extends AccessToken { - /** - * Creates an identity token instance. - * - * @param token the token string. - * @param expiresAt the expiration time. - * @param options the identity client options. - */ - public IdentityToken(String token, OffsetDateTime expiresAt, IdentityClientOptions options) { - super(token, expiresAt.plusMinutes(2).minus(options.getTokenRefreshOffset())); - } -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java index e8eca5066bd1..a8b2d3754812 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MsalToken.java @@ -3,6 +3,7 @@ package com.azure.identity.implementation; +import com.azure.core.credential.AccessToken; import com.microsoft.aad.msal4j.IAccount; import com.microsoft.aad.msal4j.IAuthenticationResult; @@ -12,7 +13,7 @@ /** * Type representing authentication result from the MSAL (Microsoft Authentication Library). */ -public final class MsalToken extends IdentityToken { +public final class MsalToken extends AccessToken { private IAuthenticationResult authenticationResult; @@ -23,8 +24,7 @@ public final class MsalToken extends IdentityToken { */ public MsalToken(IAuthenticationResult msalResult, IdentityClientOptions options) { super(msalResult.accessToken(), - OffsetDateTime.ofInstant(msalResult.expiresOnDate().toInstant(), ZoneOffset.UTC), - options); + OffsetDateTime.ofInstant(msalResult.expiresOnDate().toInstant(), ZoneOffset.UTC)); authenticationResult = msalResult; } From 886cac316a4e3bd40ded52e8a8857893c81ae658 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 25 Jun 2020 00:06:26 -0700 Subject: [PATCH 34/43] Checkstyle --- .../test/java/com/azure/core/credential/TokenCacheTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index bc311cc86ac0..e73de7f77265 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -226,7 +226,7 @@ public void testMultipleThreadsAfterExpiry() throws Exception { .subscribe(); latch.await(20, TimeUnit.SECONDS); - Assertions.assertEquals(2 , refreshes.get()); + Assertions.assertEquals(2, refreshes.get()); Assertions.assertEquals(0, latch.getCount()); } From 4b6eec05de35a23e3ee91bfdecf569048045ea74 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 25 Jun 2020 23:56:07 -0700 Subject: [PATCH 35/43] Fit and finish --- sdk/core/azure-core/CHANGELOG.md | 2 +- .../core/credential/SimpleTokenCache.java | 136 +++++++++--------- .../core/credential/TokenRefreshOptions.java | 4 +- 3 files changed, 72 insertions(+), 70 deletions(-) diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index d8222bc789df..026cd29cb61d 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -1,7 +1,7 @@ # Release History ## 1.6.0-beta.1 (Unreleased) - +- Added `TokenRefreshOptions()` to `TokenCredential`, with a default token refresh offset of 2 minutes, and a default token refresh retry timeout of 30 seconds. ## 1.5.1 (2020-06-08) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 83a041c207a6..def821c49d04 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -53,80 +53,82 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO * @return a Publisher that emits an AccessToken */ public Mono getToken() { - try { - if (wip.compareAndSet(null, MonoProcessor.create())) { - final MonoProcessor monoProcessor = wip.get(); - OffsetDateTime now = OffsetDateTime.now(); - Mono tokenRefresh; - Mono fallback; - if (cache != null && !shouldRefresh.test(cache)) { - // fresh cache & no need to refresh - tokenRefresh = Mono.empty(); - fallback = Mono.just(cache); - } else if (cache == null || cache.isExpired()) { - // no token to use - if (now.isAfter(nextTokenRefresh)) { - // refresh immediately - tokenRefresh = Mono.defer(tokenSupplier); + return Mono.defer(() -> { + try { + if (wip.compareAndSet(null, MonoProcessor.create())) { + final MonoProcessor monoProcessor = wip.get(); + OffsetDateTime now = OffsetDateTime.now(); + Mono tokenRefresh; + Mono fallback; + if (cache != null && !shouldRefresh.test(cache)) { + // fresh cache & no need to refresh + tokenRefresh = Mono.empty(); + fallback = Mono.just(cache); + } else if (cache == null || cache.isExpired()) { + // no token to use + if (now.isAfter(nextTokenRefresh)) { + // refresh immediately + tokenRefresh = Mono.defer(tokenSupplier); + } else { + // wait for timeout, then refresh + tokenRefresh = Mono.defer(tokenSupplier) + .delaySubscription(Duration.between(now, nextTokenRefresh)); + } + // cache doesn't exist or expired, no fallback + fallback = Mono.empty(); } else { - // wait for timeout, then refresh - tokenRefresh = Mono.defer(tokenSupplier) - .delaySubscription(Duration.between(now, nextTokenRefresh)); + // token available, but close to expiry + if (now.isAfter(nextTokenRefresh)) { + // refresh immediately + tokenRefresh = Mono.defer(tokenSupplier); + } else { + // still in timeout, do not refresh + tokenRefresh = Mono.empty(); + } + // cache hasn't expired, ignore refresh error this time + fallback = Mono.just(cache); } - // cache doesn't exist or expired, no fallback - fallback = Mono.empty(); + return tokenRefresh + .materialize() + .flatMap(signal -> { + AccessToken accessToken = signal.get(); + Throwable error = signal.getThrowable(); + if (signal.isOnNext() && accessToken != null) { // SUCCESS + logger.info(refreshLog(cache, now, "Acquired a new access token")); + cache = accessToken; + monoProcessor.onNext(accessToken); + monoProcessor.onComplete(); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + return Mono.just(accessToken); + } else if (signal.isOnError() && error != null) { // ERROR + logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); + nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + return fallback.switchIfEmpty(Mono.error(error)); + } else { // NO REFRESH + monoProcessor.onComplete(); + return fallback; + } + }) + .doOnError(monoProcessor::onError) + .doOnTerminate(() -> wip.set(null)); + } else if (cache != null && !cache.isExpired()) { + // another thread might be refreshing the token proactively, but the current token is still valid + return Mono.just(cache); } else { - // token available, but close to expiry - if (now.isAfter(nextTokenRefresh)) { - // refresh immediately - tokenRefresh = Mono.defer(tokenSupplier); + // another thread is definitely refreshing the expired token + MonoProcessor monoProcessor = wip.get(); + if (monoProcessor == null) { + // the refreshing thread has finished + return Mono.just(cache); } else { - // still in timeout, do not refresh - tokenRefresh = Mono.empty(); + // wait for refreshing thread to finish but defer to updated cache in case just missed onNext() + return monoProcessor.switchIfEmpty(Mono.defer(() -> Mono.just(cache))); } - // cache hasn't expired, ignore refresh error this time - fallback = Mono.just(cache); - } - return tokenRefresh - .materialize() - .flatMap(signal -> { - AccessToken accessToken = signal.get(); - Throwable error = signal.getThrowable(); - if (signal.isOnNext() && accessToken != null) { // SUCCESS - logger.info(refreshLog(cache, now, "Acquired a new access token")); - cache = accessToken; - monoProcessor.onNext(accessToken); - monoProcessor.onComplete(); - nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - return Mono.just(accessToken); - } else if (signal.isOnError() && error != null) { // ERROR - logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); - nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); - return fallback.switchIfEmpty(Mono.error(error)); - } else { // NO REFRESH - monoProcessor.onComplete(); - return fallback; - } - }) - .doOnError(monoProcessor::onError) - .doOnTerminate(() -> wip.set(null)); - } else if (cache != null && !cache.isExpired()) { - // another thread might be refreshing the token proactively, but the current token is still valid - return Mono.just(cache); - } else { - // another thread is definitely refreshing the expired token - MonoProcessor monoProcessor = wip.get(); - if (monoProcessor == null) { - // the refreshing thread has finished - return Mono.just(cache); - } else { - // wait for refreshing thread to finish but defer to updated cache in case just missed onNext() - return monoProcessor.switchIfEmpty(Mono.defer(() -> Mono.just(cache))); } + } catch (Throwable t) { + return Mono.error(t); } - } catch (Throwable t) { - return Mono.error(t); - } + }); } private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java index fd566916d351..ba9750a74930 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -30,7 +30,7 @@ public Duration getTokenRefreshRetryTimeout() { * prevent sending too many requests to the authentication service. * * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh - * @return TokenRefreshOptions + * @return the updated TokenRefreshOptions object */ public TokenRefreshOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { this.tokenRefreshRetryTimeout = tokenRefreshRetryTimeout; @@ -54,7 +54,7 @@ public Duration getTokenRefreshOffset() { * token. * * @param tokenRefreshOffset the duration representing the amount of time to subtract from the token expiry time - * @return TokenRefreshOptions + * @return the updated TokenRefreshOptions object */ public TokenRefreshOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { this.tokenRefreshOffset = tokenRefreshOffset; From cbb443845186be2d969ddb2a0592a909066f453b Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 29 Jun 2020 14:28:21 -0700 Subject: [PATCH 36/43] Simply properties in TokenRefreshOptions --- .../core/credential/SimpleTokenCache.java | 4 +-- .../core/credential/TokenRefreshOptions.java | 28 +++++++++---------- .../core/credential/TokenCacheTests.java | 12 ++++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index def821c49d04..8f067ca04257 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -44,8 +44,8 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO this.wip = new AtomicReference<>(); this.tokenSupplier = tokenSupplier; this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() - .minus(tokenRefreshOptions.getTokenRefreshOffset())); - this.refreshRetryTimeout = tokenRefreshOptions.getTokenRefreshRetryTimeout(); + .minus(tokenRefreshOptions.getOffset())); + this.refreshRetryTimeout = tokenRefreshOptions.getRetryTimeout(); } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java index ba9750a74930..7be5ba65d740 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -9,11 +9,11 @@ * The options to configure the token refresh behavior. */ public class TokenRefreshOptions { - private static final Duration DEFAULT_REFRESH_RETRY_TIMEOUT = Duration.ofSeconds(30); - private static final Duration DEFAULT_REFRESH_OFFSET = Duration.ofMinutes(2); + private static final Duration DEFAULT_RETRY_TIMEOUT = Duration.ofSeconds(30); + private static final Duration DEFAULT_OFFSET = Duration.ofMinutes(2); - private Duration tokenRefreshRetryTimeout = DEFAULT_REFRESH_RETRY_TIMEOUT; - private Duration tokenRefreshOffset = DEFAULT_REFRESH_OFFSET; + private Duration retryTimeout = DEFAULT_RETRY_TIMEOUT; + private Duration offset = DEFAULT_OFFSET; /** * Returns a Duration value representing the amount of time to wait before retrying a token refresh. This is to @@ -21,19 +21,19 @@ public class TokenRefreshOptions { * * @return the duration value representing the amount of time to wait before retrying a token refresh */ - public Duration getTokenRefreshRetryTimeout() { - return tokenRefreshRetryTimeout; + public Duration getRetryTimeout() { + return retryTimeout; } /** * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to * prevent sending too many requests to the authentication service. * - * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh + * @param retryTimeout the amount of time to wait before retrying a token refresh * @return the updated TokenRefreshOptions object */ - public TokenRefreshOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { - this.tokenRefreshRetryTimeout = tokenRefreshRetryTimeout; + public TokenRefreshOptions setRetryTimeout(Duration retryTimeout) { + this.retryTimeout = retryTimeout; return this; } @@ -44,8 +44,8 @@ public TokenRefreshOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetr * * @return the duration value representing the amount of time to subtract from the token expiry time */ - public Duration getTokenRefreshOffset() { - return tokenRefreshOffset; + public Duration getOffset() { + return offset; } /** @@ -53,11 +53,11 @@ public Duration getTokenRefreshOffset() { * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the * token. * - * @param tokenRefreshOffset the duration representing the amount of time to subtract from the token expiry time + * @param offset the duration representing the amount of time to subtract from the token expiry time * @return the updated TokenRefreshOptions object */ - public TokenRefreshOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { - this.tokenRefreshOffset = tokenRefreshOffset; + public TokenRefreshOptions setOffset(Duration offset) { + this.offset = offset; return this; } } diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index e73de7f77265..ba79b8ea5c4d 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -57,7 +57,7 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { SimpleTokenCache cache = new SimpleTokenCache(() -> { refreshes.incrementAndGet(); return remoteGetTokenThatExpiresSoonAsync(1000, 0); - }, new TokenRefreshOptions().setTokenRefreshRetryTimeout(Duration.ZERO)); + }, new TokenRefreshOptions().setRetryTimeout(Duration.ZERO)); CountDownLatch latch = new CountDownLatch(1); @@ -82,7 +82,7 @@ public void testProactiveRefreshBeforeExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(58)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); + new TokenRefreshOptions().setOffset(Duration.ofSeconds(58)).setRetryTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -111,7 +111,7 @@ public void testRefreshAfterExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 1000), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(1)).setTokenRefreshRetryTimeout(Duration.ofSeconds(5))); + new TokenRefreshOptions().setOffset(Duration.ofSeconds(1)).setRetryTimeout(Duration.ofSeconds(5))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -141,7 +141,7 @@ public void testProactiveRefreshError() throws Exception { AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); + new TokenRefreshOptions().setOffset(Duration.ofSeconds(2)).setRetryTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -174,7 +174,7 @@ public void testProactiveRefreshErrorTimeout() throws Exception { AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 4 * 1000, 2, tryCount), - new TokenRefreshOptions().setTokenRefreshOffset(Duration.ofSeconds(2)).setTokenRefreshRetryTimeout(Duration.ofSeconds(1))); + new TokenRefreshOptions().setOffset(Duration.ofSeconds(2)).setRetryTimeout(Duration.ofSeconds(1))); CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); @@ -208,7 +208,7 @@ public void testMultipleThreadsAfterExpiry() throws Exception { SimpleTokenCache cache = new SimpleTokenCache(() -> { refreshes.incrementAndGet(); return remoteGetTokenThatExpiresSoonAsync(1000, 3 * 1000); - }, new TokenRefreshOptions().setTokenRefreshOffset(Duration.ZERO).setTokenRefreshRetryTimeout(Duration.ZERO)); + }, new TokenRefreshOptions().setOffset(Duration.ZERO).setRetryTimeout(Duration.ZERO)); CountDownLatch latch = new CountDownLatch(100); From 24ff0a5e2eedacc50945de8fb9d85d634fe6d955 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 30 Jun 2020 12:14:55 -0700 Subject: [PATCH 37/43] Fix according to new azure-core changes --- .../core/credential/SimpleTokenCache.java | 20 +------------------ .../implementation/IdentityClient.java | 4 ++-- .../implementation/IdentityClientOptions.java | 4 ++-- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index ccae0d63d90f..8f067ca04257 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -128,25 +128,7 @@ public Mono getToken() { } catch (Throwable t) { return Mono.error(t); } - } catch (Throwable t) { - return Mono.error(t); - } - } - - private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { - StringBuilder info = new StringBuilder(log); - if (cache == null) { - info.append("."); - } else { - Duration tte = Duration.between(now, cache.getExpiresAt()); - info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") - .append(tte.isNegative() ? "after" : "before").append(" expiry. ") - .append("Retry may be attempted after ").append(refreshRetryTimeout.getSeconds()).append(" seconds."); - if (!tte.isNegative()) { - info.append(" The token currently cached will be used."); - } - } - return info.toString(); + }); } private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index f2b7fb980243..572c6eb38c34 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -460,7 +460,7 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req } }).map(MsalToken::new) .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus( - options.getTokenRefreshOptions().getTokenRefreshOffset()))) + options.getTokenRefreshOptions().getOffset()))) .switchIfEmpty(Mono.fromFuture(() -> { SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())).forceRefresh(true); @@ -493,7 +493,7 @@ public Mono authenticateWithConfidentialClientCache(TokenRequestCon } }).map(ar -> (AccessToken) new MsalToken(ar)) .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus( - options.getTokenRefreshOptions().getTokenRefreshOffset())))); + options.getTokenRefreshOptions().getOffset())))); } /** diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index 8d2fc948e20c..fef5d1bbe698 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java @@ -207,7 +207,7 @@ public TokenRefreshOptions getTokenRefreshOptions() { */ public IdentityClientOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { Objects.requireNonNull(tokenRefreshOffset, "The token refresh offset cannot be null."); - this.tokenRefreshOptions.setTokenRefreshOffset(tokenRefreshOffset); + this.tokenRefreshOptions.setOffset(tokenRefreshOffset); return this; } @@ -221,7 +221,7 @@ public IdentityClientOptions setTokenRefreshOffset(Duration tokenRefreshOffset) */ public IdentityClientOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { Objects.requireNonNull(tokenRefreshRetryTimeout, "The token refresh retry timeout cannot be null."); - this.tokenRefreshOptions.setTokenRefreshRetryTimeout(tokenRefreshRetryTimeout); + this.tokenRefreshOptions.setRetryTimeout(tokenRefreshRetryTimeout); return this; } From b810bc54460672fa0ff3f5f3624852d0c3c6e231 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 2 Jul 2020 16:46:36 -0700 Subject: [PATCH 38/43] Add IdentityTokenRefreshOptions --- .../core/credential/SimpleTokenCache.java | 9 +-- .../core/credential/TokenRefreshOptions.java | 54 ++++--------- .../core/credential/TokenCacheTests.java | 81 +++++++------------ .../azure/identity/CredentialBuilderBase.java | 26 +++--- .../implementation/IdentityClientOptions.java | 16 +--- .../IdentityTokenRefreshOptions.java | 25 ++++++ 6 files changed, 86 insertions(+), 125 deletions(-) create mode 100644 sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index 8f067ca04257..d7ef3fe19a34 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -17,12 +17,12 @@ * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { + private static final Duration REFRESH_TIMEOUT = Duration.ofSeconds(30); private final AtomicReference> wip; private volatile AccessToken cache; private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); private final Supplier> tokenSupplier; private final Predicate shouldRefresh; - private final Duration refreshRetryTimeout; private final ClientLogger logger = new ClientLogger(SimpleTokenCache.class); /** @@ -45,7 +45,6 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO this.tokenSupplier = tokenSupplier; this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() .minus(tokenRefreshOptions.getOffset())); - this.refreshRetryTimeout = tokenRefreshOptions.getRetryTimeout(); } /** @@ -98,11 +97,11 @@ public Mono getToken() { cache = accessToken; monoProcessor.onNext(accessToken); monoProcessor.onComplete(); - nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); return Mono.just(accessToken); } else if (signal.isOnError() && error != null) { // ERROR logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); - nextTokenRefresh = OffsetDateTime.now().plus(refreshRetryTimeout); + nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); return fallback.switchIfEmpty(Mono.error(error)); } else { // NO REFRESH monoProcessor.onComplete(); @@ -139,7 +138,7 @@ private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { Duration tte = Duration.between(now, cache.getExpiresAt()); info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") .append(tte.isNegative() ? "after" : "before").append(" expiry. ") - .append("Retry may be attempted after ").append(refreshRetryTimeout.getSeconds()).append(" seconds."); + .append("Retry may be attempted after ").append(REFRESH_TIMEOUT.getSeconds()).append(" seconds."); if (!tte.isNegative()) { info.append(" The token currently cached will be used."); } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java index 7be5ba65d740..f00d795a5412 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -9,55 +9,29 @@ * The options to configure the token refresh behavior. */ public class TokenRefreshOptions { - private static final Duration DEFAULT_RETRY_TIMEOUT = Duration.ofSeconds(30); private static final Duration DEFAULT_OFFSET = Duration.ofMinutes(2); - private Duration retryTimeout = DEFAULT_RETRY_TIMEOUT; - private Duration offset = DEFAULT_OFFSET; - - /** - * Returns a Duration value representing the amount of time to wait before retrying a token refresh. This is to - * prevent sending too many requests to the authentication service. - * - * @return the duration value representing the amount of time to wait before retrying a token refresh - */ - public Duration getRetryTimeout() { - return retryTimeout; - } - - /** - * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to - * prevent sending too many requests to the authentication service. - * - * @param retryTimeout the amount of time to wait before retrying a token refresh - * @return the updated TokenRefreshOptions object - */ - public TokenRefreshOptions setRetryTimeout(Duration retryTimeout) { - this.retryTimeout = retryTimeout; - return this; - } - /** * Returns a Duration value representing the amount of time to subtract from the token expiry time, whereupon * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the * token. * + * This is used in {@link SimpleTokenCache} and {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} + * to proactively retrieve a more up-to-date token before the cached token gets too close to its expiry. You can + * configure this from the {@code .tokenRefreshOffset(Duration)} method from any credential builder in the + * azure-identity library. + * + * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on + * high load), or you would like to simply keep a more up-to-date token in the cache (more robust against token + * refresh API down times). The user is responsible for specifying a valid offset. + * + * When a proactive token refresh fails but the previously cached token is still valid, + * {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} will NOT fail but return the previous valid + * token. Another proactive refresh will be attempted in 30 seconds. + * * @return the duration value representing the amount of time to subtract from the token expiry time */ public Duration getOffset() { - return offset; - } - - /** - * Specifies the Duration value representing the amount of time to subtract from the token expiry time, whereupon - * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the - * token. - * - * @param offset the duration representing the amount of time to subtract from the token expiry time - * @return the updated TokenRefreshOptions object - */ - public TokenRefreshOptions setOffset(Duration offset) { - this.offset = offset; - return this; + return DEFAULT_OFFSET; } } diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index ba79b8ea5c4d..9064a80231c7 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -13,7 +13,6 @@ import java.time.OffsetDateTime; import java.util.Random; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -57,7 +56,7 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { SimpleTokenCache cache = new SimpleTokenCache(() -> { refreshes.incrementAndGet(); return remoteGetTokenThatExpiresSoonAsync(1000, 0); - }, new TokenRefreshOptions().setRetryTimeout(Duration.ZERO)); + }); CountDownLatch latch = new CountDownLatch(1); @@ -74,7 +73,7 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { .subscribe(); latch.await(); - Assertions.assertTrue(refreshes.get() <= 11); + Assertions.assertEquals(2, refreshes.get()); } @Test @@ -82,13 +81,13 @@ public void testProactiveRefreshBeforeExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), - new TokenRefreshOptions().setOffset(Duration.ofSeconds(58)).setRetryTimeout(Duration.ofSeconds(1))); + new TestTokenRefreshOptions(Duration.ofSeconds(28))); // refresh at second 32, just past REFRESH_TIMEOUT CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.interval(Duration.ofSeconds(1)) - .take(5) + Flux.interval(Duration.ofSeconds(2)) + .take(20) // 38 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -110,14 +109,14 @@ public void testProactiveRefreshBeforeExpiry() throws Exception { public void testRefreshAfterExpiry() throws Exception { AtomicInteger latency = new AtomicInteger(1); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 1000), - new TokenRefreshOptions().setOffset(Duration.ofSeconds(1)).setRetryTimeout(Duration.ofSeconds(5))); + () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 15 * 1000), + new TestTokenRefreshOptions(Duration.ZERO)); // refresh at second 30 because of REFRESH_TIMEOUT CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); - Flux.interval(Duration.ofSeconds(1)) - .take(4) + Flux.interval(Duration.ofSeconds(2)) + .take(10) // 38 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -131,8 +130,7 @@ public void testRefreshAfterExpiry() throws Exception { .subscribe(); latch.await(); - System.out.println("MaxMillis:" + maxMillis.get()); - Assertions.assertTrue(maxMillis.get() >= 5000); + Assertions.assertTrue(maxMillis.get() >= 15000); } @Test @@ -140,15 +138,15 @@ public void testProactiveRefreshError() throws Exception { AtomicInteger latency = new AtomicInteger(1); AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 3 * 1000, 2, tryCount), - new TokenRefreshOptions().setOffset(Duration.ofSeconds(2)).setRetryTimeout(Duration.ofSeconds(1))); + () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), + new TestTokenRefreshOptions(Duration.ofSeconds(28))); // refresh at second 32, just past REFRESH_TIMEOUT CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); AtomicInteger errorCount = new AtomicInteger(0); - Flux.interval(Duration.ofSeconds(1)) - .take(6) + Flux.interval(Duration.ofSeconds(2)) + .take(32) // 64 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -173,15 +171,15 @@ public void testProactiveRefreshErrorTimeout() throws Exception { AtomicInteger latency = new AtomicInteger(1); AtomicInteger tryCount = new AtomicInteger(0); SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 4 * 1000, 2, tryCount), - new TokenRefreshOptions().setOffset(Duration.ofSeconds(2)).setRetryTimeout(Duration.ofSeconds(1))); + () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), + new TestTokenRefreshOptions(Duration.ofSeconds(28))); // refresh at second 32, just past REFRESH_TIMEOUT CountDownLatch latch = new CountDownLatch(1); AtomicLong maxMillis = new AtomicLong(0); AtomicInteger errorCount = new AtomicInteger(0); - Flux.interval(Duration.ofSeconds(1)) - .take(8) + Flux.interval(Duration.ofSeconds(2)) + .take(32) // 64 seconds after first token, making sure of a refresh .flatMap(i -> { OffsetDateTime start = OffsetDateTime.now(); return cache.getToken() @@ -200,36 +198,6 @@ public void testProactiveRefreshErrorTimeout() throws Exception { Assertions.assertEquals(0, errorCount.get()); // Only the error after expiresAt will be propagated } - @Test - public void testMultipleThreadsAfterExpiry() throws Exception { - AtomicLong refreshes = new AtomicLong(0); - - // token expires on creation. Run this 100 times to simulate running the application a long time - SimpleTokenCache cache = new SimpleTokenCache(() -> { - refreshes.incrementAndGet(); - return remoteGetTokenThatExpiresSoonAsync(1000, 3 * 1000); - }, new TokenRefreshOptions().setOffset(Duration.ZERO).setRetryTimeout(Duration.ZERO)); - - CountDownLatch latch = new CountDownLatch(100); - - cache.getToken().then(Mono.delay(Duration.ofSeconds(5))).block(); - - Flux.range(1, 100) - .flatMap(i -> Mono.just(OffsetDateTime.now()) - .subscribeOn(Schedulers.newParallel("pool", 100)) - .flatMap(start -> cache.getToken() - .doOnNext(t -> { - if (!t.isExpired()) { - latch.countDown(); - } - }))) // expect all getToken() calls to return a token - .subscribe(); - - latch.await(20, TimeUnit.SECONDS); - Assertions.assertEquals(2, refreshes.get()); - Assertions.assertEquals(0, latch.getCount()); - } - private Mono remoteGetTokenAsync(long delayInMillis) { return Mono.delay(Duration.ofMillis(delayInMillis)) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); @@ -293,4 +261,17 @@ public boolean isExpired() { return OffsetDateTime.now().isAfter(expiry); } } + + private static class TestTokenRefreshOptions extends TokenRefreshOptions { + private final Duration offset; + + private TestTokenRefreshOptions(Duration offset) { + this.offset = offset; + } + + @Override + public Duration getOffset() { + return offset; + } + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java index b76772ab86c7..ddb372ee561d 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java @@ -3,6 +3,7 @@ package com.azure.identity; +import com.azure.core.credential.SimpleTokenCache; import com.azure.core.http.HttpClient; import com.azure.core.http.HttpPipeline; import com.azure.core.http.ProxyOptions; @@ -95,8 +96,16 @@ public T httpClient(HttpClient client) { * token will be considered expired at and after the time of (actual * expiry - token refresh offset). The default offset is 2 minutes. * - * This is useful when network is congested and a request containing the - * token takes longer than normal to get to the server. + * This is used in {@link SimpleTokenCache} and {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} + * to proactively retrieve a more up-to-date token before the cached token gets too close to its expiry. + * + * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on + * high load), or you would like to simply keep a more up-to-date token in the cache (more robust against token + * refresh API down times). The user is responsible for specifying a valid offset. + * + * When a proactive token refresh fails but the previously cached token is still valid, + * {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} will NOT fail but return the previous valid + * token. Another proactive refresh will be attempted in 30 seconds. * * @param tokenRefreshOffset the duration before the actual expiry of a token to refresh it * @return An updated instance of this builder with the token refresh offset set as specified. @@ -106,17 +115,4 @@ public T tokenRefreshOffset(Duration tokenRefreshOffset) { this.identityClientOptions.setTokenRefreshOffset(tokenRefreshOffset); return (T) this; } - - /** - * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to - * prevent sending too many requests to the authentication service. - * - * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh - * @return An updated instance of this builder with the token refresh offset set as specified. - */ - @SuppressWarnings("unchecked") - public T setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { - this.identityClientOptions.setTokenRefreshRetryTimeout(tokenRefreshRetryTimeout); - return (T) this; - } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index fef5d1bbe698..885ba681372d 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java @@ -47,7 +47,7 @@ public final class IdentityClientOptions { private ProxyOptions proxyOptions; private HttpPipeline httpPipeline; private ExecutorService executorService; - private TokenRefreshOptions tokenRefreshOptions = new TokenRefreshOptions(); + private IdentityTokenRefreshOptions tokenRefreshOptions = new IdentityTokenRefreshOptions(); private HttpClient httpClient; private boolean allowUnencryptedCache; private boolean sharedTokenCacheEnabled; @@ -211,20 +211,6 @@ public IdentityClientOptions setTokenRefreshOffset(Duration tokenRefreshOffset) return this; } - /** - * Specifies a Duration value representing the amount of time to wait before retrying a token refresh. This is to - * prevent sending too many requests to the authentication service. - * - * @param tokenRefreshRetryTimeout the amount of time to wait before retrying a token refresh - * @return IdentityClientOptions - * @throws NullPointerException If {@code tokenRefreshRetryTimeout} is null. - */ - public IdentityClientOptions setTokenRefreshRetryTimeout(Duration tokenRefreshRetryTimeout) { - Objects.requireNonNull(tokenRefreshRetryTimeout, "The token refresh retry timeout cannot be null."); - this.tokenRefreshOptions.setRetryTimeout(tokenRefreshRetryTimeout); - return this; - } - /** * Specifies the HttpClient to send use for requests. * @param httpClient the http client to use for requests diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java new file mode 100644 index 000000000000..167cfbec9634 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java @@ -0,0 +1,25 @@ +package com.azure.identity.implementation; + +import com.azure.core.credential.TokenRefreshOptions; + +import java.time.Duration; + +/** + * The options to configure the token refresh behavior for azure-identity credentials. + */ +public class IdentityTokenRefreshOptions extends TokenRefreshOptions { + private Duration offset = Duration.ofMinutes(2); + + @Override + public Duration getOffset() { + return offset; + } + + /** + * Sets the duration value representing the amount of time to subtract from the token expiry time. + * @param offset the duration value representing the amount of time to subtract from the token expiry time + */ + public void setOffset(Duration offset) { + this.offset = offset; + } +} From 392e5e76a0e73a4773bc787a8278730e789d5a6e Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 2 Jul 2020 16:52:29 -0700 Subject: [PATCH 39/43] Fix javadocs --- .../java/com/azure/core/credential/TokenRefreshOptions.java | 2 +- .../src/main/java/com/azure/identity/CredentialBuilderBase.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java index f00d795a5412..1aaf326d3064 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java @@ -21,7 +21,7 @@ public class TokenRefreshOptions { * configure this from the {@code .tokenRefreshOffset(Duration)} method from any credential builder in the * azure-identity library. * - * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on + * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on * high load), or you would like to simply keep a more up-to-date token in the cache (more robust against token * refresh API down times). The user is responsible for specifying a valid offset. * diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java index ddb372ee561d..5c2c73d9b019 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/CredentialBuilderBase.java @@ -99,7 +99,7 @@ public T httpClient(HttpClient client) { * This is used in {@link SimpleTokenCache} and {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} * to proactively retrieve a more up-to-date token before the cached token gets too close to its expiry. * - * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on + * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on * high load), or you would like to simply keep a more up-to-date token in the cache (more robust against token * refresh API down times). The user is responsible for specifying a valid offset. * From ca3df3d6d245d1ee1c3e1f1fc0c16ddb80be18a2 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 6 Jul 2020 15:40:25 -0700 Subject: [PATCH 40/43] Checkstyle - license header --- .../identity/implementation/IdentityTokenRefreshOptions.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java index 167cfbec9634..50ca53818fc7 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityTokenRefreshOptions.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.azure.identity.implementation; import com.azure.core.credential.TokenRefreshOptions; From 587c3cc53c8271c6100ea4ccd85f536e49e6f1ae Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 6 Jul 2020 16:51:09 -0700 Subject: [PATCH 41/43] checkstyle - final --- .../test/java/com/azure/core/credential/TokenCacheTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 9064a80231c7..7effa8798590 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -262,7 +262,7 @@ public boolean isExpired() { } } - private static class TestTokenRefreshOptions extends TokenRefreshOptions { + private static final class TestTokenRefreshOptions extends TokenRefreshOptions { private final Duration offset; private TestTokenRefreshOptions(Duration offset) { From f2c7ca273127c86d3ed7b773c73de4cdcfb329b8 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 8 Jul 2020 17:28:43 -0700 Subject: [PATCH 42/43] Use released azure-core 1.7.0-beta.1 --- eng/versioning/version_client.txt | 5 +---- sdk/identity/azure-identity/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index cfc1d99ae520..640a174f0f91 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -67,10 +67,6 @@ com.microsoft.azure:spring-data-cosmosdb;2.3.0;2.3.1-beta.1 # Format; # unreleased_:;dependency-version # note: The unreleased dependencies will not be manipulated with the automatic PR creation code. -unreleased_com.azure:azure-core;1.7.0-beta.1 -unreleased_com.azure:azure-core-http-netty;1.6.0-beta.1 -unreleased_com.azure:azure-core-test;1.4.0-beta.1 -unreleased_com.azure:azure-messaging-servicebus;7.0.0-beta.4 # Released Beta dependencies: Copy the entry from above, prepend "beta_", remove the current # version and set the version to the released beta. Released beta dependencies are only valid @@ -79,3 +75,4 @@ unreleased_com.azure:azure-messaging-servicebus;7.0.0-beta.4 # Format; # beta_:;dependency-version # note: Released beta versions will not be manipulated with the automatic PR creation code. +beta_com.azure:azure-core;1.7.0-beta.1 diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 7aa7587cdbe9..80e2d8082866 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -27,7 +27,7 @@ com.azure azure-core - 1.7.0-beta.1 + 1.7.0-beta.1 com.microsoft.azure @@ -88,7 +88,7 @@ com.azure azure-core-http-netty - 1.6.0-beta.1 + 1.5.3 test From b99c60c8e3c4985a0f77b117b8587c0ade51695f Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 8 Jul 2020 17:32:35 -0700 Subject: [PATCH 43/43] Reset changes in core --- .../ClaimsBasedSecurityChannelTest.java | 3 +- sdk/core/azure-core/CHANGELOG.md | 1 - .../azure/core/credential/AccessToken.java | 2 +- .../core/credential/SimpleTokenCache.java | 135 +++------------ .../core/credential/TokenCredential.java | 9 - .../core/credential/TokenRefreshOptions.java | 37 ---- .../BearerTokenAuthenticationPolicy.java | 3 +- .../core/credential/TokenCacheTests.java | 160 +----------------- 8 files changed, 30 insertions(+), 320 deletions(-) delete mode 100644 sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java index 685de4005242..5a847b48b063 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java @@ -107,7 +107,8 @@ public void teardown() { @Test public void authorizesSasToken() { // Arrange - final Date expectedDate = Date.from(validUntil.toInstant()); + // Subtracting two minutes because the AccessToken does this internally. + final Date expectedDate = Date.from(validUntil.minusMinutes(2).toInstant()); final ClaimsBasedSecurityChannel cbsChannel = new ClaimsBasedSecurityChannel(Mono.just(requestResponseChannel), tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, options); diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 9e7e61c9fe37..c3a2c734ee58 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -2,7 +2,6 @@ ## 1.7.0-beta.1 (Unreleased) -- Added `TokenRefreshOptions()` to `TokenCredential`, with a default token refresh offset of 2 minutes, and a default token refresh retry timeout of 30 seconds. ## 1.6.0 (2020-07-02) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java index b12996a55f37..b8673a9fc960 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessToken.java @@ -19,7 +19,7 @@ public class AccessToken { */ public AccessToken(String token, OffsetDateTime expiresAt) { this.token = token; - this.expiresAt = expiresAt; + this.expiresAt = expiresAt.minusMinutes(2); // 2 minutes before token expires } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java index d7ef3fe19a34..b80416ca9952 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/SimpleTokenCache.java @@ -3,27 +3,25 @@ package com.azure.core.credential; -import com.azure.core.util.logging.ClientLogger; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.ReplayProcessor; -import java.time.Duration; -import java.time.OffsetDateTime; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; /** * A token cache that supports caching a token and refreshing it. */ public class SimpleTokenCache { - private static final Duration REFRESH_TIMEOUT = Duration.ofSeconds(30); - private final AtomicReference> wip; - private volatile AccessToken cache; - private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); + private static final int REFRESH_TIMEOUT_SECONDS = 30; + + private final AtomicBoolean wip; + private AccessToken cache; + private final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); + private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); private final Supplier> tokenSupplier; - private final Predicate shouldRefresh; - private final ClientLogger logger = new ClientLogger(SimpleTokenCache.class); /** * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". @@ -31,20 +29,8 @@ public class SimpleTokenCache { * @param tokenSupplier a method to get a new token */ public SimpleTokenCache(Supplier> tokenSupplier) { - this(tokenSupplier, new TokenRefreshOptions()); - } - - /** - * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". - * - * @param tokenSupplier a method to get a new token - * @param tokenRefreshOptions the options to configure the token refresh behavior - */ - public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshOptions tokenRefreshOptions) { - this.wip = new AtomicReference<>(); + this.wip = new AtomicBoolean(false); this.tokenSupplier = tokenSupplier; - this.shouldRefresh = accessToken -> OffsetDateTime.now().isAfter(accessToken.getExpiresAt() - .minus(tokenRefreshOptions.getOffset())); } /** @@ -52,97 +38,18 @@ public SimpleTokenCache(Supplier> tokenSupplier, TokenRefreshO * @return a Publisher that emits an AccessToken */ public Mono getToken() { + if (cache != null && !cache.isExpired()) { + return Mono.just(cache); + } return Mono.defer(() -> { - try { - if (wip.compareAndSet(null, MonoProcessor.create())) { - final MonoProcessor monoProcessor = wip.get(); - OffsetDateTime now = OffsetDateTime.now(); - Mono tokenRefresh; - Mono fallback; - if (cache != null && !shouldRefresh.test(cache)) { - // fresh cache & no need to refresh - tokenRefresh = Mono.empty(); - fallback = Mono.just(cache); - } else if (cache == null || cache.isExpired()) { - // no token to use - if (now.isAfter(nextTokenRefresh)) { - // refresh immediately - tokenRefresh = Mono.defer(tokenSupplier); - } else { - // wait for timeout, then refresh - tokenRefresh = Mono.defer(tokenSupplier) - .delaySubscription(Duration.between(now, nextTokenRefresh)); - } - // cache doesn't exist or expired, no fallback - fallback = Mono.empty(); - } else { - // token available, but close to expiry - if (now.isAfter(nextTokenRefresh)) { - // refresh immediately - tokenRefresh = Mono.defer(tokenSupplier); - } else { - // still in timeout, do not refresh - tokenRefresh = Mono.empty(); - } - // cache hasn't expired, ignore refresh error this time - fallback = Mono.just(cache); - } - return tokenRefresh - .materialize() - .flatMap(signal -> { - AccessToken accessToken = signal.get(); - Throwable error = signal.getThrowable(); - if (signal.isOnNext() && accessToken != null) { // SUCCESS - logger.info(refreshLog(cache, now, "Acquired a new access token")); - cache = accessToken; - monoProcessor.onNext(accessToken); - monoProcessor.onComplete(); - nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); - return Mono.just(accessToken); - } else if (signal.isOnError() && error != null) { // ERROR - logger.error(refreshLog(cache, now, "Failed to acquire a new access token")); - nextTokenRefresh = OffsetDateTime.now().plus(REFRESH_TIMEOUT); - return fallback.switchIfEmpty(Mono.error(error)); - } else { // NO REFRESH - monoProcessor.onComplete(); - return fallback; - } - }) - .doOnError(monoProcessor::onError) - .doOnTerminate(() -> wip.set(null)); - } else if (cache != null && !cache.isExpired()) { - // another thread might be refreshing the token proactively, but the current token is still valid - return Mono.just(cache); - } else { - // another thread is definitely refreshing the expired token - MonoProcessor monoProcessor = wip.get(); - if (monoProcessor == null) { - // the refreshing thread has finished - return Mono.just(cache); - } else { - // wait for refreshing thread to finish but defer to updated cache in case just missed onNext() - return monoProcessor.switchIfEmpty(Mono.defer(() -> Mono.just(cache))); - } - } - } catch (Throwable t) { - return Mono.error(t); + if (!wip.getAndSet(true)) { + return tokenSupplier.get().doOnNext(ac -> cache = ac) + .doOnNext(sink::next) + .doOnError(sink::error) + .doOnTerminate(() -> wip.set(false)); + } else { + return emitterProcessor.next(); } }); } - - private String refreshLog(AccessToken cache, OffsetDateTime now, String log) { - StringBuilder info = new StringBuilder(log); - if (cache == null) { - info.append("."); - } else { - Duration tte = Duration.between(now, cache.getExpiresAt()); - info.append(" at ").append(tte.abs().getSeconds()).append(" seconds ") - .append(tte.isNegative() ? "after" : "before").append(" expiry. ") - .append("Retry may be attempted after ").append(REFRESH_TIMEOUT.getSeconds()).append(" seconds."); - if (!tte.isNegative()) { - info.append(" The token currently cached will be used."); - } - } - return info.toString(); - } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java index 0408548dec65..79c29d6e0416 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenCredential.java @@ -16,13 +16,4 @@ public interface TokenCredential { * @return a Publisher that emits a single access token */ Mono getToken(TokenRequestContext request); - - /** - * The options to configure the token refresh behavior. - * - * @return the current offset for token refresh - */ - default TokenRefreshOptions getTokenRefreshOptions() { - return new TokenRefreshOptions(); - } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java deleted file mode 100644 index 1aaf326d3064..000000000000 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/TokenRefreshOptions.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.core.credential; - -import java.time.Duration; - -/** - * The options to configure the token refresh behavior. - */ -public class TokenRefreshOptions { - private static final Duration DEFAULT_OFFSET = Duration.ofMinutes(2); - - /** - * Returns a Duration value representing the amount of time to subtract from the token expiry time, whereupon - * attempts will be made to refresh the token. By default this will occur two minutes prior to the expiry of the - * token. - * - * This is used in {@link SimpleTokenCache} and {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} - * to proactively retrieve a more up-to-date token before the cached token gets too close to its expiry. You can - * configure this from the {@code .tokenRefreshOffset(Duration)} method from any credential builder in the - * azure-identity library. - * - * Extending this offset is recommended if it takes > 2 minutes to reach the service (application is running on - * high load), or you would like to simply keep a more up-to-date token in the cache (more robust against token - * refresh API down times). The user is responsible for specifying a valid offset. - * - * When a proactive token refresh fails but the previously cached token is still valid, - * {@link com.azure.core.http.policy.BearerTokenAuthenticationPolicy} will NOT fail but return the previous valid - * token. Another proactive refresh will be attempted in 30 seconds. - * - * @return the duration value representing the amount of time to subtract from the token expiry time - */ - public Duration getOffset() { - return DEFAULT_OFFSET; - } -} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index ae9a4616c975..f643b4975664 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -37,8 +37,7 @@ public BearerTokenAuthenticationPolicy(TokenCredential credential, String... sco assert scopes.length > 0; this.credential = credential; this.scopes = scopes; - this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes)), - credential.getTokenRefreshOptions()); + this.cache = new SimpleTokenCache(() -> credential.getToken(new TokenRequestContext().addScopes(scopes))); } @Override diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 7effa8798590..1aa0689b8d3c 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -49,7 +49,7 @@ public void testOnlyOneThreadRefreshesToken() throws Exception { } @Test - public void testMultipleThreadsWaitForTimeout() throws Exception { + public void testLongRunningWontOverflow() throws Exception { AtomicLong refreshes = new AtomicLong(0); // token expires on creation. Run this 100 times to simulate running the application a long time @@ -68,134 +68,15 @@ public void testMultipleThreadsWaitForTimeout() throws Exception { .flatMap(start -> cache.getToken() .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) .doOnNext(millis -> { +// System.out.format("Thread: %s\tDuration: %smillis%n", +// Thread.currentThread().getName(), Duration.between(start, OffsetDateTime.now()).toMillis()); }))) .doOnComplete(latch::countDown) .subscribe(); latch.await(); - Assertions.assertEquals(2, refreshes.get()); - } - - @Test - public void testProactiveRefreshBeforeExpiry() throws Exception { - AtomicInteger latency = new AtomicInteger(1); - SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 60 * 1000), - new TestTokenRefreshOptions(Duration.ofSeconds(28))); // refresh at second 32, just past REFRESH_TIMEOUT - - CountDownLatch latch = new CountDownLatch(1); - AtomicLong maxMillis = new AtomicLong(0); - - Flux.interval(Duration.ofSeconds(2)) - .take(20) // 38 seconds after first token, making sure of a refresh - .flatMap(i -> { - OffsetDateTime start = OffsetDateTime.now(); - return cache.getToken() - .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) - .doOnNext(millis -> { - if (millis > maxMillis.get()) { - maxMillis.set(millis); - } - }); - }).doOnComplete(latch::countDown) - .subscribe(); - - latch.await(); - Assertions.assertTrue(maxMillis.get() >= 2000); - Assertions.assertTrue(maxMillis.get() < 3000); // Big enough for any latency, small enough to make sure no get token is called twice - } - - @Test - public void testRefreshAfterExpiry() throws Exception { - AtomicInteger latency = new AtomicInteger(1); - SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenThatExpiresSoonAsync(1000 * latency.getAndIncrement(), 15 * 1000), - new TestTokenRefreshOptions(Duration.ZERO)); // refresh at second 30 because of REFRESH_TIMEOUT - - CountDownLatch latch = new CountDownLatch(1); - AtomicLong maxMillis = new AtomicLong(0); - - Flux.interval(Duration.ofSeconds(2)) - .take(10) // 38 seconds after first token, making sure of a refresh - .flatMap(i -> { - OffsetDateTime start = OffsetDateTime.now(); - return cache.getToken() - .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) - .doOnNext(millis -> { - if (millis > maxMillis.get()) { - maxMillis.set(millis); - } - }); - }).doOnComplete(latch::countDown) - .subscribe(); - - latch.await(); - Assertions.assertTrue(maxMillis.get() >= 15000); - } - - @Test - public void testProactiveRefreshError() throws Exception { - AtomicInteger latency = new AtomicInteger(1); - AtomicInteger tryCount = new AtomicInteger(0); - SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithPersistentError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), - new TestTokenRefreshOptions(Duration.ofSeconds(28))); // refresh at second 32, just past REFRESH_TIMEOUT - - CountDownLatch latch = new CountDownLatch(1); - AtomicLong maxMillis = new AtomicLong(0); - AtomicInteger errorCount = new AtomicInteger(0); - - Flux.interval(Duration.ofSeconds(2)) - .take(32) // 64 seconds after first token, making sure of a refresh - .flatMap(i -> { - OffsetDateTime start = OffsetDateTime.now(); - return cache.getToken() - .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) - .doOnNext(millis -> { - if (millis > maxMillis.get()) { - maxMillis.set(millis); - } - }) - .doOnError(t -> errorCount.incrementAndGet()); - }).doOnTerminate(latch::countDown) - .subscribe(); - - latch.await(); - Assertions.assertTrue(maxMillis.get() >= 1000); - Assertions.assertTrue(maxMillis.get() < 2000); // Big enough for any latency, small enough to make sure no get token is called twice - Assertions.assertEquals(1, errorCount.get()); // Only the error after expiresAt will be propagated - } - - @Test - public void testProactiveRefreshErrorTimeout() throws Exception { - AtomicInteger latency = new AtomicInteger(1); - AtomicInteger tryCount = new AtomicInteger(0); - SimpleTokenCache cache = new SimpleTokenCache( - () -> remoteGetTokenWithTemporaryError(1000 * latency.getAndIncrement(), 60 * 1000, 2, tryCount), - new TestTokenRefreshOptions(Duration.ofSeconds(28))); // refresh at second 32, just past REFRESH_TIMEOUT - - CountDownLatch latch = new CountDownLatch(1); - AtomicLong maxMillis = new AtomicLong(0); - AtomicInteger errorCount = new AtomicInteger(0); - - Flux.interval(Duration.ofSeconds(2)) - .take(32) // 64 seconds after first token, making sure of a refresh - .flatMap(i -> { - OffsetDateTime start = OffsetDateTime.now(); - return cache.getToken() - .map(t -> Duration.between(start, OffsetDateTime.now()).toMillis()) - .doOnNext(millis -> { - if (millis > maxMillis.get()) { - maxMillis.set(millis); - } - }) - .doOnError(t -> errorCount.incrementAndGet()); - }).doOnTerminate(latch::countDown) - .subscribe(); - - latch.await(); - Assertions.assertTrue(maxMillis.get() >= 3000); - Assertions.assertEquals(0, errorCount.get()); // Only the error after expiresAt will be propagated + // At most 10 requests should do actual token acquisition, use 11 for safe + Assertions.assertTrue(refreshes.get() <= 11); } private Mono remoteGetTokenAsync(long delayInMillis) { @@ -214,24 +95,6 @@ private Mono incrementalRemoteGetTokenAsync(AtomicInteger latency) .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)))); } - private Mono remoteGetTokenWithTemporaryError(long delayInMillis, long validityInMillis, int errorAt, AtomicInteger tryCount) { - if (tryCount.incrementAndGet() == errorAt) { - return Mono.error(new RuntimeException("Expected error")); - } else { - return Mono.delay(Duration.ofMillis(delayInMillis)) - .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)), validityInMillis)); - } - } - - private Mono remoteGetTokenWithPersistentError(long delayInMillis, long validityInMillis, int errorAfter, AtomicInteger tryCount) { - if (tryCount.incrementAndGet() >= errorAfter) { - return Mono.error(new RuntimeException("Expected error")); - } else { - return Mono.delay(Duration.ofMillis(delayInMillis)) - .map(l -> new Token(Integer.toString(RANDOM.nextInt(100)), validityInMillis)); - } - } - private static class Token extends AccessToken { private String token; private OffsetDateTime expiry; @@ -261,17 +124,4 @@ public boolean isExpired() { return OffsetDateTime.now().isAfter(expiry); } } - - private static final class TestTokenRefreshOptions extends TokenRefreshOptions { - private final Duration offset; - - private TestTokenRefreshOptions(Duration offset) { - this.offset = offset; - } - - @Override - public Duration getOffset() { - return offset; - } - } }