From d8f8635cd66c50acd7051301321ddfa3fa666d50 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Wed, 19 Apr 2023 15:42:05 -0700 Subject: [PATCH 1/5] add sync flow to workload identity --- .../identity/WorkloadIdentityCredential.java | 21 ++++++-- .../implementation/IdentityClient.java | 44 +---------------- .../implementation/IdentityClientBase.java | 48 +++++++++++++++++++ .../implementation/IdentitySyncClient.java | 13 ++++- .../WorkloadIdentityCredentialTest.java | 31 ++++++++++++ 5 files changed, 110 insertions(+), 47 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java index 8a348df83623..a4abce48cbec 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java @@ -12,6 +12,7 @@ import com.azure.identity.implementation.IdentityClient; import com.azure.identity.implementation.IdentityClientBuilder; import com.azure.identity.implementation.IdentityClientOptions; +import com.azure.identity.implementation.IdentitySyncClient; import com.azure.identity.implementation.util.ValidationUtil; import reactor.core.publisher.Mono; @@ -54,6 +55,7 @@ public class WorkloadIdentityCredential implements TokenCredential { private static final ClientLogger LOGGER = new ClientLogger(WorkloadIdentityCredential.class); private final IdentityClient identityClient; + private final IdentitySyncClient identitySyncClient; /** * WorkloadIdentityCredential supports Azure workload identity on Kubernetes. @@ -82,14 +84,16 @@ public class WorkloadIdentityCredential implements TokenCredential { || CoreUtils.isNullOrEmpty(federatedTokenFilePathInput) || CoreUtils.isNullOrEmpty(clientIdInput) || CoreUtils.isNullOrEmpty(identityClientOptions.getAuthorityHost()))) { - identityClient = new IdentityClientBuilder() + IdentityClientBuilder builder = new IdentityClientBuilder() .clientAssertionPath(federatedTokenFilePathInput) .clientId(clientId) .tenantId(tenantIdInput) - .identityClientOptions(identityClientOptions) - .build(); + .identityClientOptions(identityClientOptions); + identityClient = builder.build(); + identitySyncClient = builder.buildSyncClient(); } else { identityClient = null; + identitySyncClient = null; } } @@ -104,6 +108,17 @@ public Mono getToken(TokenRequestContext request) { return identityClient.authenticateWithExchangeToken(request); } + @Override + public AccessToken getTokenSync(TokenRequestContext request) { + if (identitySyncClient == null) { + throw LOGGER.logExceptionAsError(new CredentialUnavailableException("WorkloadIdentityCredential" + + " authentication unavailable. The workload options are not fully configured. See the troubleshooting" + + " guide for more information." + + " https://aka.ms/azsdk/java/identity/workloadidentitycredential/troubleshoot")); + } + return identitySyncClient.authenticateWithExchangeTokenSync(request); + } + String getClientId() { return this.identityClient.getClientId(); } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index c8a86a8487ab..37854cd4058a 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -36,7 +36,6 @@ import reactor.core.publisher.Mono; import javax.net.ssl.HttpsURLConnection; -import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -902,41 +901,7 @@ private Mono authenticateToArcManagedIdentityEndpoint(String identi public Mono authenticateWithExchangeToken(TokenRequestContext request) { return clientAssertionAccessor.getValue() - .flatMap(assertionToken -> Mono.fromCallable(() -> { - String authorityUrl = TRAILING_FORWARD_SLASHES.matcher(options.getAuthorityHost()).replaceAll("") - + "/" + tenantId + "/oauth2/v2.0/token"; - - String urlParams = "client_assertion=" + assertionToken - + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_id=" - + clientId + "&grant_type=client_credentials&scope=" + urlEncode(request.getScopes().get(0)); - - byte[] postData = urlParams.getBytes(StandardCharsets.UTF_8); - int postDataLength = postData.length; - - HttpURLConnection connection = null; - - URL url = getUrl(authorityUrl); - - try { - connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); - connection.setRequestProperty("User-Agent", userAgent); - connection.setDoOutput(true); - try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { - outputStream.write(postData); - } - connection.connect(); - - return SERIALIZER_ADAPTER.deserialize(connection.getInputStream(), MSIToken.class, - SerializerEncoding.JSON); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - })); + .flatMap(assertionToken -> Mono.fromCallable(() -> authenticateWithExchangeTokenHelper(request, assertionToken))); } /** @@ -1088,9 +1053,6 @@ public Mono authenticateToManagedIdentityEndpoint(String identityEn }); } - static URL getUrl(String uri) throws MalformedURLException { - return new URL(uri); - } /** * Asynchronously acquire a token from the Virtual Machine IMDS endpoint. * @@ -1272,8 +1234,4 @@ public IdentityClientOptions getIdentityClientOptions() { private boolean isADFSTenant() { return ADFS_TENANT.equals(this.tenantId); } - - private static String urlEncode(String value) throws IOException { - return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); - } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java index 4874843dc5b1..2d8f44b9ccc0 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java @@ -48,14 +48,18 @@ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.Proxy; import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; @@ -613,6 +617,42 @@ AccessToken getTokenFromAzureDeveloperCLIAuthentication(StringBuilder azdCommand return token; } + AccessToken authenticateWithExchangeTokenHelper(TokenRequestContext request, String assertionToken) throws IOException { + String authorityUrl = TRAILING_FORWARD_SLASHES.matcher(options.getAuthorityHost()).replaceAll("") + + "/" + tenantId + "/oauth2/v2.0/token"; + + String urlParams = "client_assertion=" + assertionToken + + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_id=" + + clientId + "&grant_type=client_credentials&scope=" + urlEncode(request.getScopes().get(0)); + + byte[] postData = urlParams.getBytes(StandardCharsets.UTF_8); + int postDataLength = postData.length; + + HttpURLConnection connection = null; + + URL url = getUrl(authorityUrl); + + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); + connection.setRequestProperty("User-Agent", userAgent); + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(postData); + } + connection.connect(); + + return SERIALIZER_ADAPTER.deserialize(connection.getInputStream(), MSIToken.class, + SerializerEncoding.JSON); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + String getSafeWorkingDirectory() { if (isWindowsPlatform()) { String windowsSystemRoot = System.getenv("SystemRoot"); @@ -724,6 +764,14 @@ private static Proxy proxyOptionsToJavaNetProxy(ProxyOptions options) { } } + static String urlEncode(String value) throws IOException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + } + + static URL getUrl(String uri) throws MalformedURLException { + return new URL(uri); + } + /** * Get the configured tenant id. * diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java index 3422f997c83c..45344b2cdeab 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -373,7 +374,7 @@ public AccessToken authenticateWithAzureDeveloperCli(TokenRequestContext request ? LoggingUtil.logCredentialUnavailableException(LOGGER, options, (CredentialUnavailableException) e) : LOGGER.logExceptionAsError(e)); } - } + } /** * Asynchronously acquire a token from Active Directory with Azure PowerShell. @@ -390,6 +391,16 @@ public AccessToken authenticateWithOBO(TokenRequestContext request) { } } + public AccessToken authenticateWithExchangeTokenSync(TokenRequestContext request) { + + try { + String assertionToken = clientAssertionAccessor.getValue(); + return authenticateWithExchangeTokenHelper(request, assertionToken); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + /** * Get the configured identity client options. * diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java index 36e1553de0fc..13efef5b5864 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java @@ -3,10 +3,12 @@ package com.azure.identity; +import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenRequestContext; import com.azure.core.test.utils.TestConfigurationSource; import com.azure.core.util.Configuration; import com.azure.identity.implementation.IdentityClient; +import com.azure.identity.implementation.IdentitySyncClient; import com.azure.identity.util.TestUtils; import org.junit.Assert; import org.junit.Test; @@ -52,6 +54,35 @@ public void testWorkloadIdentityFlow() { } } + @Test + public void testWorkloadIdentityFlowSync() { + // setup + String endpoint = "https://localhost"; + String token1 = "token1"; + TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com"); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource() + .put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint)); + + // mock + try (MockedConstruction identityClientMock = mockConstruction(IdentitySyncClient.class, (identityClient, context) -> { + when(identityClient.authenticateWithExchangeTokenSync(request1)).thenReturn(TestUtils.getMockAccessTokenSync(token1, expiresAt)); + })) { + // test + WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder() + .tenantId("dummy-tenantid") + .clientId("dummy-clientid") + .tokenFilePath("dummy-path") + .configuration(configuration).clientId(CLIENT_ID).build(); + + AccessToken token = credential.getTokenSync(request1); + + Assert.assertTrue(token1.equals(token.getToken())); + Assert.assertTrue(expiresAt.getSecond() == token.getExpiresAt().getSecond()); + Assert.assertNotNull(identityClientMock); + } + } + @Test(expected = IllegalArgumentException.class) public void testWorkloadIdentityFlowFailureNoTenantId() { // setup From 20fd42eded15be298618203161a855fd8e077234 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Wed, 19 Apr 2023 16:57:38 -0700 Subject: [PATCH 2/5] fix tests --- .../identity/implementation/IdentityClient.java | 2 -- .../implementation/IdentityClientTests.java | 16 ++++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 37854cd4058a..9e62623ff720 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -1199,8 +1199,6 @@ private static Proxy proxyOptionsToJavaNetProxy(ProxyOptions options) { } } - - void openUrl(String url) throws IOException { Runtime rt = Runtime.getRuntime(); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java index aaa671b82bef..ed2baf965e50 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java @@ -640,7 +640,7 @@ private void mockForClientPemCertificate(String accessToken, TokenRequestContext } private void mockForMSICodeFlow(String tokenJson, Runnable test) throws Exception { - try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClient.class)) { + try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClientBase.class)) { URL url = mock(URL.class); HttpURLConnection huc = mock(HttpURLConnection.class); doNothing().when(huc).setRequestMethod(anyString()); @@ -649,14 +649,14 @@ private void mockForMSICodeFlow(String tokenJson, Runnable test) throws Exceptio when(url.openConnection()).thenReturn(huc); InputStream inputStream = new ByteArrayInputStream(tokenJson.getBytes(Charset.defaultCharset())); when(huc.getInputStream()).thenReturn(inputStream); - identityClientMockedStatic.when(() -> IdentityClient.getUrl(anyString())).thenReturn(url); + identityClientMockedStatic.when(() -> IdentityClientBase.getUrl(anyString())).thenReturn(url); test.run(); } } private void mockForServiceFabricCodeFlow(String tokenJson, Runnable test) throws Exception { - try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClient.class)) { + try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClientBase.class)) { URL url = mock(URL.class); HttpsURLConnection huc = mock(HttpsURLConnection.class); doNothing().when(huc).setRequestMethod(anyString()); @@ -666,13 +666,13 @@ private void mockForServiceFabricCodeFlow(String tokenJson, Runnable test) throw when(url.openConnection()).thenReturn(huc); InputStream inputStream = new ByteArrayInputStream(tokenJson.getBytes(Charset.defaultCharset())); when(huc.getInputStream()).thenReturn(inputStream); - identityClientMockedStatic.when(() -> IdentityClient.getUrl(anyString())).thenReturn(url); + identityClientMockedStatic.when(() -> IdentityClientBase.getUrl(anyString())).thenReturn(url); test.run(); } } private void mockForArcCodeFlow(int responseCode, Runnable test) throws Exception { - try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClient.class)) { + try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClientBase.class)) { URL url = mock(URL.class); HttpURLConnection huc = mock(HttpURLConnection.class); doNothing().when(huc).setRequestMethod(anyString()); @@ -681,13 +681,13 @@ private void mockForArcCodeFlow(int responseCode, Runnable test) throws Exceptio when(url.openConnection()).thenReturn(huc); when(huc.getInputStream()).thenThrow(new IOException()); when(huc.getResponseCode()).thenReturn(responseCode); - identityClientMockedStatic.when(() -> IdentityClient.getUrl(anyString())).thenReturn(url); + identityClientMockedStatic.when(() -> IdentityClientBase.getUrl(anyString())).thenReturn(url); test.run(); } } private void mockForIMDSCodeFlow(String endpoint, String tokenJson, Runnable test) throws Exception { - try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClient.class)) { + try (MockedStatic identityClientMockedStatic = mockStatic(IdentityClientBase.class)) { URL url = mock(URL.class); HttpURLConnection huc = mock(HttpURLConnection.class); doNothing().when(huc).setRequestMethod(anyString()); @@ -696,7 +696,7 @@ private void mockForIMDSCodeFlow(String endpoint, String tokenJson, Runnable tes when(url.openConnection()).thenReturn(huc); InputStream inputStream = new ByteArrayInputStream(tokenJson.getBytes(Charset.defaultCharset())); when(huc.getInputStream()).thenReturn(inputStream); - identityClientMockedStatic.when(() -> IdentityClient.getUrl(anyString())).thenReturn(url); + identityClientMockedStatic.when(() -> IdentityClientBase.getUrl(anyString())).thenReturn(url); test.run(); } } From 63fbfac4b337b5a1b94a134697107d9610eb9e5c Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 5 May 2023 12:53:37 -0700 Subject: [PATCH 3/5] revert cae beta feature --- .../azure/identity/implementation/IdentityClient.java | 9 --------- .../identity/implementation/IdentityClientBase.java | 11 ----------- 2 files changed, 20 deletions(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 9e62623ff720..5f0f81d8cc67 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -491,10 +491,6 @@ public Mono authenticateWithConfidentialClient(TokenRequestContext builder.clientCredential(ClientCredentialFactory .createFromClientAssertion(clientAssertionSupplier.get())); } - if (request.getClaims() != null) { - ClaimsRequest customClaimRequest = CustomClaimRequest.formatAsClaimsRequest(request.getClaims()); - builder.claims(customClaimRequest); - } return confidentialClient.acquireToken(builder.build()); } )).map(MsalToken::new); @@ -601,11 +597,6 @@ public Mono authenticateWithConfidentialClientCache(TokenRequestCon SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters.builder( new HashSet<>(request.getScopes())) .tenant(IdentityUtil.resolveTenantId(tenantId, request, options)); - if (request.getClaims() != null) { - ClaimsRequest customClaimRequest = CustomClaimRequest - .formatAsClaimsRequest(request.getClaims()); - parametersBuilder.claims(customClaimRequest); - } try { return confidentialClient.acquireTokenSilently(parametersBuilder.build()); } catch (MalformedURLException e) { diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java index 2d8f44b9ccc0..213f14f979da 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java @@ -228,12 +228,6 @@ ConfidentialClientApplication getConfidentialClient() { applicationBuilder.executorService(options.getExecutorService()); } - if (!options.isCp1Disabled()) { - Set set = new HashSet<>(1); - set.add("CP1"); - applicationBuilder.clientCapabilities(set); - } - TokenCachePersistenceOptions tokenCachePersistenceOptions = options.getTokenCacheOptions(); PersistentTokenCacheImpl tokenCache = null; if (tokenCachePersistenceOptions != null) { @@ -387,11 +381,6 @@ OnBehalfOfParameters buildOBOFlowParameters(TokenRequestContext request) { OnBehalfOfParameters.OnBehalfOfParametersBuilder builder = OnBehalfOfParameters .builder(new HashSet<>(request.getScopes()), options.getUserAssertion()) .tenant(IdentityUtil.resolveTenantId(tenantId, request, options)); - - if (request.getClaims() != null) { - ClaimsRequest customClaimRequest = CustomClaimRequest.formatAsClaimsRequest(request.getClaims()); - builder.claims(customClaimRequest); - } return builder.build(); } From 5a43881082ea1e1a1891adca93a4c1b1ba21b985 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Mon, 8 May 2023 09:16:12 -0700 Subject: [PATCH 4/5] code refactor + compile fix --- .../resources/spotbugs/spotbugs-exclude.xml | 7 +++ .../identity/WorkloadIdentityCredential.java | 4 +- .../WorkloadIdentityCredentialBuilder.java | 2 +- .../implementation/IdentityClient.java | 49 +++++++++++++++++++ .../implementation/IdentityClientBase.java | 39 +++++++++++++++ .../implementation/IdentitySyncClient.java | 47 ++++++++++++++++++ .../identity/DefaultAzureCredentialTest.java | 4 +- .../WorkloadIdentityCredentialTest.java | 4 +- 8 files changed, 149 insertions(+), 7 deletions(-) diff --git a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml index 402ebcf585f9..791a3f3fdee0 100644 --- a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml +++ b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml @@ -889,6 +889,13 @@ + + + + + + + diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java index a4abce48cbec..ba77c3b4727f 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredential.java @@ -105,7 +105,7 @@ public Mono getToken(TokenRequestContext request) { + " guide for more information." + " https://aka.ms/azsdk/java/identity/workloadidentitycredential/troubleshoot"))); } - return identityClient.authenticateWithExchangeToken(request); + return identityClient.authenticateWithWorkloadIdentityConfidentialClient(request); } @Override @@ -116,7 +116,7 @@ public AccessToken getTokenSync(TokenRequestContext request) { + " guide for more information." + " https://aka.ms/azsdk/java/identity/workloadidentitycredential/troubleshoot")); } - return identitySyncClient.authenticateWithExchangeTokenSync(request); + return identitySyncClient.authenticateWithWorkloadIdentityConfidentialClient(request); } String getClientId() { diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredentialBuilder.java index f20f29147858..8dbbe9564a44 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredentialBuilder.java @@ -72,6 +72,6 @@ public WorkloadIdentityCredential build() { ValidationUtil.validate(this.getClass().getSimpleName(), LOGGER, "Client ID", clientId, "Tenant ID", tenantId, "Service Token File Path", tokenFilePath); - return new WorkloadIdentityCredential(clientId, tenantId, tokenFilePath, identityClientOptions.clone()); + return new WorkloadIdentityCredential(tenantId, clientId, tokenFilePath, identityClientOptions.clone()); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index 5f0f81d8cc67..67b63ddc9a9e 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -18,6 +18,7 @@ import com.azure.identity.implementation.util.ScopeUtil; import com.fasterxml.jackson.databind.JsonNode; import com.microsoft.aad.msal4j.AuthorizationCodeParameters; +import com.microsoft.aad.msal4j.AppTokenProviderParameters; import com.microsoft.aad.msal4j.ClaimsRequest; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ClientCredentialParameters; @@ -30,6 +31,7 @@ import com.microsoft.aad.msal4j.PublicClientApplication; import com.microsoft.aad.msal4j.RefreshTokenParameters; import com.microsoft.aad.msal4j.SilentParameters; +import com.microsoft.aad.msal4j.TokenProviderResult; import com.microsoft.aad.msal4j.UserNamePasswordParameters; import com.sun.jna.Platform; import reactor.core.publisher.Flux; @@ -61,6 +63,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -71,6 +74,7 @@ public class IdentityClient extends IdentityClientBase { private final SynchronizedAccessor publicClientApplicationAccessor; private final SynchronizedAccessor confidentialClientApplicationAccessor; private final SynchronizedAccessor managedIdentityConfidentialClientApplicationAccessor; + private final SynchronizedAccessor workloadIdentityConfidentialClientApplicationAccessor; private final SynchronizedAccessor clientAssertionAccessor; @@ -105,6 +109,9 @@ public class IdentityClient extends IdentityClientBase { this.managedIdentityConfidentialClientApplicationAccessor = new SynchronizedAccessor<>(this::getManagedIdentityConfidentialClientApplication); + this.workloadIdentityConfidentialClientApplicationAccessor = + new SynchronizedAccessor<>(this::getWorkloadIdentityConfidentialClientApplication); + Duration cacheTimeout = (clientAssertionTimeout == null) ? Duration.ofMinutes(5) : clientAssertionTimeout; this.clientAssertionAccessor = new SynchronizedAccessor<>(this::parseClientAssertion, cacheTimeout); } @@ -129,6 +136,16 @@ private Mono getManagedIdentityConfidentialClient }); } + private Mono getWorkloadIdentityConfidentialClientApplication() { + return Mono.defer(() -> { + try { + return Mono.just(super.getWorkloadIdentityConfidentialClient()); + } catch (RuntimeException e) { + return Mono.error(e); + } + }); + } + @Override Mono getTokenFromTargetManagedIdentity(TokenRequestContext tokenRequestContext) { ManagedIdentityParameters parameters = options.getManagedIdentityParameters(); @@ -509,6 +526,19 @@ public Mono authenticateWithManagedIdentityConfidentialClient(Token .map(MsalToken::new); } + public Mono authenticateWithWorkloadIdentityConfidentialClient(TokenRequestContext request) { + return workloadIdentityConfidentialClientApplicationAccessor.getValue() + .flatMap(confidentialClient -> Mono.fromFuture(() -> { + ClientCredentialParameters.ClientCredentialParametersBuilder builder = + ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) + .tenant(IdentityUtil + .resolveTenantId(tenantId, request, options)); + return confidentialClient.acquireToken(builder.build()); + } + )).onErrorMap(t -> new CredentialUnavailableException("Managed Identity authentication is not available.", t)) + .map(MsalToken::new); + } + /** * Asynchronously acquire a token from Active Directory with a username and a password. * @@ -1223,4 +1253,23 @@ public IdentityClientOptions getIdentityClientOptions() { private boolean isADFSTenant() { return ADFS_TENANT.equals(this.tenantId); } + + Function> getWorkloadIdentityTokenProvider() { + return appTokenProviderParameters -> { + TokenRequestContext trc = new TokenRequestContext() + .setScopes(new ArrayList<>(appTokenProviderParameters.scopes)) + .setClaims(appTokenProviderParameters.claims) + .setTenantId(appTokenProviderParameters.tenantId); + + Mono accessTokenAsync = authenticateWithExchangeToken(trc); + + return accessTokenAsync.map(accessToken -> { + TokenProviderResult result = new TokenProviderResult(); + result.setAccessToken(accessToken.getToken()); + result.setTenantId(trc.getTenantId()); + result.setExpiresInSeconds(accessToken.getExpiresAt().toEpochSecond()); + return result; + }).toFuture(); + }; + } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java index 213f14f979da..cb5d20aeb51b 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java @@ -32,6 +32,7 @@ import com.azure.identity.implementation.util.CertificateUtil; import com.azure.identity.implementation.util.IdentityUtil; import com.azure.identity.implementation.util.LoggingUtil; +import com.microsoft.aad.msal4j.AppTokenProviderParameters; import com.microsoft.aad.msal4j.ClaimsRequest; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ConfidentialClientApplication; @@ -78,8 +79,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -361,6 +364,42 @@ ConfidentialClientApplication getManagedIdentityConfidentialClient() { return applicationBuilder.build(); } + ConfidentialClientApplication getWorkloadIdentityConfidentialClient() { + String authorityUrl = TRAILING_FORWARD_SLASHES.matcher(options.getAuthorityHost()).replaceAll("") + + "/" + tenantId; + + // Temporarily pass in Dummy Client secret and Client ID. until MSal removes its requirements. + IClientCredential credential = ClientCredentialFactory + .createFromSecret(clientSecret != null ? clientSecret : "dummy-secret"); + ConfidentialClientApplication.Builder applicationBuilder = + ConfidentialClientApplication.builder(clientId == null ? "SYSTEM-ASSIGNED-MANAGED-IDENTITY" + : clientId, credential); + + try { + applicationBuilder = applicationBuilder.authority(authorityUrl).instanceDiscovery(options.getInstanceDiscovery()); + } catch (MalformedURLException e) { + throw LOGGER.logExceptionAsWarning(new IllegalStateException(e)); + } + + applicationBuilder.appTokenProvider(getWorkloadIdentityTokenProvider()); + + + initializeHttpPipelineAdapter(); + if (httpPipelineAdapter != null) { + applicationBuilder.httpClient(httpPipelineAdapter); + } else { + applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions())); + } + + if (options.getExecutorService() != null) { + applicationBuilder.executorService(options.getExecutorService()); + } + + return applicationBuilder.build(); + } + + abstract Function> getWorkloadIdentityTokenProvider(); + DeviceCodeFlowParameters.DeviceCodeFlowParametersBuilder buildDeviceCodeFlowParameters(TokenRequestContext request, Consumer deviceCodeConsumer) { DeviceCodeFlowParameters.DeviceCodeFlowParametersBuilder parametersBuilder = DeviceCodeFlowParameters.builder( diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java index 45344b2cdeab..6c2eb9415326 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java @@ -12,6 +12,7 @@ import com.azure.identity.implementation.util.IdentityUtil; import com.azure.identity.implementation.util.LoggingUtil; import com.azure.identity.implementation.util.ScopeUtil; +import com.microsoft.aad.msal4j.AppTokenProviderParameters; import com.microsoft.aad.msal4j.ClaimsRequest; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ClientCredentialParameters; @@ -22,6 +23,7 @@ import com.microsoft.aad.msal4j.InteractiveRequestParameters; import com.microsoft.aad.msal4j.PublicClientApplication; import com.microsoft.aad.msal4j.SilentParameters; +import com.microsoft.aad.msal4j.TokenProviderResult; import com.microsoft.aad.msal4j.UserNamePasswordParameters; import reactor.core.publisher.Mono; @@ -36,10 +38,13 @@ import java.nio.file.Paths; 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; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public class IdentitySyncClient extends IdentityClientBase { @@ -47,6 +52,7 @@ public class IdentitySyncClient extends IdentityClientBase { private final SynchronousAccessor publicClientApplicationAccessor; private final SynchronousAccessor confidentialClientApplicationAccessor; private final SynchronousAccessor managedIdentityConfidentialClientApplicationAccessor; + private final SynchronousAccessor workloadIdentityConfidentialClientApplicationAccessor; private final SynchronousAccessor clientAssertionAccessor; @@ -81,6 +87,9 @@ public class IdentitySyncClient extends IdentityClientBase { this.managedIdentityConfidentialClientApplicationAccessor = new SynchronousAccessor<>(() -> this.getManagedIdentityConfidentialClient()); + this.workloadIdentityConfidentialClientApplicationAccessor = new SynchronousAccessor<>(() -> + this.getWorkloadIdentityConfidentialClient()); + this.clientAssertionAccessor = clientAssertionTimeout == null ? new SynchronousAccessor<>(() -> parseClientAssertion(), Duration.ofMinutes(5)) : new SynchronousAccessor<>(() -> parseClientAssertion(), clientAssertionTimeout); @@ -401,6 +410,44 @@ public AccessToken authenticateWithExchangeTokenSync(TokenRequestContext request } } + Function> getWorkloadIdentityTokenProvider() { + return appTokenProviderParameters -> { + TokenRequestContext trc = new TokenRequestContext() + .setScopes(new ArrayList<>(appTokenProviderParameters.scopes)) + .setClaims(appTokenProviderParameters.claims) + .setTenantId(appTokenProviderParameters.tenantId); + + AccessToken accessToken = authenticateWithExchangeTokenSync(trc); + + Supplier tokenProviderResultSupplier = () -> { + TokenProviderResult result = new TokenProviderResult(); + result.setAccessToken(accessToken.getToken()); + result.setTenantId(trc.getTenantId()); + result.setExpiresInSeconds(accessToken.getExpiresAt().toEpochSecond()); + return result; + }; + + return options.getExecutorService() != null + ? CompletableFuture.supplyAsync(tokenProviderResultSupplier, options.getExecutorService()) + : CompletableFuture.supplyAsync(tokenProviderResultSupplier); + }; + } + + public AccessToken authenticateWithWorkloadIdentityConfidentialClient(TokenRequestContext request) { + ConfidentialClientApplication confidentialClient = + workloadIdentityConfidentialClientApplicationAccessor.getValue(); + + try { + ClientCredentialParameters.ClientCredentialParametersBuilder builder = + ClientCredentialParameters.builder(new HashSet<>(request.getScopes())) + .tenant(IdentityUtil + .resolveTenantId(tenantId, request, options)); + return new MsalToken(confidentialClient.acquireToken(builder.build()).get()); + } catch (Exception e) { + throw new CredentialUnavailableException("Managed Identity authentication is not available.", e); + } + } + /** * Get the configured identity client options. * diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java index 056ba65c473f..739f46e717b2 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java @@ -100,7 +100,7 @@ public void testUseWorkloadIdentityCredentialWithManagedIdentityClientId() { // mock try (MockedConstruction mocked = mockConstruction(IdentityClient.class, (identityClient, context) -> { when(identityClient.authenticateWithAzureDeveloperCli(request)).thenReturn(Mono.empty()); - when(identityClient.authenticateWithExchangeToken(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + when(identityClient.authenticateWithWorkloadIdentityConfidentialClient(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); }); MockedConstruction ijcredential = mockConstruction(IntelliJCredential.class, (intelliJCredential, context) -> { when(intelliJCredential.getToken(request)).thenReturn(Mono.empty()); })) { @@ -130,7 +130,7 @@ public void testUseWorkloadIdentityCredentialWithWorkloadClientId() { // mock try (MockedConstruction mocked = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithExchangeToken(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + when(identityClient.authenticateWithWorkloadIdentityConfidentialClient(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); })) { // test String clientId = "dummy-client-id"; diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java index 13efef5b5864..de9b862b3329 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialTest.java @@ -38,7 +38,7 @@ public void testWorkloadIdentityFlow() { // mock try (MockedConstruction identityClientMock = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithExchangeToken(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + when(identityClient.authenticateWithWorkloadIdentityConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); })) { // test WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder() @@ -66,7 +66,7 @@ public void testWorkloadIdentityFlowSync() { // mock try (MockedConstruction identityClientMock = mockConstruction(IdentitySyncClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithExchangeTokenSync(request1)).thenReturn(TestUtils.getMockAccessTokenSync(token1, expiresAt)); + when(identityClient.authenticateWithWorkloadIdentityConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessTokenSync(token1, expiresAt)); })) { // test WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder() From c0650c343f501c1146f0aac89738ade9fd61a5f1 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Mon, 8 May 2023 09:54:41 -0700 Subject: [PATCH 5/5] fix compile issue. --- .../com/azure/identity/implementation/IdentityClientBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java index df3ab139c34d..e3dc53d159e3 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java @@ -376,7 +376,7 @@ ConfidentialClientApplication getWorkloadIdentityConfidentialClient() { : clientId, credential); try { - applicationBuilder = applicationBuilder.authority(authorityUrl).instanceDiscovery(options.getInstanceDiscovery()); + applicationBuilder = applicationBuilder.authority(authorityUrl).instanceDiscovery(options.getDisableAuthorityValidationAndInstanceDiscovery()); } catch (MalformedURLException e) { throw LOGGER.logExceptionAsWarning(new IllegalStateException(e)); }