diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 90fa3592f9bf2..6275c8b0c39ff 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -134,7 +134,7 @@ com.azure azure-identity - 1.1.0-beta.7 + 1.1.0-beta.8 com.azure diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 9488f46f47144..fb38ca6aca0d0 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -27,7 +27,7 @@ com.azure:azure-data-schemaregistry;1.0.0-beta.2;1.0.0-beta.3 com.azure:azure-data-schemaregistry-avro;1.0.0-beta.2;1.0.0-beta.3 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.9;1.1.0-beta.7 +com.azure:azure-identity;1.0.9;1.1.0-beta.8 com.azure:azure-identity-perf;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-messaging-eventhubs;5.1.2;5.2.0-beta.1 com.azure:azure-messaging-eventhubs-checkpointstore-blob;1.1.2;1.2.0-beta.2 @@ -75,7 +75,7 @@ com.microsoft.azure:spring-data-cosmosdb;3.0.0-beta.1;3.0.0-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.3 unreleased_com.azure:azure-core-test;1.4.0-beta.1 unreleased_com.azure:azure-messaging-servicebus;7.0.0-beta.4 unreleased_com.azure:azure-security-keyvault-keys;4.2.0-beta.6 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 5a847b48b0630..685de40052424 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); 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 b8673a9fc960a..b12996a55f37b 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 b80416ca99528..cfba9ace91bc6 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,30 @@ package com.azure.core.credential; -import reactor.core.publisher.FluxSink; -import reactor.core.publisher.FluxSink.OverflowStrategy; +import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; +import reactor.core.publisher.MonoProcessor; -import java.util.concurrent.atomic.AtomicBoolean; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.concurrent.atomic.AtomicReference; +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 final ReplayProcessor emitterProcessor = ReplayProcessor.create(1); - private final FluxSink sink = emitterProcessor.sink(OverflowStrategy.BUFFER); + // The delay after a refresh to attempt another token refresh + private static final Duration REFRESH_DELAY = Duration.ofSeconds(30); + // the offset before token expiry to attempt proactive token refresh + private static final Duration REFRESH_OFFSET = Duration.ofMinutes(5); + private final AtomicReference> wip; + private volatile AccessToken cache; + private volatile OffsetDateTime nextTokenRefresh = OffsetDateTime.now(); 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". @@ -29,8 +34,10 @@ public class SimpleTokenCache { * @param tokenSupplier a method to get a new token */ public SimpleTokenCache(Supplier> tokenSupplier) { - this.wip = new AtomicBoolean(false); + this.wip = new AtomicReference<>(); this.tokenSupplier = tokenSupplier; + this.shouldRefresh = accessToken -> OffsetDateTime.now() + .isAfter(accessToken.getExpiresAt().minus(REFRESH_OFFSET)); } /** @@ -38,18 +45,97 @@ 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(() -> { - 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(); + 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_DELAY); + 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_DELAY); + 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); } }); } + + 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_DELAY.getSeconds()).append(" seconds."); + if (!tte.isNegative()) { + info.append(" The token currently cached will be used."); + } + } + return info.toString(); + } } diff --git a/sdk/e2e/pom.xml b/sdk/e2e/pom.xml index 89c6e4da0620f..cf032813d2285 100644 --- a/sdk/e2e/pom.xml +++ b/sdk/e2e/pom.xml @@ -33,7 +33,7 @@ com.azure azure-identity - 1.1.0-beta.7 + 1.1.0-beta.8 com.azure diff --git a/sdk/identity/azure-identity-perf/pom.xml b/sdk/identity/azure-identity-perf/pom.xml index 56d2e7379410a..61af9d2214cb7 100644 --- a/sdk/identity/azure-identity-perf/pom.xml +++ b/sdk/identity/azure-identity-perf/pom.xml @@ -27,7 +27,7 @@ com.azure azure-identity - 1.1.0-beta.7 + 1.1.0-beta.8 com.azure diff --git a/sdk/identity/azure-identity-perf/src/main/java/com/azure/identity/perf/WriteCache.java b/sdk/identity/azure-identity-perf/src/main/java/com/azure/identity/perf/WriteCache.java index edee2684493f2..3c7351218aae7 100644 --- a/sdk/identity/azure-identity-perf/src/main/java/com/azure/identity/perf/WriteCache.java +++ b/sdk/identity/azure-identity-perf/src/main/java/com/azure/identity/perf/WriteCache.java @@ -9,8 +9,6 @@ import com.azure.perf.test.core.PerfStressOptions; import reactor.core.publisher.Mono; -import java.time.Duration; - public class WriteCache extends ServiceTest { private final SharedTokenCacheCredential credential; @@ -18,7 +16,6 @@ public WriteCache(PerfStressOptions options) { super(options); credential = new SharedTokenCacheCredentialBuilder() .clientId(CLI_CLIENT_ID) - .tokenRefreshOffset(Duration.ofMinutes(60)) .build(); } diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 660f6942e3de4..7ea05a1bef72f 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,8 +1,23 @@ # Release History -## 1.1.0-beta.7 (Unreleased) -- Added support for web apps (confidential apps) for `InteractiveBrowserCredential` and `AuthorizationCodeCredential`. A client secret is required on the builder for web apps. +## 1.1.0-beta.8 (Unreleased) + + +## 1.1.0-beta.7 (2020-07-23) + +### Features +- Added support for web apps (confidential apps) for `AuthorizationCodeCredential`. A client secret is required on the builder for web apps. - Added support for user assigned managed identities for `DefaultAzureCredential` with `.managedIdentityClientId()`. +- Added`AzureAuthorityHosts` to access well knwon authority hosts. +- Added `getClientId()` method in `AuthenticationRecord` + +### Breaking Changes +- Removed persistent caching support from `AuthorizationCodeCredential`. +- Removed `KnownAuthorityHosts` +- Removed `getCredentials()` method in `ChainedTokenCredential` & `DefaultAzureCredential` +- Changed return type of `serialize` method in `AuthenticationRecord` to `Mono`. +- Changed method signatures`enablePersistentCache(boolean)` and `allowUnencryptedCache(boolean)` on credential builders to `enablePersistentCache()` and `allowUnencryptedCache()` + ## 1.1.0-beta.6 (2020-07-10) - Added `.getCredentials()` method to `DefaultAzureCredential` and `ChainedTokenCredential` and added option `.addAll(Collection)` on `ChainedtokenCredentialBuilder`. diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 0092b19261600..7cbded89bd719 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -6,7 +6,7 @@ com.azure azure-identity - 1.1.0-beta.7 + 1.1.0-beta.8 Microsoft Azure client library for Identity This module contains client library for Microsoft Azure Identity. @@ -27,7 +27,7 @@ com.azure azure-core - 1.6.0 + 1.7.0-beta.3 com.microsoft.azure 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 13c76e29c1bed..73922c7a2fff4 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 @@ -89,21 +89,4 @@ public T httpClient(HttpClient client) { this.identityClientOptions.setHttpClient(client); return (T) this; } - - /** - * Sets how long before the actual token expiry to refresh the token. The - * 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. - * - * @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. - */ - @SuppressWarnings("unchecked") - public T tokenRefreshOffset(Duration tokenRefreshOffset) { - this.identityClientOptions.setTokenRefreshOffset(tokenRefreshOffset); - 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 1b35dad0dd533..5b9ee03d44d9f 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 @@ -22,7 +22,6 @@ */ @Immutable public final class DefaultAzureCredential extends ChainedTokenCredential { - /** * Creates default DefaultAzureCredential instance to use. This will use AZURE_CLIENT_ID, * AZURE_CLIENT_SECRET, and AZURE_TENANT_ID environment variables to create a 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 6b7f3f6d93792..81f2caf6193a7 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 @@ -37,7 +37,6 @@ @Immutable public class EnvironmentCredential implements TokenCredential { private final Configuration configuration; - private final IdentityClientOptions identityClientOptions; private final ClientLogger logger = new ClientLogger(EnvironmentCredential.class); private final TokenCredential tokenCredential; @@ -48,7 +47,6 @@ public class EnvironmentCredential implements TokenCredential { */ EnvironmentCredential(IdentityClientOptions identityClientOptions) { this.configuration = Configuration.getGlobalConfiguration().clone(); - this.identityClientOptions = identityClientOptions; TokenCredential targetCredential = null; String clientId = configuration.get(Configuration.PROPERTY_AZURE_CLIENT_ID); 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 15bd1807f9df2..aad793b2f5c65 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 @@ -122,5 +122,4 @@ private AccessToken updateCache(MsalToken msalToken) { identityClient.getTenantId(), identityClient.getClientId()))); return msalToken; } - } 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 6842568ac80ca..65d649be92fc8 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 @@ -92,6 +92,7 @@ public class IdentityClient { private static final String LINUX_MAC_PROCESS_ERROR_MESSAGE = "(.*)az:(.*)not found"; private static final String DEFAULT_WINDOWS_SYSTEM_ROOT = System.getenv("SystemRoot"); private static final String DEFAULT_MAC_LINUX_PATH = "/bin/"; + private static final Duration REFRESH_OFFSET = Duration.ofMinutes(5); private final ClientLogger logger = new ClientLogger(IdentityClient.class); private final IdentityClientOptions options; @@ -265,8 +266,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { ConfidentialClientApplication application = applicationBuilder.build(); return Mono.fromFuture(application.acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) - .build())) - .map(ar -> new MsalToken(ar, options)); + .build())).map(MsalToken::new); } catch (MalformedURLException e) { return Mono.error(e); } @@ -280,7 +280,7 @@ public Mono authenticateWithIntelliJ(TokenRequestContext request) { .build(); return publicClientApplicationAccessor.getValue() - .flatMap(pc -> Mono.fromFuture(pc.acquireToken(parameters)).map(ar -> new MsalToken(ar, options))); + .flatMap(pc -> Mono.fromFuture(pc.acquireToken(parameters)).map(MsalToken::new)); } else { throw logger.logExceptionAsError(new CredentialUnavailableException( @@ -381,7 +381,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) { @@ -408,7 +408,7 @@ public Mono authenticateWithConfidentialClient(TokenRequestContext return confidentialClientApplicationAccessor.getValue() .flatMap(confidentialClient -> Mono.fromFuture(() -> confidentialClient.acquireToken( ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build())) - .map(ar -> new MsalToken(ar, options))); + .map(MsalToken::new)); } private HttpPipeline setupPipeline(HttpClient httpClient) { @@ -437,7 +437,7 @@ public Mono authenticateWithUsernamePassword(TokenRequestContext requ 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))); + .map(MsalToken::new)); } /** @@ -449,31 +449,31 @@ public Mono authenticateWithUsernamePassword(TokenRequestContext requ */ public Mono authenticateWithPublicClientCache(TokenRequestContext request, IAccount account) { return publicClientApplicationAccessor.getValue() - .flatMap(pc -> Mono.fromFuture(() -> { - SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( - new HashSet<>(request.getScopes())); + .flatMap(pc -> Mono.fromFuture(() -> { + SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( + new HashSet<>(request.getScopes())); + if (account != null) { + parametersBuilder = parametersBuilder.account(account); + } + try { + return pc.acquireTokenSilently(parametersBuilder.build()); + } catch (MalformedURLException e) { + return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); + } + }).map(MsalToken::new) + .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(REFRESH_OFFSET))) + .switchIfEmpty(Mono.fromFuture(() -> { + SilentParameters.SilentParametersBuilder forceParametersBuilder = SilentParameters.builder( + new HashSet<>(request.getScopes())).forceRefresh(true); if (account != null) { - parametersBuilder = parametersBuilder.account(account); + forceParametersBuilder = forceParametersBuilder.account(account); } try { - return pc.acquireTokenSilently(parametersBuilder.build()); + return pc.acquireTokenSilently(forceParametersBuilder.build()); } catch (MalformedURLException e) { return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(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); - } - try { - return pc.acquireTokenSilently(forceParametersBuilder.build()); - } catch (MalformedURLException e) { - return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); - } - }).map(result -> new MsalToken(result, options)))); + }).map(MsalToken::new))); } /** @@ -484,16 +484,16 @@ public Mono authenticateWithPublicClientCache(TokenRequestContext req */ public Mono authenticateWithConfidentialClientCache(TokenRequestContext request) { return confidentialClientApplicationAccessor.getValue() - .flatMap(confidentialClient -> Mono.fromFuture(() -> { - SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( - new HashSet<>(request.getScopes())); - try { - return confidentialClient.acquireTokenSilently(parametersBuilder.build()); - } catch (MalformedURLException e) { - return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); - } - }).map(ar -> (AccessToken) new MsalToken(ar, options)) - .filter(t -> !t.isExpired())); + .flatMap(confidentialClient -> Mono.fromFuture(() -> { + SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( + new HashSet<>(request.getScopes())); + try { + return confidentialClient.acquireTokenSilently(parametersBuilder.build()); + } catch (MalformedURLException e) { + return getFailedCompletableFuture(logger.logExceptionAsError(new RuntimeException(e))); + } + }).map(ar -> (AccessToken) new MsalToken(ar)) + .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(REFRESH_OFFSET)))); } /** @@ -516,7 +516,7 @@ public Mono authenticateWithDeviceCode(TokenRequestContext request, OffsetDateTime.now().plusSeconds(dc.expiresIn()), dc.message()))).build(); return pc.acquireToken(parameters); }).onErrorMap(t -> new ClientAuthenticationException("Failed to acquire token with device code", null, t)) - .map(ar -> new MsalToken(ar, options))); + .map(MsalToken::new)); } /** @@ -536,8 +536,7 @@ public Mono authenticateWithVsCodeCredential(TokenRequestContext requ .build(); return publicClientApplicationAccessor.getValue() - .flatMap(pc -> Mono.fromFuture(pc.acquireToken(parameters)) - .map(ar -> new MsalToken(ar, options))); + .flatMap(pc -> Mono.fromFuture(pc.acquireToken(parameters)).map(MsalToken::new)); } /** @@ -562,7 +561,7 @@ public Mono authenticateWithAuthorizationCode(TokenRequestContext req .flatMap(pc -> Mono.fromFuture(() -> pc.acquireToken(parameters))); } return acquireToken.onErrorMap(t -> new ClientAuthenticationException( - "Failed to acquire token with authorization code", null, t)).map(ar -> new MsalToken(ar, options)); + "Failed to acquire token with authorization code", null, t)).map(MsalToken::new); } @@ -699,8 +698,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(); @@ -752,9 +750,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/IdentityClientOptions.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientOptions.java index eccf9e9f11bfe..25462ac68d3c1 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 @@ -15,7 +15,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; -import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; import java.util.function.Function; @@ -46,7 +45,6 @@ public final class IdentityClientOptions { private ProxyOptions proxyOptions; private HttpPipeline httpPipeline; private ExecutorService executorService; - private Duration tokenRefreshOffset = Duration.ofMinutes(2); private HttpClient httpClient; private boolean allowUnencryptedCache; private boolean sharedTokenCacheEnabled; @@ -184,31 +182,6 @@ public ExecutorService getExecutorService() { return executorService; } - /** - * @return how long before the actual token expiry to refresh the token. - */ - public Duration getTokenRefreshOffset() { - return tokenRefreshOffset; - } - - /** - * Sets how long before the actual token expiry to refresh the token. The - * 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. - * - * @param tokenRefreshOffset the duration before the actual expiry of a token to refresh it - * @return IdentityClientOptions - * @throws NullPointerException If {@code tokenRefreshOffset} is null. - */ - public IdentityClientOptions setTokenRefreshOffset(Duration tokenRefreshOffset) { - Objects.requireNonNull(tokenRefreshOffset, "The token refresh offset cannot be null."); - this.tokenRefreshOffset = tokenRefreshOffset; - 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/IdentityToken.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityToken.java deleted file mode 100644 index c6973b3d99a8f..0000000000000 --- 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 e8eca5066bd1a..8299c42422d21 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; @@ -21,10 +22,9 @@ public final class MsalToken extends IdentityToken { * * @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), - options); + OffsetDateTime.ofInstant(msalResult.expiresOnDate().toInstant(), ZoneOffset.UTC)); authenticationResult = msalResult; } 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 ac446bc06ad8e..ba71ce4ce2bfc 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 @@ -17,7 +17,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.time.Duration; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.UUID; @@ -66,41 +65,6 @@ public void testValidSecrets() throws Exception { .verifyComplete(); } - @Test - public void testValidSecretsWithTokenRefreshOffset() throws Exception { - // setup - String secret = "secret"; - String token1 = "token1"; - String token2 = "token2"; - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com"); - TokenRequestContext request2 = new TokenRequestContext().addScopes("https://vault.azure.net"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - Duration offset = Duration.ofMinutes(10); - - // mock - IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - 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 - ClientSecretCredential credential = new ClientSecretCredentialBuilder() - .tenantId(tenantId) - .clientId(clientId) - .clientSecret(secret) - .tokenRefreshOffset(offset) - .build(); - StepVerifier.create(credential.getToken(request1)) - .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()) - && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) - .verifyComplete(); - StepVerifier.create(credential.getToken(request2)) - .expectNextMatches(accessToken -> token2.equals(accessToken.getToken()) - && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) - .verifyComplete(); - } - @Test public void testInvalidSecrets() throws Exception { // setup diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java index ebaf988af2221..92567a00f008a 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java @@ -16,7 +16,6 @@ import org.powermock.modules.junit4.PowerMockRunner; import reactor.test.StepVerifier; -import java.time.Duration; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.UUID; @@ -88,64 +87,4 @@ public void testIMDS() throws Exception { && expiresOn.getSecond() == token.getExpiresAt().getSecond()) .verifyComplete(); } - - @Test - public void testMSIEndpointWithTokenRefreshOffset() throws Exception { - Configuration configuration = Configuration.getGlobalConfiguration(); - - try { - // setup - String endpoint = "http://localhost"; - String secret = "secret"; - String token1 = "token1"; - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - configuration.put("MSI_ENDPOINT", endpoint); - configuration.put("MSI_SECRET", secret); - Duration offset = Duration.ofMinutes(10); - - // mock - IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateToManagedIdentityEndpoint(endpoint, secret, request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt, offset)); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); - - // test - ManagedIdentityCredential credential = new ManagedIdentityCredentialBuilder() - .clientId(clientId) - .tokenRefreshOffset(offset) - .build(); - StepVerifier.create(credential.getToken(request1)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && expiresAt.getSecond() == token.getExpiresAt().getSecond()) - .verifyComplete(); - } finally { - // clean up - configuration.remove("MSI_ENDPOINT"); - configuration.remove("MSI_SECRET"); - } - } - - @Test - public void testIMDSWithTokenRefreshOffset() throws Exception { - // setup - String token1 = "token1"; - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); - OffsetDateTime expiresOn = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - Duration offset = Duration.ofMinutes(10); - - // mock - IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateToIMDSEndpoint(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn, offset)); - PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); - - // test - ManagedIdentityCredential credential = new ManagedIdentityCredentialBuilder() - .clientId(clientId) - .tokenRefreshOffset(offset) - .build(); - StepVerifier.create(credential.getToken(request)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && expiresOn.getSecond() == token.getExpiresAt().getSecond()) - .verifyComplete(); - } } 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 3284fc197e46b..ec8e6a590ba64 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()); } } 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 229e127344043..e26574ebb32f2 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); } /**