From 233286ccd5b43133782b0ebb575067dffb8b0906 Mon Sep 17 00:00:00 2001 From: hongli750210 Date: Wed, 3 Feb 2021 14:40:40 +0800 Subject: [PATCH 1/2] Fixed Task 113173 StaticTokenCredential and OnBehalfOfCredential Implement 202102031440 --- java/pom.xml | 66 +++- .../extensions/CredentialBuilderBase.java | 92 +++++ .../extensions/HttpPipelineAdapter.java | 61 +++ .../identity/extensions/IdentityClient.java | 374 ++++++++++++++++++ .../extensions/IdentityClientBuilder.java | 117 ++++++ .../extensions/OnBehalfOfFlowCredential.java | 46 +++ .../OnBehalfOfFlowCredentialBuilder.java | 91 +++++ .../extensions/StaticTokenCredential.java | 35 ++ .../StaticTokenCredentialBuilder.java | 55 +++ .../identity/extensions/ValidationUtil.java | 32 ++ .../extensions/IdentityClientTests.java | 93 +++++ .../OnBehalfOfFlowCredentialTests.java | 137 +++++++ .../StaticTokenCredentialTests.java | 37 ++ .../azure/identity/extensions/TestUtils.java | 135 +++++++ 14 files changed, 1370 insertions(+), 1 deletion(-) create mode 100644 java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java create mode 100644 java/src/main/java/com/azure/identity/extensions/HttpPipelineAdapter.java create mode 100644 java/src/main/java/com/azure/identity/extensions/IdentityClient.java create mode 100644 java/src/main/java/com/azure/identity/extensions/IdentityClientBuilder.java create mode 100644 java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java create mode 100644 java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java create mode 100644 java/src/main/java/com/azure/identity/extensions/StaticTokenCredential.java create mode 100644 java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java create mode 100644 java/src/main/java/com/azure/identity/extensions/ValidationUtil.java create mode 100644 java/src/test/java/com/azure/identity/extensions/IdentityClientTests.java create mode 100644 java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java create mode 100644 java/src/test/java/com/azure/identity/extensions/StaticTokenCredentialTests.java create mode 100644 java/src/test/java/com/azure/identity/extensions/TestUtils.java 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..5129cf9 --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java @@ -0,0 +1,92 @@ +// 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. + * @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/HttpPipelineAdapter.java b/java/src/main/java/com/azure/identity/extensions/HttpPipelineAdapter.java new file mode 100644 index 0000000..92c53af --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/HttpPipelineAdapter.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.extensions; + +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. + */ +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/IdentityClient.java b/java/src/main/java/com/azure/identity/extensions/IdentityClient.java new file mode 100644 index 0000000..f5c10ce --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/IdentityClient.java @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.extensions; + +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. + */ +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/IdentityClientBuilder.java b/java/src/main/java/com/azure/identity/extensions/IdentityClientBuilder.java new file mode 100644 index 0000000..6301605 --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/IdentityClientBuilder.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.extensions; + +import com.azure.identity.SharedTokenCacheCredential; +import com.azure.identity.implementation.IdentityClientOptions; + +import java.io.InputStream; + +/** + * Fluent client builder for instantiating an {@link IdentityClient}. + * + * @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/OnBehalfOfFlowCredential.java b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java new file mode 100644 index 0000000..85c14e5 --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java @@ -0,0 +1,46 @@ +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.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 com.azure.identity.extensions.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..b87d9cd --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java @@ -0,0 +1,91 @@ +package com.azure.identity.extensions; + +import com.azure.core.credential.AccessToken; + +import java.util.HashMap; + +/** + * Fluent credential builder for instantiating a {@link OnBehalfOfFlowCredential}. + * + * @see OnBehalfOfFlowCredential + */ +public class OnBehalfOfFlowCredentialBuilder extends com.azure.identity.extensions.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); + }}); + com.azure.identity.extensions.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..10f6ca8 --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java @@ -0,0 +1,55 @@ +package com.azure.identity.extensions; + +import com.azure.core.credential.AccessToken; + +import java.util.HashMap; + +/** + * Fluent credential builder for instantiating a {@link StaticTokenCredential}. + * + * @see StaticTokenCredential + */ +public class StaticTokenCredentialBuilder extends com.azure.identity.extensions.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() { + com.azure.identity.extensions.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/ValidationUtil.java b/java/src/main/java/com/azure/identity/extensions/ValidationUtil.java new file mode 100644 index 0000000..0e887f2 --- /dev/null +++ b/java/src/main/java/com/azure/identity/extensions/ValidationUtil.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.extensions; + +import com.azure.core.util.logging.ClientLogger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Utility class for validating parameters. + */ +public final class ValidationUtil { + private static Pattern tenantIdentifierCharPattern = Pattern.compile("^(?:[A-Z]|[0-9]|[a-z]|-|.)+$"); + + 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/IdentityClientTests.java b/java/src/test/java/com/azure/identity/extensions/IdentityClientTests.java new file mode 100644 index 0000000..f0b18eb --- /dev/null +++ b/java/src/test/java/com/azure/identity/extensions/IdentityClientTests.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.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.core.util.logging.ClientLogger; +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/OnBehalfOfFlowCredentialTests.java b/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java new file mode 100644 index 0000000..cce9177 --- /dev/null +++ b/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java @@ -0,0 +1,137 @@ +package com.azure.identity.extensions; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +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 + com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.IdentityClient.class); + when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class))) + .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + PowerMockito.whenNew(com.azure.identity.extensions.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 + com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.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(com.azure.identity.extensions.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 + com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.IdentityClient.class); + when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class))) + .thenReturn(TestUtils.getMockAccessToken(accessToken.getToken(), accessToken.getExpiresAt())); + PowerMockito.whenNew(com.azure.identity.extensions.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 + com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.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(com.azure.identity.extensions.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/TestUtils.java b/java/src/test/java/com/azure/identity/extensions/TestUtils.java new file mode 100644 index 0000000..bae9979 --- /dev/null +++ b/java/src/test/java/com/azure/identity/extensions/TestUtils.java @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.extensions; + +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() { + } +} From 016a7bf82cb528a77dee2a44e7b404642f7785d9 Mon Sep 17 00:00:00 2001 From: hongli750210 Date: Wed, 3 Feb 2021 17:31:06 +0800 Subject: [PATCH 2/2] Fixed task 113173 project structure adjustment 202102031730 --- .../extensions/CredentialBuilderBase.java | 1 + .../extensions/OnBehalfOfFlowCredential.java | 4 +++- .../OnBehalfOfFlowCredentialBuilder.java | 5 +++-- .../StaticTokenCredentialBuilder.java | 5 +++-- .../HttpPipelineAdapter.java | 3 ++- .../{ => implementation}/IdentityClient.java | 3 ++- .../IdentityClientBuilder.java | 4 ++-- .../util}/ValidationUtil.java | 5 ++--- .../OnBehalfOfFlowCredentialTests.java | 18 ++++++++++-------- .../IdentityClientTests.java | 3 ++- .../extensions/{ => util}/TestUtils.java | 2 +- 11 files changed, 31 insertions(+), 22 deletions(-) rename java/src/main/java/com/azure/identity/extensions/{ => implementation}/HttpPipelineAdapter.java (95%) rename java/src/main/java/com/azure/identity/extensions/{ => implementation}/IdentityClient.java (99%) rename java/src/main/java/com/azure/identity/extensions/{ => implementation}/IdentityClientBuilder.java (96%) rename java/src/main/java/com/azure/identity/extensions/{ => implementation/util}/ValidationUtil.java (84%) rename java/src/test/java/com/azure/identity/extensions/{ => implementation}/IdentityClientTests.java (97%) rename java/src/test/java/com/azure/identity/extensions/{ => util}/TestUtils.java (99%) diff --git a/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java b/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java index 5129cf9..a8d994e 100644 --- a/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java +++ b/java/src/main/java/com/azure/identity/extensions/CredentialBuilderBase.java @@ -14,6 +14,7 @@ /** * 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> { diff --git a/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java index 85c14e5..052f942 100644 --- a/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java +++ b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredential.java @@ -5,6 +5,8 @@ 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; @@ -27,7 +29,7 @@ public class OnBehalfOfFlowCredential implements TokenCredential { OnBehalfOfFlowCredential(String tenantId, String clientId, String clientSecret, String tokenString, AccessToken accessToken) { - identityClient = new com.azure.identity.extensions.IdentityClientBuilder() + identityClient = new IdentityClientBuilder() .tenantId(tenantId) .clientId(clientId) .clientSecret(clientSecret) diff --git a/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java index b87d9cd..b22d70d 100644 --- a/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java +++ b/java/src/main/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialBuilder.java @@ -1,6 +1,7 @@ package com.azure.identity.extensions; import com.azure.core.credential.AccessToken; +import com.azure.identity.extensions.implementation.util.ValidationUtil; import java.util.HashMap; @@ -9,7 +10,7 @@ * * @see OnBehalfOfFlowCredential */ -public class OnBehalfOfFlowCredentialBuilder extends com.azure.identity.extensions.CredentialBuilderBase { +public class OnBehalfOfFlowCredentialBuilder extends CredentialBuilderBase { private String tenantId; @@ -81,7 +82,7 @@ public OnBehalfOfFlowCredential build() { put("clientId", clientId); put("clientSecret", clientSecret); }}); - com.azure.identity.extensions.ValidationUtil.validateAllEmpty(getClass().getSimpleName(), new HashMap() {{ + ValidationUtil.validateAllEmpty(getClass().getSimpleName(), new HashMap() {{ put("tokenString", tokenString); put("accessToken", 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 index 10f6ca8..4203255 100644 --- a/java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java +++ b/java/src/main/java/com/azure/identity/extensions/StaticTokenCredentialBuilder.java @@ -1,6 +1,7 @@ package com.azure.identity.extensions; import com.azure.core.credential.AccessToken; +import com.azure.identity.extensions.implementation.util.ValidationUtil; import java.util.HashMap; @@ -9,7 +10,7 @@ * * @see StaticTokenCredential */ -public class StaticTokenCredentialBuilder extends com.azure.identity.extensions.CredentialBuilderBase { +public class StaticTokenCredentialBuilder extends CredentialBuilderBase { private String tokenString; @@ -45,7 +46,7 @@ public StaticTokenCredentialBuilder accessToken(AccessToken accessToken) { * @return a {@link StaticTokenCredentialBuilder} with the current configurations. */ public StaticTokenCredential build() { - com.azure.identity.extensions.ValidationUtil.validateAllEmpty(getClass().getSimpleName(), new HashMap() {{ + ValidationUtil.validateAllEmpty(getClass().getSimpleName(), new HashMap() {{ put("tokenString", tokenString); put("accessToken", accessToken); }}); diff --git a/java/src/main/java/com/azure/identity/extensions/HttpPipelineAdapter.java b/java/src/main/java/com/azure/identity/extensions/implementation/HttpPipelineAdapter.java similarity index 95% rename from java/src/main/java/com/azure/identity/extensions/HttpPipelineAdapter.java rename to java/src/main/java/com/azure/identity/extensions/implementation/HttpPipelineAdapter.java index 92c53af..db4fdab 100644 --- a/java/src/main/java/com/azure/identity/extensions/HttpPipelineAdapter.java +++ b/java/src/main/java/com/azure/identity/extensions/implementation/HttpPipelineAdapter.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.identity.extensions; +package com.azure.identity.extensions.implementation; import com.azure.core.http.HttpHeader; import com.azure.core.http.HttpHeaders; @@ -17,6 +17,7 @@ /** * 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; diff --git a/java/src/main/java/com/azure/identity/extensions/IdentityClient.java b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClient.java similarity index 99% rename from java/src/main/java/com/azure/identity/extensions/IdentityClient.java rename to java/src/main/java/com/azure/identity/extensions/implementation/IdentityClient.java index f5c10ce..7d674f3 100644 --- a/java/src/main/java/com/azure/identity/extensions/IdentityClient.java +++ b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClient.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.identity.extensions; +package com.azure.identity.extensions.implementation; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenRequestContext; @@ -57,6 +57,7 @@ /** * 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); diff --git a/java/src/main/java/com/azure/identity/extensions/IdentityClientBuilder.java b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClientBuilder.java similarity index 96% rename from java/src/main/java/com/azure/identity/extensions/IdentityClientBuilder.java rename to java/src/main/java/com/azure/identity/extensions/implementation/IdentityClientBuilder.java index 6301605..cc7970e 100644 --- a/java/src/main/java/com/azure/identity/extensions/IdentityClientBuilder.java +++ b/java/src/main/java/com/azure/identity/extensions/implementation/IdentityClientBuilder.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.identity.extensions; +package com.azure.identity.extensions.implementation; import com.azure.identity.SharedTokenCacheCredential; import com.azure.identity.implementation.IdentityClientOptions; @@ -10,7 +10,7 @@ /** * 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 { diff --git a/java/src/main/java/com/azure/identity/extensions/ValidationUtil.java b/java/src/main/java/com/azure/identity/extensions/implementation/util/ValidationUtil.java similarity index 84% rename from java/src/main/java/com/azure/identity/extensions/ValidationUtil.java rename to java/src/main/java/com/azure/identity/extensions/implementation/util/ValidationUtil.java index 0e887f2..41d3119 100644 --- a/java/src/main/java/com/azure/identity/extensions/ValidationUtil.java +++ b/java/src/main/java/com/azure/identity/extensions/implementation/util/ValidationUtil.java @@ -1,20 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.identity.extensions; +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; -import java.util.regex.Pattern; /** * Utility class for validating parameters. + * Added validate method in this class. */ public final class ValidationUtil { - private static Pattern tenantIdentifierCharPattern = Pattern.compile("^(?:[A-Z]|[0-9]|[a-z]|-|.)+$"); public static void validateAllEmpty(String className, Map parameters) { ClientLogger logger = new ClientLogger(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 index cce9177..4b81da4 100644 --- a/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java +++ b/java/src/test/java/com/azure/identity/extensions/OnBehalfOfFlowCredentialTests.java @@ -2,6 +2,8 @@ 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; @@ -39,10 +41,10 @@ public class OnBehalfOfFlowCredentialTests { public void testValidCacheStaticTokenString() throws Exception { // mock - com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.IdentityClient.class); + IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class))) .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - PowerMockito.whenNew(com.azure.identity.extensions.IdentityClient.class).withAnyArguments().thenReturn(identityClient); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test OnBehalfOfFlowCredential credential = @@ -60,12 +62,12 @@ public void testValidCacheStaticTokenString() throws Exception { public void testValidStaticTokenString() throws Exception { // mock - com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.IdentityClient.class); + 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(com.azure.identity.extensions.IdentityClient.class).withAnyArguments().thenReturn(identityClient); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test OnBehalfOfFlowCredential credential = @@ -82,10 +84,10 @@ public void testValidStaticTokenString() throws Exception { @Test public void testValidCacheStaticAccessToken() throws Exception { // mock - com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.IdentityClient.class); + IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); when(identityClient.authenticateWithOnBehalfOfCredentialCache(any(TokenRequestContext.class), any(UserAssertion.class))) .thenReturn(TestUtils.getMockAccessToken(accessToken.getToken(), accessToken.getExpiresAt())); - PowerMockito.whenNew(com.azure.identity.extensions.IdentityClient.class).withAnyArguments().thenReturn(identityClient); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test OnBehalfOfFlowCredential credential = @@ -102,12 +104,12 @@ public void testValidCacheStaticAccessToken() throws Exception { @Test public void testValidStaticAccessToken() throws Exception { // mock - com.azure.identity.extensions.IdentityClient identityClient = PowerMockito.mock(com.azure.identity.extensions.IdentityClient.class); + 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(com.azure.identity.extensions.IdentityClient.class).withAnyArguments().thenReturn(identityClient); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); // test OnBehalfOfFlowCredential credential = diff --git a/java/src/test/java/com/azure/identity/extensions/IdentityClientTests.java b/java/src/test/java/com/azure/identity/extensions/implementation/IdentityClientTests.java similarity index 97% rename from java/src/test/java/com/azure/identity/extensions/IdentityClientTests.java rename to java/src/test/java/com/azure/identity/extensions/implementation/IdentityClientTests.java index f0b18eb..3787a7c 100644 --- a/java/src/test/java/com/azure/identity/extensions/IdentityClientTests.java +++ b/java/src/test/java/com/azure/identity/extensions/implementation/IdentityClientTests.java @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.identity.extensions; +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; diff --git a/java/src/test/java/com/azure/identity/extensions/TestUtils.java b/java/src/test/java/com/azure/identity/extensions/util/TestUtils.java similarity index 99% rename from java/src/test/java/com/azure/identity/extensions/TestUtils.java rename to java/src/test/java/com/azure/identity/extensions/util/TestUtils.java index bae9979..473c559 100644 --- a/java/src/test/java/com/azure/identity/extensions/TestUtils.java +++ b/java/src/test/java/com/azure/identity/extensions/util/TestUtils.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.identity.extensions; +package com.azure.identity.extensions.util; import com.azure.core.credential.AccessToken; import com.azure.identity.implementation.MsalToken;