diff --git a/java/pom.xml b/java/pom.xml
index 4f8f516..9840a4d 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -17,13 +17,28 @@
com.azure
azure-identity
- 1.1.0
+ 1.2.2
com.microsoft.rest
client-runtime
1.7.0
+
+ com.azure
+ azure-core
+ 1.12.0
+
+
+ com.microsoft.azure
+ msal4j
+ 1.8.0
+
+
+ com.microsoft.azure
+ msal4j-persistence-extension
+ 1.0.0
+
com.microsoft.azure
azure
@@ -52,6 +67,55 @@
junit-jupiter-engine
5.5.2
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.3.3
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 2.0.2
+ test
+
+
+ org.powermock
+ powermock-api-mockito2
+ 2.0.2
+ test
+
+
+ net.java.dev.jna
+ jna-platform
+ 5.6.0
+
+
+ io.projectreactor
+ reactor-test
+ 3.3.11.RELEASE
+ test
+
+
+
+ com.google.code.gson
+ gson
+ 2.8.6
+ test
+
+
+
+ org.linguafranca.pwdb
+ KeePassJava2
+ 2.1.4
+
diff --git a/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java b/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java
new file mode 100644
index 0000000..a8d994e
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions;
+
+import com.azure.core.http.HttpClient;
+import com.azure.core.http.HttpPipeline;
+import com.azure.core.http.ProxyOptions;
+import com.azure.identity.implementation.IdentityClientOptions;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * The base class for all the credential builders.
+ * The constructor of this class is friendly, so must include it and in same folder as the extension class
+ * @param the type of the credential builder
+ */
+public abstract class CredentialBuilderBase> {
+ IdentityClientOptions identityClientOptions;
+
+ CredentialBuilderBase() {
+ this.identityClientOptions = new IdentityClientOptions();
+ }
+
+ /**
+ * Specifies the max number of retries when an authentication request fails.
+ *
+ * @param maxRetry the number of retries
+ * @return An updated instance of this builder with the max retry set as specified.
+ */
+ @SuppressWarnings("unchecked")
+ public T maxRetry(int maxRetry) {
+ this.identityClientOptions.setMaxRetry(maxRetry);
+ return (T) this;
+ }
+
+ /**
+ * Specifies a Function to calculate seconds of timeout on every retried request.
+ *
+ * @param retryTimeout the Function that returns a timeout in seconds given the number of retry
+ * @return An updated instance of this builder with the retry timeout set as specified.
+ */
+ @SuppressWarnings("unchecked")
+ public T retryTimeout(Function retryTimeout) {
+ this.identityClientOptions.setRetryTimeout(retryTimeout);
+ return (T) this;
+ }
+
+
+ /**
+ * Specifies the options for proxy configuration.
+ *
+ * @deprecated Configure the proxy options on the {@link HttpClient} instead and then set that
+ * client on the credential using {@link #httpClient(HttpClient)}.
+ *
+ * @param proxyOptions the options for proxy configuration
+ * @return An updated instance of this builder with the proxy options set as specified.
+ */
+ @Deprecated
+ @SuppressWarnings("unchecked")
+ public T proxyOptions(ProxyOptions proxyOptions) {
+ this.identityClientOptions.setProxyOptions(proxyOptions);
+ return (T) this;
+ }
+
+ /**
+ * Specifies the HttpPipeline to send all requests. This setting overrides the others.
+ *
+ * @param httpPipeline the HttpPipeline to send all requests
+ * @return An updated instance of this builder with the http pipeline set as specified.
+ */
+ @SuppressWarnings("unchecked")
+ public T httpPipeline(HttpPipeline httpPipeline) {
+ this.identityClientOptions.setHttpPipeline(httpPipeline);
+ return (T) this;
+ }
+
+ /**
+ * Sets the HTTP client to use for sending and receiving requests to and from the service.
+ *
+ * @param client The HTTP client to use for requests.
+ * @return An updated instance of this builder with the http client set as specified.
+ * @throws NullPointerException If {@code client} is {@code null}.
+ */
+ @SuppressWarnings("unchecked")
+ public T httpClient(HttpClient client) {
+ Objects.requireNonNull(client);
+ this.identityClientOptions.setHttpClient(client);
+ return (T) this;
+ }
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java
new file mode 100644
index 0000000..052f942
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java
@@ -0,0 +1,48 @@
+package com.azure.identity.extensions;
+
+import com.azure.core.annotation.Immutable;
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenCredential;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.identity.extensions.implementation.IdentityClient;
+import com.azure.identity.extensions.implementation.IdentityClientBuilder;
+import com.azure.identity.implementation.util.LoggingUtil;
+import com.microsoft.aad.msal4j.UserAssertion;
+import reactor.core.publisher.Mono;
+
+/**
+ * An AAD credential that via the On Behalf Of flow for an AAD application.
+ */
+@Immutable
+public class OnBehalfOfFlowCredential implements TokenCredential {
+ private final IdentityClient identityClient;
+ private final UserAssertion userAssertion;
+ private final ClientLogger logger = new ClientLogger(StaticTokenCredential.class);
+
+ /**
+ * Creates a OnBehalfOfCredential
+ *
+ * @param tokenString The string of prefetched token
+ * @param accessToken The prefetched token
+ */
+ OnBehalfOfFlowCredential(String tenantId, String clientId,
+ String clientSecret, String tokenString,
+ AccessToken accessToken) {
+ identityClient = new IdentityClientBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .build();
+ userAssertion = new UserAssertion(tokenString != null ? tokenString : accessToken.getToken());
+ }
+
+ @Override
+ public Mono getToken(TokenRequestContext request) {
+ return identityClient.authenticateWithOnBehalfOfCredentialCache(request, this.userAssertion)
+ .onErrorResume(t -> Mono.empty())
+ .switchIfEmpty(Mono.defer(() -> identityClient.authenticateWithOnBehalfOfCredential(request, this.userAssertion)))
+ .doOnNext(token -> LoggingUtil.logTokenSuccess(logger, request))
+ .doOnError(error -> LoggingUtil.logTokenError(logger, request, error));
+ }
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java
new file mode 100644
index 0000000..b22d70d
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java
@@ -0,0 +1,92 @@
+package com.azure.identity.extensions;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.identity.extensions.implementation.util.ValidationUtil;
+
+import java.util.HashMap;
+
+/**
+ * Fluent credential builder for instantiating a {@link OnBehalfOfFlowCredential}.
+ *
+ * @see OnBehalfOfFlowCredential
+ */
+public class OnBehalfOfFlowCredentialBuilder extends CredentialBuilderBase {
+
+ private String tenantId;
+
+ private String clientId;
+
+ private String clientSecret;
+
+ private String tokenString;
+
+ private AccessToken accessToken;
+
+ public OnBehalfOfFlowCredentialBuilder tenantId(String tenantId){
+ this.tenantId = tenantId;
+ return this;
+ }
+
+ /**
+ * Sets the prefetched token string for the token.
+ *
+ * @param clientId The id of the client
+ *
+ * @return The updated OnBehalfOfCredentialBuilder object.
+ */
+ public OnBehalfOfFlowCredentialBuilder clientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ /**
+ * Sets the prefetched token string for the token.
+ *
+ * @param clientSecret The secret of the client
+ *
+ * @return The updated OnBehalfOfCredentialBuilder object.
+ */
+ public OnBehalfOfFlowCredentialBuilder clientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+
+ /**
+ * Sets the On behalf of Flow token string for the token.
+ *
+ * @param tokenString The On behalf of Flow token string of prefetched token
+ *
+ * @return The updated OnBehalfOfCredentialBuilder object.
+ */
+ public OnBehalfOfFlowCredentialBuilder tokenString(String tokenString) {
+ this.tokenString = tokenString;
+ return this;
+ }
+
+ /**
+ * Sets the On behalf of Flow token for the token.
+ *
+ * @param accessToken The On behalf of Flow token of prefetched token
+ *
+ * @return The updated OnBehalfOfCredentialBuilder object.
+ */
+ public OnBehalfOfFlowCredentialBuilder accessToken(AccessToken accessToken) {
+ this.accessToken = accessToken;
+ return this;
+ }
+
+ public OnBehalfOfFlowCredential build() {
+ com.azure.identity.implementation.util.ValidationUtil.validate(getClass().getSimpleName(), new HashMap() {{
+ put("tenantId", tenantId);
+ put("clientId", clientId);
+ put("clientSecret", clientSecret);
+ }});
+ ValidationUtil.validateAllEmpty(getClass().getSimpleName(), new HashMap() {{
+ put("tokenString", tokenString);
+ put("accessToken", accessToken);
+ }});
+ return new OnBehalfOfFlowCredential(tenantId, clientId, clientSecret, tokenString, accessToken);
+ }
+
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/StaticTokenCredential.java b/java/src/main/java/com/azure/identity/extensions/StaticTokenCredential.java
new file mode 100644
index 0000000..c6910c2
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/StaticTokenCredential.java
@@ -0,0 +1,35 @@
+package com.azure.identity.extensions;
+
+import com.azure.core.annotation.Immutable;
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenCredential;
+import com.azure.core.credential.TokenRequestContext;
+import reactor.core.publisher.Mono;
+
+import java.time.OffsetDateTime;
+
+/**
+ * An AAD credential with a prefetched token for an AAD application.
+ */
+@Immutable
+public class StaticTokenCredential implements TokenCredential {
+ private final AccessToken accessToken;
+
+ /**
+ * Creates a StaticTokenCredential
+ *
+ * @param tokenString The string of prefetched token
+ * @param accessToken The prefetched token
+ */
+ StaticTokenCredential(String tokenString, AccessToken accessToken) {
+ this.accessToken = new AccessToken(
+ tokenString != null ? tokenString : accessToken.getToken(),
+ tokenString != null ? OffsetDateTime.MIN : accessToken.getExpiresAt()
+ );
+ }
+
+ @Override
+ public Mono getToken(TokenRequestContext request) {
+ return Mono.just(this.accessToken);
+ }
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java b/java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java
new file mode 100644
index 0000000..4203255
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java
@@ -0,0 +1,56 @@
+package com.azure.identity.extensions;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.identity.extensions.implementation.util.ValidationUtil;
+
+import java.util.HashMap;
+
+/**
+ * Fluent credential builder for instantiating a {@link StaticTokenCredential}.
+ *
+ * @see StaticTokenCredential
+ */
+public class StaticTokenCredentialBuilder extends CredentialBuilderBase {
+
+ private String tokenString;
+
+ private AccessToken accessToken;
+
+ /**
+ * Sets the prefetched token string for the token.
+ *
+ * @param tokenString The string of prefetched token
+ *
+ * @return The updated StaticTokenCredentialBuilder object.
+ */
+ public StaticTokenCredentialBuilder tokenString(String tokenString) {
+ this.tokenString = tokenString;
+ return this;
+ }
+
+ /**
+ * Sets the prefetched access token for the access token.
+ *
+ * @param accessToken The prefetched token
+ *
+ * @return The updated StaticTokenCredentialBuilder object.
+ */
+ public StaticTokenCredentialBuilder accessToken(AccessToken accessToken) {
+ this.accessToken = accessToken;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link StaticTokenCredentialBuilder} with the current configurations.
+ *
+ * @return a {@link StaticTokenCredentialBuilder} with the current configurations.
+ */
+ public StaticTokenCredential build() {
+ ValidationUtil.validateAllEmpty(getClass().getSimpleName(), new HashMap() {{
+ put("tokenString", tokenString);
+ put("accessToken", accessToken);
+ }});
+ return new StaticTokenCredential(tokenString, accessToken);
+ }
+
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/implementation/HttpPipelineAdapter.java b/java/src/main/java/com/azure/identity/extensions/implementation/HttpPipelineAdapter.java
new file mode 100644
index 0000000..db4fdab
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/implementation/HttpPipelineAdapter.java
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions.implementation;
+
+import com.azure.core.http.HttpHeader;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.http.HttpPipeline;
+import com.microsoft.aad.msal4j.HttpRequest;
+import com.microsoft.aad.msal4j.IHttpClient;
+import com.microsoft.aad.msal4j.IHttpResponse;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.stream.Collectors;
+
+/**
+ * Adapts an HttpPipeline to an instance of IHttpClient in the MSAL4j pipeline.
+ * This class is friendly class, so must include it.
+ */
+class HttpPipelineAdapter implements IHttpClient {
+ private final HttpPipeline httpPipeline;
+
+ HttpPipelineAdapter(HttpPipeline httpPipeline) {
+ this.httpPipeline = httpPipeline;
+ }
+
+ @Override
+ public IHttpResponse send(HttpRequest httpRequest) throws Exception {
+ // convert request
+ com.azure.core.http.HttpRequest request = new com.azure.core.http.HttpRequest(
+ HttpMethod.valueOf(httpRequest.httpMethod().name()),
+ httpRequest.url());
+ if (httpRequest.headers() != null) {
+ request.setHeaders(new HttpHeaders(httpRequest.headers()));
+ }
+ if (httpRequest.body() != null) {
+ request.setBody(httpRequest.body());
+ }
+
+ return httpPipeline.send(request)
+ .flatMap(response -> response.getBodyAsString()
+ .map(body -> {
+ com.microsoft.aad.msal4j.HttpResponse httpResponse = new com.microsoft.aad.msal4j.HttpResponse()
+ .body(body)
+ .statusCode(response.getStatusCode());
+ httpResponse.addHeaders(response.getHeaders().stream().collect(Collectors.toMap(HttpHeader::getName,
+ h -> Collections.singletonList(h.getValue()))));
+ return httpResponse;
+ })
+ // if no body
+ .switchIfEmpty(Mono.defer(() -> {
+ com.microsoft.aad.msal4j.HttpResponse httpResponse = new com.microsoft.aad.msal4j.HttpResponse()
+ .statusCode(response.getStatusCode());
+ httpResponse.addHeaders(response.getHeaders().stream().collect(Collectors.toMap(HttpHeader::getName,
+ h -> Collections.singletonList(h.getValue()))));
+ return Mono.just(httpResponse);
+ })))
+ .block();
+ }
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClient.java b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClient.java
new file mode 100644
index 0000000..7d674f3
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClient.java
@@ -0,0 +1,375 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions.implementation;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.core.exception.ClientAuthenticationException;
+import com.azure.core.http.HttpClient;
+import com.azure.core.http.HttpPipeline;
+import com.azure.core.http.HttpPipelineBuilder;
+import com.azure.core.http.ProxyOptions;
+import com.azure.core.http.policy.HttpLogOptions;
+import com.azure.core.http.policy.HttpLoggingPolicy;
+import com.azure.core.http.policy.HttpPipelinePolicy;
+import com.azure.core.http.policy.HttpPolicyProviders;
+import com.azure.core.http.policy.RetryPolicy;
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.identity.CredentialUnavailableException;
+import com.azure.identity.implementation.IdentityClientOptions;
+import com.azure.identity.implementation.MsalToken;
+import com.azure.identity.implementation.SynchronizedAccessor;
+import com.azure.identity.implementation.util.CertificateUtil;
+import com.microsoft.aad.msal4j.ClientCredentialFactory;
+import com.microsoft.aad.msal4j.ConfidentialClientApplication;
+import com.microsoft.aad.msal4j.IAuthenticationResult;
+import com.microsoft.aad.msal4j.IClientCredential;
+import com.microsoft.aad.msal4j.PublicClientApplication;
+import com.microsoft.aad.msal4j.OnBehalfOfParameters;
+import com.microsoft.aad.msal4j.UserAssertion;
+import com.microsoft.aad.msal4jextensions.PersistenceSettings;
+import com.microsoft.aad.msal4jextensions.PersistenceTokenCacheAccessAspect;
+import com.microsoft.aad.msal4jextensions.persistence.linux.KeyRingAccessException;
+import com.sun.jna.Platform;
+import reactor.core.publisher.Mono;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.Proxy.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * The identity client that contains APIs to retrieve access tokens
+ * from various configurations.
+ * Added two authenticate methods in this class.
+ */
+public class IdentityClient {
+ private static final Duration REFRESH_OFFSET = Duration.ofMinutes(5);
+ 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_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_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";
+ private final ClientLogger logger = new ClientLogger(IdentityClient.class);
+
+ private final IdentityClientOptions options;
+ private final String tenantId;
+ private final String clientId;
+ private final String clientSecret;
+ private final InputStream certificate;
+ private final String certificatePath;
+ private final String certificatePassword;
+ private HttpPipelineAdapter httpPipelineAdapter;
+ private final SynchronizedAccessor publicClientApplicationAccessor;
+ private final SynchronizedAccessor confidentialClientApplicationAccessor;
+
+ /**
+ * Creates an IdentityClient with the given options.
+ *
+ * @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 certificate the PKCS12 or PEM certificate of the application.
+ * @param certificatePassword the password protecting the PFX certificate.
+ * @param isSharedTokenCacheCredential Indicate whether the credential is
+ * {@link com.azure.identity.SharedTokenCacheCredential} or not.
+ * @param options the options configuring the client.
+ */
+ IdentityClient(String tenantId, String clientId, String clientSecret, String certificatePath,
+ InputStream certificate, String certificatePassword, boolean isSharedTokenCacheCredential,
+ IdentityClientOptions options) {
+ if (tenantId == null) {
+ tenantId = "organizations";
+ }
+ if (options == null) {
+ options = new IdentityClientOptions();
+ }
+ this.tenantId = tenantId;
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.certificatePath = certificatePath;
+ this.certificate = certificate;
+ this.certificatePassword = certificatePassword;
+ this.options = options;
+
+ this.publicClientApplicationAccessor = new SynchronizedAccessor(() ->
+ getPublicClientApplication(isSharedTokenCacheCredential));
+
+ this.confidentialClientApplicationAccessor = new SynchronizedAccessor(() ->
+ getConfidentialClientApplication());
+ }
+
+ private ConfidentialClientApplication getConfidentialClientApplication() {
+ if (clientId == null) {
+ throw logger.logExceptionAsError(new IllegalArgumentException(
+ "A non-null value for client ID must be provided for user authentication."));
+ }
+ String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId;
+ IClientCredential credential;
+ if (clientSecret != null) {
+ credential = ClientCredentialFactory.createFromSecret(clientSecret);
+ } else if (certificate != null || certificatePath != null) {
+ try {
+ if (certificatePassword == null) {
+ byte[] pemCertificateBytes = getCertificateBytes();
+
+ List x509CertificateList = CertificateUtil.publicKeyFromPem(pemCertificateBytes);
+ PrivateKey privateKey = CertificateUtil.privateKeyFromPem(pemCertificateBytes);
+ if (x509CertificateList.size() == 1) {
+ credential = ClientCredentialFactory.createFromCertificate(
+ privateKey, x509CertificateList.get(0));
+ } else {
+ credential = ClientCredentialFactory.createFromCertificateChain(
+ privateKey, x509CertificateList);
+ }
+ } else {
+ InputStream pfxCertificateStream = getCertificateInputStream();
+ credential = ClientCredentialFactory.createFromCertificate(
+ pfxCertificateStream, certificatePassword);
+ }
+ } catch (IOException | GeneralSecurityException e) {
+ throw logger.logExceptionAsError(new RuntimeException(
+ "Failed to parse the certificate for the credential: " + e.getMessage(), e));
+ }
+ } else {
+ throw logger.logExceptionAsError(
+ 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));
+ }
+
+ applicationBuilder.sendX5c(options.isIncludeX5c());
+
+ initializeHttpPipelineAdapter();
+ if (httpPipelineAdapter != null) {
+ applicationBuilder.httpClient(httpPipelineAdapter);
+ } else {
+ applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions()));
+ }
+
+ if (options.getExecutorService() != null) {
+ applicationBuilder.executorService(options.getExecutorService());
+ }
+ if (options.isSharedTokenCacheEnabled()) {
+ try {
+ PersistenceSettings.Builder persistenceSettingsBuilder = PersistenceSettings.builder(
+ DEFAULT_CONFIDENTIAL_CACHE_FILE_NAME, DEFAULT_CACHE_FILE_PATH);
+ if (Platform.isMac()) {
+ persistenceSettingsBuilder.setMacKeychain(
+ DEFAULT_KEYCHAIN_SERVICE, DEFAULT_CONFIDENTIAL_KEYCHAIN_ACCOUNT);
+ }
+ if (Platform.isLinux()) {
+ try {
+ persistenceSettingsBuilder
+ .setLinuxKeyring(DEFAULT_KEYRING_NAME, DEFAULT_KEYRING_SCHEMA,
+ DEFAULT_CONFIDENTIAL_KEYRING_ITEM_NAME, DEFAULT_KEYRING_ATTR_NAME,
+ DEFAULT_KEYRING_ATTR_VALUE, null, null);
+ applicationBuilder.setTokenCacheAccessAspect(
+ new PersistenceTokenCacheAccessAspect(persistenceSettingsBuilder.build()));
+ } catch (KeyRingAccessException e) {
+ if (!options.getAllowUnencryptedCache()) {
+ throw logger.logExceptionAsError(e);
+ }
+ persistenceSettingsBuilder.setLinuxUseUnprotectedFileAsCacheStorage(true);
+ applicationBuilder.setTokenCacheAccessAspect(
+ new PersistenceTokenCacheAccessAspect(persistenceSettingsBuilder.build()));
+ }
+ }
+ } catch (Throwable t) {
+ throw logger.logExceptionAsError(new ClientAuthenticationException(
+ "Shared token cache is unavailable in this environment.", null, t));
+ }
+ }
+ return applicationBuilder.build();
+ }
+
+ private PublicClientApplication getPublicClientApplication(boolean sharedTokenCacheCredential) {
+ if (clientId == null) {
+ throw logger.logExceptionAsError(new IllegalArgumentException(
+ "A non-null value for client ID must be provided for user authentication."));
+ }
+ String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId;
+ PublicClientApplication.Builder publicClientApplicationBuilder = PublicClientApplication.builder(clientId);
+ try {
+ publicClientApplicationBuilder = publicClientApplicationBuilder.authority(authorityUrl);
+ } catch (MalformedURLException e) {
+ throw logger.logExceptionAsWarning(new IllegalStateException(e));
+ }
+
+ initializeHttpPipelineAdapter();
+ if (httpPipelineAdapter != null) {
+ publicClientApplicationBuilder.httpClient(httpPipelineAdapter);
+ } else {
+ publicClientApplicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions()));
+ }
+
+ if (options.getExecutorService() != null) {
+ publicClientApplicationBuilder.executorService(options.getExecutorService());
+ }
+ if (options.isSharedTokenCacheEnabled()) {
+ try {
+ PersistenceSettings.Builder persistenceSettingsBuilder = PersistenceSettings.builder(
+ DEFAULT_PUBLIC_CACHE_FILE_NAME, DEFAULT_CACHE_FILE_PATH);
+ if (Platform.isWindows()) {
+ publicClientApplicationBuilder.setTokenCacheAccessAspect(
+ new PersistenceTokenCacheAccessAspect(persistenceSettingsBuilder.build()));
+ } else if (Platform.isMac()) {
+ persistenceSettingsBuilder.setMacKeychain(
+ DEFAULT_KEYCHAIN_SERVICE, DEFAULT_PUBLIC_KEYCHAIN_ACCOUNT);
+ publicClientApplicationBuilder.setTokenCacheAccessAspect(
+ new PersistenceTokenCacheAccessAspect(persistenceSettingsBuilder.build()));
+ } else if (Platform.isLinux()) {
+ try {
+ persistenceSettingsBuilder
+ .setLinuxKeyring(DEFAULT_KEYRING_NAME, DEFAULT_KEYRING_SCHEMA,
+ DEFAULT_PUBLIC_KEYRING_ITEM_NAME, DEFAULT_KEYRING_ATTR_NAME, DEFAULT_KEYRING_ATTR_VALUE,
+ null, null);
+ publicClientApplicationBuilder.setTokenCacheAccessAspect(
+ new PersistenceTokenCacheAccessAspect(persistenceSettingsBuilder.build()));
+ } catch (KeyRingAccessException e) {
+ if (!options.getAllowUnencryptedCache()) {
+ throw logger.logExceptionAsError(e);
+ }
+ persistenceSettingsBuilder.setLinuxUseUnprotectedFileAsCacheStorage(true);
+ publicClientApplicationBuilder.setTokenCacheAccessAspect(
+ new PersistenceTokenCacheAccessAspect(persistenceSettingsBuilder.build()));
+ }
+ }
+ } catch (Throwable t) {
+ String message = "Shared token cache is unavailable in this environment.";
+ if (sharedTokenCacheCredential) {
+ throw logger.logExceptionAsError(new CredentialUnavailableException(message, t));
+ } else {
+ throw logger.logExceptionAsError(new ClientAuthenticationException(message, null, t));
+ }
+ }
+ }
+ return publicClientApplicationBuilder.build();
+ }
+
+ public Mono authenticateWithOnBehalfOfCredentialCache(TokenRequestContext request, UserAssertion userAssertion) {
+ return confidentialClientApplicationAccessor.getValue()
+ .flatMap(confidentialClient -> Mono.fromFuture(() -> {
+ OnBehalfOfParameters.OnBehalfOfParametersBuilder parametersBuilder = OnBehalfOfParameters.builder(
+ new HashSet<>(request.getScopes()), userAssertion);
+ return confidentialClient.acquireToken(parametersBuilder.build());
+ }).map(MsalToken::new)
+ .filter(t -> OffsetDateTime.now().isBefore(t.getExpiresAt().minus(REFRESH_OFFSET))));
+ }
+
+ public Mono authenticateWithOnBehalfOfCredential(TokenRequestContext request, UserAssertion userAssertion) {
+ return confidentialClientApplicationAccessor.getValue()
+ .flatMap(confidentialClient -> Mono.fromFuture(() -> {
+ OnBehalfOfParameters.OnBehalfOfParametersBuilder parametersBuilder = OnBehalfOfParameters.builder(
+ new HashSet<>(request.getScopes()), userAssertion);
+ return confidentialClient.acquireToken(parametersBuilder.build());
+ })
+ .map(MsalToken::new)
+ );
+ }
+
+ private HttpPipeline setupPipeline(HttpClient httpClient) {
+ List policies = new ArrayList<>();
+ HttpLogOptions httpLogOptions = new HttpLogOptions();
+ HttpPolicyProviders.addBeforeRetryPolicies(policies);
+ policies.add(new RetryPolicy());
+ HttpPolicyProviders.addAfterRetryPolicies(policies);
+ policies.add(new HttpLoggingPolicy(httpLogOptions));
+ return new HttpPipelineBuilder().httpClient(httpClient)
+ .policies(policies.toArray(new HttpPipelinePolicy[0])).build();
+ }
+
+ private static Proxy proxyOptionsToJavaNetProxy(ProxyOptions options) {
+ switch (options.getType()) {
+ case SOCKS4:
+ case SOCKS5:
+ return new Proxy(Type.SOCKS, options.getAddress());
+ case HTTP:
+ default:
+ return new Proxy(Type.HTTP, options.getAddress());
+ }
+ }
+
+ private CompletableFuture getFailedCompletableFuture(Exception e) {
+ CompletableFuture completableFuture = new CompletableFuture<>();
+ completableFuture.completeExceptionally(e);
+ return completableFuture;
+ }
+
+ private void initializeHttpPipelineAdapter() {
+ // 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);
+ } 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));
+ } else if (options.getProxyOptions() == null) {
+ //Http Client is null, proxy options are not set, use the default client and build the pipeline.
+ httpPipelineAdapter = new HttpPipelineAdapter(setupPipeline(HttpClient.createDefault()));
+ }
+ }
+ }
+
+ private byte[] getCertificateBytes() throws IOException {
+ if (certificatePath != null) {
+ return Files.readAllBytes(Paths.get(certificatePath));
+ } else if (certificate != null) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int read = certificate.read(buffer, 0, buffer.length);
+ while (read != -1) {
+ outputStream.write(buffer, 0, read);
+ read = certificate.read(buffer, 0, buffer.length);
+ }
+ return outputStream.toByteArray();
+ } else {
+ return new byte[0];
+ }
+ }
+
+ private InputStream getCertificateInputStream() throws IOException {
+ if (certificatePath != null) {
+ return new FileInputStream(certificatePath);
+ } else if (certificate != null) {
+ return certificate;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClientBuilder.java b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClientBuilder.java
new file mode 100644
index 0000000..cc7970e
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClientBuilder.java
@@ -0,0 +1,117 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions.implementation;
+
+import com.azure.identity.SharedTokenCacheCredential;
+import com.azure.identity.implementation.IdentityClientOptions;
+
+import java.io.InputStream;
+
+/**
+ * Fluent client builder for instantiating an {@link IdentityClient}.
+ * The constructors of [IdentityClient] class is friendly, so must include builder class.
+ * @see IdentityClient
+ */
+public final class IdentityClientBuilder {
+ private IdentityClientOptions identityClientOptions;
+ private String tenantId;
+ private String clientId;
+ private String clientSecret;
+ private String certificatePath;
+ private InputStream certificate;
+ private String certificatePassword;
+ private boolean sharedTokenCacheCred;
+
+ /**
+ * Sets the tenant ID for the client.
+ * @param tenantId the tenant ID for the client.
+ * @return the IdentityClientBuilder itself
+ */
+ public IdentityClientBuilder tenantId(String tenantId) {
+ this.tenantId = tenantId;
+ return this;
+ }
+
+ /**
+ * Sets the client ID for the client.
+ * @param clientId the client ID for the client.
+ * @return the IdentityClientBuilder itself
+ */
+ public IdentityClientBuilder clientId(String clientId) {
+ this.clientId = 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 certificate the PEM/PFX certificate
+ * @return the IdentityClientBuilder itself
+ */
+ public IdentityClientBuilder certificate(InputStream certificate) {
+ this.certificate = certificate;
+ 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.
+ * @return the IdentityClientBuilder itself
+ */
+ public IdentityClientBuilder identityClientOptions(IdentityClientOptions identityClientOptions) {
+ this.identityClientOptions = identityClientOptions;
+ return this;
+ }
+
+ /**
+ * Indicate whether the credential is {@link SharedTokenCacheCredential} or not.
+ *
+ * @param isSharedTokenCacheCred the shared token cache credential status.
+ * @return the updated IdentityClientBuilder.
+ */
+ public IdentityClientBuilder sharedTokenCacheCredential(boolean isSharedTokenCacheCred) {
+ this.sharedTokenCacheCred = isSharedTokenCacheCred;
+ return this;
+ }
+
+ /**
+ * @return a {@link IdentityClient} with the current configurations.
+ */
+ public IdentityClient build() {
+ return new IdentityClient(tenantId, clientId, clientSecret, certificatePath, certificate,
+ certificatePassword, sharedTokenCacheCred, identityClientOptions);
+ }
+}
diff --git a/java/src/main/java/com/azure/identity/extensions/implementation/util/ValidationUtil.java b/java/src/main/java/com/azure/identity/extensions/implementation/util/ValidationUtil.java
new file mode 100644
index 0000000..41d3119
--- /dev/null
+++ b/java/src/main/java/com/azure/identity/extensions/implementation/util/ValidationUtil.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions.implementation.util;
+
+import com.azure.core.util.logging.ClientLogger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class for validating parameters.
+ * Added validate method in this class.
+ */
+public final class ValidationUtil {
+
+ public static void validateAllEmpty(String className, Map parameters) {
+ ClientLogger logger = new ClientLogger(className);
+ List missing = new ArrayList<>();
+ for (Map.Entry entry : parameters.entrySet()) {
+ if (entry.getValue() == null) {
+ missing.add(entry.getKey());
+ }
+ }
+ if (missing.size() == parameters.size()) {
+ throw logger.logExceptionAsWarning(new IllegalArgumentException("Must provide non-null values for "
+ + String.join(" or ", missing) + " properties in " + className));
+ }
+ }
+}
diff --git a/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java b/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java
new file mode 100644
index 0000000..4b81da4
--- /dev/null
+++ b/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java
@@ -0,0 +1,139 @@
+package com.azure.identity.extensions;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.identity.extensions.implementation.IdentityClient;
+import com.azure.identity.extensions.util.TestUtils;
+import com.microsoft.aad.msal4j.UserAssertion;
+import net.minidev.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.Base64;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(fullyQualifiedNames = "com.azure.identity.*")
+@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*"})
+public class OnBehalfOfFlowCredentialTests {
+
+ private OffsetDateTime expiresAt = OffsetDateTime.MIN;
+ private static Base64.Encoder encoder = Base64.getEncoder();
+ private String token1 = createTokenString();
+ private AccessToken accessToken = new AccessToken(token1, expiresAt);
+ private String clientId = UUID.randomUUID().toString();
+ private String tenantId = UUID.randomUUID().toString();
+ private String clientSecret = "clientSecret";
+ private TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com");
+
+ @Test
+ public void testValidCacheStaticTokenString() throws Exception {
+
+ // mock
+ IdentityClient identityClient = PowerMockito.mock(IdentityClient.class);
+ when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class)))
+ .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt));
+ PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient);
+
+ // test
+ OnBehalfOfFlowCredential credential =
+ new OnBehalfOfFlowCredentialBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .tokenString(token1).build();
+ StepVerifier.create(credential.getToken(request1)).expectNext()
+ .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()))
+ .verifyComplete();
+ }
+
+ @Test
+ public void testValidStaticTokenString() throws Exception {
+
+ // mock
+ IdentityClient identityClient = PowerMockito.mock(IdentityClient.class);
+ when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class)))
+ .thenReturn(Mono.empty());
+ when(identityClient.authenticateWithOnBehalfOfCredential(any(TokenRequestContext.class), any(UserAssertion.class)))
+ .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt));
+ PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient);
+
+ // test
+ OnBehalfOfFlowCredential credential =
+ new OnBehalfOfFlowCredentialBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .tokenString(token1).build();
+ StepVerifier.create(credential.getToken(request1)).expectNext()
+ .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()))
+ .verifyComplete();
+ }
+
+ @Test
+ public void testValidCacheStaticAccessToken() throws Exception {
+ // mock
+ IdentityClient identityClient = PowerMockito.mock(IdentityClient.class);
+ when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class)))
+ .thenReturn(TestUtils.getMockAccessToken(accessToken.getToken(), accessToken.getExpiresAt()));
+ PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient);
+
+ // test
+ OnBehalfOfFlowCredential credential =
+ new OnBehalfOfFlowCredentialBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .accessToken(accessToken).build();
+ StepVerifier.create(credential.getToken(request1))
+ .expectNextMatches(accessToken -> accessToken.getToken().equals(accessToken.getToken()))
+ .verifyComplete();
+ }
+
+ @Test
+ public void testValidStaticAccessToken() throws Exception {
+ // mock
+ IdentityClient identityClient = PowerMockito.mock(IdentityClient.class);
+ when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class)))
+ .thenReturn(Mono.empty());
+ when(identityClient.authenticateWithOnBehalfOfCredential(any(TokenRequestContext.class), any(UserAssertion.class)))
+ .thenReturn(TestUtils.getMockAccessToken(accessToken.getToken(), accessToken.getExpiresAt()));
+ PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient);
+
+ // test
+ OnBehalfOfFlowCredential credential =
+ new OnBehalfOfFlowCredentialBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .accessToken(accessToken).build();
+ StepVerifier.create(credential.getToken(request1))
+ .expectNextMatches(accessToken -> accessToken.getToken().equals(accessToken.getToken()))
+ .verifyComplete();
+ }
+
+ private String createTokenString() {
+ JSONObject part1Object = new JSONObject();
+ String[] firstPartArray = {"alg", "typ"};
+ for (String firstPart : firstPartArray) {
+ if ("alg".equals(firstPart)) {
+ part1Object.put(firstPart, "HS256");
+ continue;
+ }
+ part1Object.put(firstPart, firstPart);
+ }
+ return encoder.encodeToString(part1Object.toJSONString().getBytes(StandardCharsets.UTF_8)) + ".parts2.parts3";
+ }
+
+}
diff --git a/java/src/test/java/com/azure/identity/extensions/StaticTokenCredentialTests.java b/java/src/test/java/com/azure/identity/extensions/StaticTokenCredentialTests.java
new file mode 100644
index 0000000..d066bf9
--- /dev/null
+++ b/java/src/test/java/com/azure/identity/extensions/StaticTokenCredentialTests.java
@@ -0,0 +1,37 @@
+package com.azure.identity.extensions;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.time.OffsetDateTime;
+
+public class StaticTokenCredentialTests {
+
+ @Test
+ public void testValidStaticTokenString() {
+ String token = "token1";
+ TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com");
+ OffsetDateTime expiresAt = OffsetDateTime.MIN;
+ // test
+ StaticTokenCredential credential =
+ new StaticTokenCredentialBuilder().tokenString(token).build();
+ AccessToken accessToken = credential.getToken(request).block();
+ Assert.assertEquals(token, accessToken.getToken());
+ Assert.assertEquals(expiresAt, accessToken.getExpiresAt());
+ }
+
+ @Test
+ public void testValidStaticAccessToken() {
+ AccessToken token = new AccessToken("token1", OffsetDateTime.MIN);
+ TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com");
+ // test
+ StaticTokenCredential credential =
+ new StaticTokenCredentialBuilder().accessToken(token).build();
+ AccessToken accessToken = credential.getToken(request).block();
+ Assert.assertEquals(token.getToken(), accessToken.getToken());
+ Assert.assertEquals(token.getExpiresAt(), accessToken.getExpiresAt());
+ }
+
+}
diff --git a/java/src/test/java/com/azure/identity/extensions/implementation/IdentityClientTests.java b/java/src/test/java/com/azure/identity/extensions/implementation/IdentityClientTests.java
new file mode 100644
index 0000000..3787a7c
--- /dev/null
+++ b/java/src/test/java/com/azure/identity/extensions/implementation/IdentityClientTests.java
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions.implementation;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.identity.extensions.util.TestUtils;
+import com.azure.identity.implementation.IdentityClientOptions;
+import com.azure.identity.implementation.util.CertificateUtil;
+import com.microsoft.aad.msal4j.ClientCredentialFactory;
+import com.microsoft.aad.msal4j.ConfidentialClientApplication;
+import com.microsoft.aad.msal4j.IClientCredential;
+import com.microsoft.aad.msal4j.MsalServiceException;
+import com.microsoft.aad.msal4j.PublicClientApplication;
+import com.microsoft.aad.msal4j.OnBehalfOfParameters;
+import com.microsoft.aad.msal4j.UserAssertion;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.net.URL;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.whenNew;
+
+@RunWith(PowerMockRunner.class)
+@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "javax.net.ssl.*", "org.xml.*"})
+@PrepareForTest({CertificateUtil.class, ClientCredentialFactory.class, Runtime.class, URL.class, ConfidentialClientApplication.class, ConfidentialClientApplication.Builder.class, PublicClientApplication.class, PublicClientApplication.Builder.class, IdentityClient.class})
+public class IdentityClientTests {
+
+ private static final String TENANT_ID = "contoso.com";
+ private static final String CLIENT_ID = UUID.randomUUID().toString();
+ private final ClientLogger logger = new ClientLogger(IdentityClientTests.class);
+
+ @Test
+ public void testValidOnBehalfOfFLowCredential() throws Exception {
+ // setup
+ String accessToken = "token";
+ UserAssertion userAssertion = new UserAssertion(accessToken);
+ TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com");
+ OffsetDateTime expiresOn = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1);
+
+ // mock
+ mockForOnBehalfOfFlowCredential(accessToken, request, expiresOn);
+
+ // test
+ IdentityClientOptions options = new IdentityClientOptions();
+ IdentityClient client = new IdentityClientBuilder().tenantId(TENANT_ID).clientId(CLIENT_ID).clientSecret("secret").identityClientOptions(options).build();
+ AccessToken token = client.authenticateWithOnBehalfOfCredential(request, userAssertion).block();
+ Assert.assertEquals(accessToken, token.getToken());
+ Assert.assertEquals(expiresOn.getSecond(), token.getExpiresAt().getSecond());
+ }
+
+ private void mockForOnBehalfOfFlowCredential(String token, TokenRequestContext request, OffsetDateTime expiresAt) throws Exception {
+ ConfidentialClientApplication application = PowerMockito.mock(ConfidentialClientApplication.class);
+ when(application.acquireToken(any(OnBehalfOfParameters.class))).thenAnswer(invocation -> {
+ OnBehalfOfParameters argument = (OnBehalfOfParameters) invocation.getArguments()[0];
+ if (argument.scopes().size() == 1 && request.getScopes().get(0).equals(argument.scopes().iterator().next())) {
+ return TestUtils.getMockAuthenticationResult(token, expiresAt);
+ } else {
+ return CompletableFuture.runAsync(() -> {
+ throw new MsalServiceException("Invalid request", "InvalidScopes");
+ });
+ }
+ });
+ ConfidentialClientApplication.Builder builder = PowerMockito.mock(ConfidentialClientApplication.Builder.class);
+ when(builder.build()).thenReturn(application);
+ when(builder.authority(any())).thenReturn(builder);
+ when(builder.httpClient(any())).thenReturn(builder);
+ whenNew(ConfidentialClientApplication.Builder.class).withAnyArguments().thenAnswer(invocation -> {
+ String cid = (String) invocation.getArguments()[0];
+ IClientCredential keyCredential = (IClientCredential) invocation.getArguments()[1];
+ if (!CLIENT_ID.equals(cid)) {
+ throw new MsalServiceException("Invalid CLIENT_ID", "InvalidClientId");
+ }
+ if (keyCredential == null) {
+ throw new MsalServiceException("Invalid clientCertificate", "InvalidClientCertificate");
+ }
+ return builder;
+ });
+ }
+
+}
diff --git a/java/src/test/java/com/azure/identity/extensions/util/TestUtils.java b/java/src/test/java/com/azure/identity/extensions/util/TestUtils.java
new file mode 100644
index 0000000..473c559
--- /dev/null
+++ b/java/src/test/java/com/azure/identity/extensions/util/TestUtils.java
@@ -0,0 +1,135 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.identity.extensions.util;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.identity.implementation.MsalToken;
+import com.microsoft.aad.msal4j.IAccount;
+import com.microsoft.aad.msal4j.IAuthenticationResult;
+import com.microsoft.aad.msal4j.ITenantProfile;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.util.Date;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Utilities for identity tests.
+ */
+public final class TestUtils {
+ /**
+ * Creates a mock {@link IAuthenticationResult} instance.
+ * @param accessToken the access token to return
+ * @param expiresOn the expiration time
+ * @return a completable future of the result
+ */
+ public static CompletableFuture getMockAuthenticationResult(String accessToken, OffsetDateTime expiresOn) {
+ return CompletableFuture.completedFuture(new IAuthenticationResult() {
+ @Override
+ public String accessToken() {
+ return accessToken;
+ }
+
+ @Override
+ public String idToken() {
+ return null;
+ }
+
+ @Override
+ public IAccount account() {
+ return new IAccount() {
+ @Override
+ public String homeAccountId() {
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String environment() {
+ return "http://login.microsoftonline.com";
+ }
+
+ @Override
+ public String username() {
+ return "testuser";
+ }
+
+ @Override
+ public Map getTenantProfiles() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public ITenantProfile tenantProfile() {
+ return null;
+ }
+
+ @Override
+ public String environment() {
+ return "http://login.microsoftonline.com";
+ }
+
+ @Override
+ public String scopes() {
+ return null;
+ }
+
+ @Override
+ public Date expiresOnDate() {
+ // Access token dials back 2 minutes
+ return Date.from(expiresOn.plusMinutes(2).toInstant());
+ }
+ });
+ }
+
+ /**
+ * Creates a mock {@link MsalToken} instance.
+ * @param accessToken the access token to return
+ * @param expiresOn the expiration time
+ * @return a Mono publisher of the result
+ */
+ public static Mono getMockMsalToken(String accessToken, OffsetDateTime expiresOn) {
+ return Mono.fromFuture(getMockAuthenticationResult(accessToken, expiresOn))
+ .map(MsalToken::new);
+ }
+
+ /**
+ * Creates a mock {@link IAccount} instance.
+ * @param accessToken the access token to return
+ * @param expiresOn the expiration time
+ * @return a Mono publisher of the result
+ */
+ public static Mono getMockMsalAccount(String accessToken, OffsetDateTime expiresOn) {
+ return Mono.fromFuture(getMockAuthenticationResult(accessToken, expiresOn))
+ .map(IAuthenticationResult::account);
+ }
+
+ /**
+ * Creates a mock {@link AccessToken} instance.
+ * @param accessToken the access token to return
+ * @param expiresOn the expiration time
+ * @return a Mono publisher of the result
+ */
+ public static Mono getMockAccessToken(String accessToken, OffsetDateTime expiresOn) {
+ return Mono.just(new AccessToken(accessToken, expiresOn.plusMinutes(2)));
+ }
+
+ /**
+ * Creates a mock {@link AccessToken} instance.
+ * @param accessToken the access token to return
+ * @param expiresOn the expiration time
+ * @param tokenRefreshOffset how long before the actual expiry to refresh the token
+ * @return a Mono publisher of the result
+ */
+ public static Mono getMockAccessToken(String accessToken, OffsetDateTime expiresOn, Duration tokenRefreshOffset) {
+ return Mono.just(new AccessToken(accessToken, expiresOn.plusMinutes(2).minus(tokenRefreshOffset)));
+ }
+
+ private TestUtils() {
+ }
+}