diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 73d4dac8e44f..17e06411afa7 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -134,9 +134,9 @@ com.azure:azure-health-deidentification;1.0.0;1.1.0-beta.2 com.azure:azure-health-insights-clinicalmatching;1.0.0-beta.1;1.0.0-beta.2 com.azure:azure-health-insights-cancerprofiling;1.0.0-beta.1;1.0.0-beta.2 com.azure:azure-health-insights-radiologyinsights;1.1.5;1.2.0-beta.1 -com.azure:azure-identity;1.18.1;1.19.0-beta.2 +com.azure:azure-identity;1.18.1;1.18.2 com.azure:azure-identity-extensions;1.2.6;1.3.0-beta.1 -com.azure:azure-identity-broker;1.1.18;1.2.0-beta.1 +com.azure:azure-identity-broker;1.1.18;1.1.19 com.azure:azure-identity-broker-samples;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-identity-perf;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-iot-deviceupdate;1.0.31;1.1.0-beta.1 diff --git a/sdk/e2e/pom.xml b/sdk/e2e/pom.xml index 160b37d81062..5f67e8f09465 100644 --- a/sdk/e2e/pom.xml +++ b/sdk/e2e/pom.xml @@ -39,7 +39,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 com.azure diff --git a/sdk/identity/azure-identity-broker-samples/pom.xml b/sdk/identity/azure-identity-broker-samples/pom.xml index a10f87f1ec9b..e1c3decd07bd 100644 --- a/sdk/identity/azure-identity-broker-samples/pom.xml +++ b/sdk/identity/azure-identity-broker-samples/pom.xml @@ -34,12 +34,12 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 com.azure azure-identity-broker - 1.2.0-beta.1 + 1.1.19 org.openjfx diff --git a/sdk/identity/azure-identity-broker/CHANGELOG.md b/sdk/identity/azure-identity-broker/CHANGELOG.md index e3de2b6bc2b6..7a9fc9feeb35 100644 --- a/sdk/identity/azure-identity-broker/CHANGELOG.md +++ b/sdk/identity/azure-identity-broker/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.2.0-beta.1 (Unreleased) +## 1.1.19 (2026-01-20) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-identity` from `1.18.1` to version `1.18.2`. ## 1.1.18 (2025-10-13) diff --git a/sdk/identity/azure-identity-broker/pom.xml b/sdk/identity/azure-identity-broker/pom.xml index 8cd360d58ad6..9772ac8c4fa5 100644 --- a/sdk/identity/azure-identity-broker/pom.xml +++ b/sdk/identity/azure-identity-broker/pom.xml @@ -7,7 +7,7 @@ com.azure azure-identity-broker - 1.2.0-beta.1 + 1.1.19 Microsoft Azure Identity Brokered Authentication Library This module contains brokered authentication extensions for Microsoft Azure Identity. @@ -36,7 +36,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 diff --git a/sdk/identity/azure-identity-perf/pom.xml b/sdk/identity/azure-identity-perf/pom.xml index efc51614d446..6b5fde77af28 100644 --- a/sdk/identity/azure-identity-perf/pom.xml +++ b/sdk/identity/azure-identity-perf/pom.xml @@ -25,7 +25,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 com.azure diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 645c58d43caf..223835f88472 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,23 +1,16 @@ # Release History -## 1.19.0-beta.2 (Unreleased) - -### Features Added - -### Breaking Changes - -### Bugs Fixed +## 1.18.2 (2026-01-20) ### Other Changes -- Removed unused jetty, redisson, and lettuce-core dependencies. -## 1.19.0-beta.1 (2025-11-14) +- Removed unused jetty, redisson, and lettuce-core dependencies. -### Features Added -- Added `enableAzureTokenProxy()` method to `WorkloadIdentityCredentialBuilder` to enable custom token proxy support for Azure Kubernetes clusters. When enabled, the credential attempts to use a custom token proxy configured through environment variables (`AZURE_KUBERNETES_TOKEN_PROXY`, `AZURE_KUBERNETES_CA_FILE`, `AZURE_KUBERNETES_CA_DATA`, `AZURE_KUBERNETES_SNI_NAME`). +#### Dependency Updates -### Other Changes -- Ported the authentication flow of WorkloadIdentityCredential to use Msal4j. +- Upgraded `azure-core` from `1.57.0` to version `1.57.1`. +- Upgraded `azure-core-http-netty` from `1.16.2` to version `1.16.3`. +- Upgraded `azure-json` from `1.5.0` to version `1.5.1`. ## 1.18.1 (2025-10-13) diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index a8f1c6a65b1f..8e60b6db447a 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -6,7 +6,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 Microsoft Azure client library for Identity This module contains client library for Microsoft Azure Identity. 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 a19bd61d66ae..95f16fd9628d 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 @@ -6,9 +6,6 @@ import com.azure.core.util.Configuration; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; -import com.azure.identity.implementation.customtokenproxy.CustomTokenProxyConfiguration; -import com.azure.identity.implementation.customtokenproxy.CustomTokenProxyHttpClient; -import com.azure.identity.implementation.customtokenproxy.ProxyConfig; import com.azure.identity.implementation.util.ValidationUtil; import static com.azure.identity.ManagedIdentityCredential.AZURE_FEDERATED_TOKEN_FILE; @@ -50,7 +47,6 @@ public class WorkloadIdentityCredentialBuilder extends AadCredentialBuilderBase { private static final ClientLogger LOGGER = new ClientLogger(WorkloadIdentityCredentialBuilder.class); private String tokenFilePath; - private boolean enableTokenProxy; /** * Creates an instance of a WorkloadIdentityCredentialBuilder. @@ -70,19 +66,6 @@ public WorkloadIdentityCredentialBuilder tokenFilePath(String tokenFilePath) { return this; } - /** - * Enables the custom token proxy feature for clusters running in Azure. - * When enabled, the credential will attempt to use a custom token proxy configured through - * environment variables (AZURE_KUBERNETES_TOKEN_PROXY, AZURE_KUBERNETES_CA_FILE, - * AZURE_KUBERNETES_CA_DATA, AZURE_KUBERNETES_SNI_NAME). - * - * @return An updated instance of this builder with Azure token proxy enabled. - */ - public WorkloadIdentityCredentialBuilder enableAzureTokenProxy() { - this.enableTokenProxy = true; - return this; - } - /** * Creates new {@link WorkloadIdentityCredential} with the configured options set. * @@ -105,13 +88,6 @@ public WorkloadIdentityCredential build() { ValidationUtil.validate(this.getClass().getSimpleName(), LOGGER, "Client ID", clientIdInput, "Tenant ID", tenantIdInput, "Service Token File Path", federatedTokenFilePathInput); - if (enableTokenProxy) { - ProxyConfig proxyConfig = CustomTokenProxyConfiguration.parseAndValidate(configuration); - if (proxyConfig != null) { - identityClientOptions.setHttpClient(new CustomTokenProxyHttpClient(proxyConfig)); - } - } - return new WorkloadIdentityCredential(tenantIdInput, clientIdInput, federatedTokenFilePathInput, identityClientOptions.clone()); } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/SniSslSocketFactory.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/SniSslSocketFactory.java deleted file mode 100644 index fb7eff32c362..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/SniSslSocketFactory.java +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation; - -import com.azure.core.util.CoreUtils; -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import java.io.IOException; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.net.InetAddress; - -public final class SniSslSocketFactory extends SSLSocketFactory { - private final SSLSocketFactory sslSocketFactory; - private final String sniName; - - public SniSslSocketFactory(SSLSocketFactory sslSocketFactory, String sniName) { - this.sslSocketFactory = sslSocketFactory; - this.sniName = sniName; - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { - Socket sslSocket = (SSLSocket) sslSocketFactory.createSocket(s, host, port, autoClose); - configureSni(sslSocket); - return sslSocket; - } - - @Override - public Socket createSocket(String host, int port) throws IOException { - Socket sslSocket = sslSocketFactory.createSocket(host, port); - configureSni(sslSocket); - return sslSocket; - } - - @Override - public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException { - Socket sslSocket = sslSocketFactory.createSocket(host, port, localAddress, localPort); - configureSni(sslSocket); - return sslSocket; - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - Socket sslSocket = sslSocketFactory.createSocket(host, port); - configureSni(sslSocket); - return sslSocket; - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) - throws IOException { - Socket sslSocket = sslSocketFactory.createSocket(address, port, localAddress, localPort); - configureSni(sslSocket); - return sslSocket; - } - - @Override - public String[] getDefaultCipherSuites() { - return sslSocketFactory.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return sslSocketFactory.getSupportedCipherSuites(); - } - - private void configureSni(Socket socket) { - if (socket instanceof SSLSocket && !CoreUtils.isNullOrEmpty(sniName)) { - SSLSocket sslSocket = (SSLSocket) socket; - SSLParameters sslParameters = sslSocket.getSSLParameters(); - sslParameters.setServerNames(Collections.singletonList(new RawSniServerName(sniName))); - sslSocket.setSSLParameters(sslParameters); - } - } - - private static final class RawSniServerName extends SNIServerName { - RawSniServerName(String sniHost) { - super(0, sniHost.getBytes(StandardCharsets.UTF_8)); - } - } -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyConfiguration.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyConfiguration.java deleted file mode 100644 index 9c789aa758a6..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyConfiguration.java +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation.customtokenproxy; - -import com.azure.core.util.logging.ClientLogger; - -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.net.URISyntaxException; - -import com.azure.core.util.Configuration; -import com.azure.core.util.CoreUtils; - -public final class CustomTokenProxyConfiguration { - - private static final ClientLogger LOGGER = new ClientLogger(CustomTokenProxyConfiguration.class); - - private static final String AZURE_KUBERNETES_TOKEN_PROXY = "AZURE_KUBERNETES_TOKEN_PROXY"; - private static final String AZURE_KUBERNETES_CA_FILE = "AZURE_KUBERNETES_CA_FILE"; - private static final String AZURE_KUBERNETES_CA_DATA = "AZURE_KUBERNETES_CA_DATA"; - private static final String AZURE_KUBERNETES_SNI_NAME = "AZURE_KUBERNETES_SNI_NAME"; - - private CustomTokenProxyConfiguration() { - } - - public static ProxyConfig parseAndValidate(Configuration configuration) { - String tokenProxyUrl = configuration.get(AZURE_KUBERNETES_TOKEN_PROXY); - String caFile = configuration.get(AZURE_KUBERNETES_CA_FILE); - String caData = configuration.get(AZURE_KUBERNETES_CA_DATA); - String sniName = configuration.get(AZURE_KUBERNETES_SNI_NAME); - - if (CoreUtils.isNullOrEmpty(tokenProxyUrl)) { - if (!CoreUtils.isNullOrEmpty(sniName) - || !CoreUtils.isNullOrEmpty(caFile) - || !CoreUtils.isNullOrEmpty(caData)) { - throw LOGGER.logExceptionAsError(new IllegalArgumentException( - "AZURE_KUBERNETES_TOKEN_PROXY is not set but other custom endpoint-related environment variables are present")); - } - return null; - } - - if (!CoreUtils.isNullOrEmpty(caFile) && !CoreUtils.isNullOrEmpty(caData)) { - throw LOGGER.logExceptionAsError(new IllegalArgumentException( - "Only one of AZURE_KUBERNETES_CA_FILE or AZURE_KUBERNETES_CA_DATA can be set.")); - } - - URL proxyUrl = validateProxyUrl(tokenProxyUrl); - - byte[] caCertBytes = null; - if (!CoreUtils.isNullOrEmpty(caData)) { - caCertBytes = caData.getBytes(StandardCharsets.UTF_8); - } - - ProxyConfig config = new ProxyConfig(proxyUrl, sniName, caFile, caCertBytes); - return config; - } - - private static URL validateProxyUrl(String endpoint) { - if (CoreUtils.isNullOrEmpty(endpoint)) { - throw LOGGER.logExceptionAsError(new IllegalArgumentException("Proxy endpoint cannot be null or empty")); - } - - try { - URI tokenProxy = new URI(endpoint); - - if (!"https".equals(tokenProxy.getScheme())) { - throw LOGGER.logExceptionAsError(new IllegalArgumentException( - "Custom token endpoint must use https scheme, got: " + tokenProxy.getScheme())); - } - - if (tokenProxy.getRawUserInfo() != null) { - throw LOGGER.logExceptionAsError( - new IllegalArgumentException("Custom token endpoint URL must not contain user info: " + endpoint)); - } - - if (tokenProxy.getRawQuery() != null) { - throw LOGGER.logExceptionAsError( - new IllegalArgumentException("Custom token endpoint URL must not contain a query: " + endpoint)); - } - - if (tokenProxy.getRawFragment() != null) { - throw LOGGER.logExceptionAsError( - new IllegalArgumentException("Custom token endpoint URL must not contain a fragment: " + endpoint)); - } - - if (tokenProxy.getRawPath() == null || tokenProxy.getRawPath().isEmpty()) { - tokenProxy = new URI(tokenProxy.getScheme(), null, tokenProxy.getHost(), tokenProxy.getPort(), "/", - null, null); - } - - return tokenProxy.toURL(); - - } catch (URISyntaxException | IllegalArgumentException e) { - throw LOGGER.logExceptionAsError(new IllegalArgumentException("Failed to normalize proxy URL path", e)); - } catch (Exception e) { - throw new RuntimeException("Unexpected error while validating proxy URL: " + endpoint, e); - } - } - -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyHttpClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyHttpClient.java deleted file mode 100644 index ef2c7f0446a6..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyHttpClient.java +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation.customtokenproxy; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyStore; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; - -import com.azure.core.http.HttpClient; -import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; -import com.azure.core.util.BinaryData; -import com.azure.core.util.Context; -import com.azure.core.util.CoreUtils; -import com.azure.core.util.logging.ClientLogger; -import com.azure.identity.implementation.SniSslSocketFactory; - -import reactor.core.publisher.Mono; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class CustomTokenProxyHttpClient implements HttpClient { - - private static final ClientLogger LOGGER = new ClientLogger(CustomTokenProxyHttpClient.class); - - private final ProxyConfig proxyConfig; - private volatile SSLContext cachedSSLContext; - private volatile byte[] cachedFileContentHash; - private volatile int cachedFileContentLength; - private volatile SSLSocketFactory cachedSslSocketFactory; - private final URL proxyUrl; - private final String sniName; - private final byte[] caData; - private final String caFile; - - public CustomTokenProxyHttpClient(ProxyConfig proxyConfig) { - this.proxyConfig = proxyConfig; - this.proxyUrl = proxyConfig.getTokenProxyUrl(); - this.sniName = proxyConfig.getSniName(); - this.caData = proxyConfig.getCaData(); - this.caFile = proxyConfig.getCaFile(); - } - - @Override - public Mono send(HttpRequest request) { - return Mono.fromCallable(() -> sendSync(request, Context.NONE)); - } - - @Override - public HttpResponse sendSync(HttpRequest request, Context context) { - try { - HttpURLConnection connection = createConnection(request); - return new CustomTokenProxyHttpResponse(request, connection); - } catch (IOException e) { - throw LOGGER.logExceptionAsError(new RuntimeException("Failed to create connection to token proxy", e)); - } - } - - private HttpURLConnection createConnection(HttpRequest request) throws IOException { - URL updatedUrl = rewriteTokenRequestForProxy(request.getUrl()); - HttpsURLConnection connection = (HttpsURLConnection) updatedUrl.openConnection(); - try { - SSLSocketFactory sslSocketFactory = getSSLSocketFactory(); - connection.setSSLSocketFactory(sslSocketFactory); - if (!CoreUtils.isNullOrEmpty(sniName)) { - connection.setHostnameVerifier(sniAwareVerifier(sniName, proxyUrl)); - } - } catch (Exception e) { - throw LOGGER.logExceptionAsError(new RuntimeException("Failed to set up SSL context for token proxy", e)); - } - - String method = request.getHttpMethod().toString(); - connection.setRequestMethod(method); - connection.setInstanceFollowRedirects(false); - connection.setConnectTimeout(10_000); - connection.setReadTimeout(20_000); - connection.setDoOutput(true); - - BinaryData bodyData = request.getBodyAsBinaryData(); - - request.getHeaders().forEach(header -> { - connection.addRequestProperty(header.getName(), header.getValue()); - }); - - if (bodyData != null) { - byte[] bytes = bodyData.toBytes(); - if (bytes != null && bytes.length > 0) { - try (OutputStream os = connection.getOutputStream()) { - os.write(bytes); - os.flush(); - } - } - } - return connection; - } - - private URL rewriteTokenRequestForProxy(URL originalUrl) throws MalformedURLException { - try { - String originalPath = originalUrl.getPath(); - String originalQuery = originalUrl.getQuery(); - - String tokenProxyBase = proxyUrl.toString(); - if (!tokenProxyBase.endsWith("/")) { - tokenProxyBase += "/"; - } - - URI combined = URI.create(tokenProxyBase) - .resolve(originalPath.startsWith("/") ? originalPath.substring(1) : originalPath); - - String combinedStr = combined.toString(); - if (originalQuery != null && !originalQuery.isEmpty()) { - combinedStr += "?" + originalQuery; - } - - return URI.create(combinedStr).toURL(); - - } catch (Exception e) { - throw LOGGER.logExceptionAsError(new RuntimeException("Failed to rewrite token request for proxy", e)); - } - } - - private SSLSocketFactory getSSLSocketFactory() { - SSLContext sslContext = getSSLContext(); - if (cachedSslSocketFactory == null) { - synchronized (this) { - if (cachedSslSocketFactory == null) { - SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - if (!CoreUtils.isNullOrEmpty(sniName)) { - sslSocketFactory = new SniSslSocketFactory(sslSocketFactory, sniName); - } - cachedSslSocketFactory = sslSocketFactory; - } - } - } - return cachedSslSocketFactory; - } - - private SSLContext getSSLContext() { - try { - // If no CA override provided, use default - if (CoreUtils.isNullOrEmpty(caFile) && (caData == null || caData.length == 0)) { - if (cachedSSLContext == null) { - synchronized (this) { - if (cachedSSLContext == null) { - cachedSSLContext = SSLContext.getDefault(); - cachedSslSocketFactory = null; - } - } - } - return cachedSSLContext; - } - - // If CA data provided, use it - if (CoreUtils.isNullOrEmpty(caFile)) { - if (cachedSSLContext == null) { - synchronized (this) { - if (cachedSSLContext == null) { - cachedSSLContext = createSslContextFromBytes(caData); - cachedSslSocketFactory = null; - } - } - } - return cachedSSLContext; - } - - // If CA file provided, read it (and re-read if it changes) - Path path = Paths.get(caFile); - if (!Files.exists(path)) { - throw LOGGER.logExceptionAsError(new RuntimeException("CA file not found: " + caFile)); - } - - byte[] currentContent = Files.readAllBytes(path); - int currentLength = currentContent.length; - byte[] currentHash = generateSHA256Hash(currentContent); - - synchronized (this) { - if (currentLength == 0) { - if (cachedSSLContext == null) { - throw LOGGER.logExceptionAsError(new IllegalStateException("CA file " + caFile + " is empty")); - } - LOGGER.warning("CA file " + caFile + " is empty, using cached SSL context from previous load"); - return cachedSSLContext; - } - - if (cachedSSLContext == null - || currentLength != cachedFileContentLength - || !Arrays.equals(currentHash, cachedFileContentHash)) { - cachedSSLContext = createSslContextFromBytes(currentContent); - cachedFileContentLength = currentLength; - cachedFileContentHash = currentHash; - cachedSslSocketFactory = null; - } - } - return cachedSSLContext; - - } catch (Exception e) { - throw LOGGER.logExceptionAsError(new RuntimeException("Failed to initialize SSLContext for proxy", e)); - } - } - - // Create SSLContext from byte array containing PEM certificate data - private SSLContext createSslContextFromBytes(byte[] certificateData) { - try (InputStream inputStream = new ByteArrayInputStream(certificateData)) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - - List certificates = new ArrayList<>(); - while (true) { - try { - X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); - certificates.add(cert); - } catch (CertificateException e) { - break; - } - } - - if (certificates.isEmpty()) { - throw LOGGER.logExceptionAsError(new IllegalStateException("No valid certificates found")); - } - return createSslContext(certificates); - } catch (Exception e) { - throw LOGGER.logExceptionAsError(new RuntimeException("Failed to create SSLContext from bytes", e)); - } - } - - private SSLContext createSslContext(List certificates) { - try { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(null, null); - int index = 1; - for (X509Certificate caCert : certificates) { - keystore.setCertificateEntry("ca-cert-" + index, caCert); - index++; - } - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keystore); - - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, tmf.getTrustManagers(), null); - return context; - } catch (Exception e) { - throw LOGGER.logExceptionAsError(new RuntimeException("Failed to create SSLContext", e)); - } - } - - private static HostnameVerifier sniAwareVerifier(String sniName, URL customProxyUrl) { - return (urlHost, session) -> { - String peerHost = session.getPeerHost(); - String proxyHost = customProxyUrl.getHost(); - - if (peerHost.equalsIgnoreCase(proxyHost)) { - if (!CoreUtils.isNullOrEmpty(sniName)) { - try { - Certificate[] certificates = session.getPeerCertificates(); - if (certificates.length > 0 && certificates[0] instanceof X509Certificate) { - X509Certificate cert = (X509Certificate) certificates[0]; - return certificateContainsDnsName(cert, sniName); - } - return false; - } catch (SSLPeerUnverifiedException e) { - return false; - } - } - return true; - } - - return false; - }; - } - - private static boolean certificateContainsDnsName(X509Certificate cert, String dnsName) { - try { - Collection> sanList = cert.getSubjectAlternativeNames(); - if (sanList != null) { - for (List san : sanList) { - if (san.size() >= 2 && san.get(0).equals(2)) { - String certDnsName = (String) san.get(1); - if (certDnsName.equalsIgnoreCase(dnsName)) { - return true; - } - } - } - } - } catch (CertificateParsingException e) { - return false; - } - return false; - } - - private static byte[] generateSHA256Hash(byte[] data) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] encodedHash = digest.digest(data); - return encodedHash; - } catch (NoSuchAlgorithmException e) { - throw LOGGER.logExceptionAsError(new RuntimeException("SHA-256 algorithm not found", e)); - } - } -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyHttpResponse.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyHttpResponse.java deleted file mode 100644 index f31b859985e1..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/CustomTokenProxyHttpResponse.java +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation.customtokenproxy; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; - -import com.azure.core.http.HttpHeaderName; -import com.azure.core.http.HttpHeaders; -import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; -import com.azure.core.util.logging.ClientLogger; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public final class CustomTokenProxyHttpResponse extends HttpResponse { - - private static final ClientLogger LOGGER = new ClientLogger(CustomTokenProxyHttpResponse.class); - - // private final HttpRequest request; - private final int statusCode; - private final HttpHeaders headers; - private final HttpURLConnection connection; - private byte[] cachedResponseBodyBytes; - - public CustomTokenProxyHttpResponse(HttpRequest request, HttpURLConnection connection) { - super(request); - this.connection = connection; - this.statusCode = extractStatusCode(connection); - this.headers = extractHeaders(connection); - } - - private HttpHeaders extractHeaders(HttpURLConnection connection) { - HttpHeaders headers = new HttpHeaders(); - for (Map.Entry> entry : connection.getHeaderFields().entrySet()) { - String headerName = entry.getKey(); - if (headerName != null) { - for (String headerValue : entry.getValue()) { - headers.add(HttpHeaderName.fromString(headerName), headerValue); - } - } - } - return headers; - } - - private int extractStatusCode(HttpURLConnection connection) { - try { - return connection.getResponseCode(); - } catch (IOException e) { - throw LOGGER - .logExceptionAsError(new RuntimeException("Failed to get status code from token proxy response", e)); - } - } - - @Override - public int getStatusCode() { - return statusCode; - } - - @Override - public String getHeaderValue(String name) { - return headers.getValue(HttpHeaderName.fromString(name)); - } - - @Override - public HttpHeaders getHeaders() { - return headers; - } - - @Override - public Mono getBodyAsByteArray() { - return Mono.fromCallable(() -> { - if (cachedResponseBodyBytes != null) { - return cachedResponseBodyBytes; - } - - InputStream stream = getResponseStream(); - if (stream == null) { - cachedResponseBodyBytes = new byte[0]; - return cachedResponseBodyBytes; - } - - try (InputStream notNullStream = stream) { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int n; - byte[] temp = new byte[4096]; - while ((n = notNullStream.read(temp)) != -1) { - buffer.write(temp, 0, n); - } - cachedResponseBodyBytes = buffer.toByteArray(); - return cachedResponseBodyBytes; - } - }); - } - - @Override - public Flux getBody() { - return getBodyAsByteArray().flatMapMany(bytes -> Flux.just(ByteBuffer.wrap(bytes))); - } - - @Override - public Mono getBodyAsString() { - return getBodyAsString(StandardCharsets.UTF_8); - } - - @Override - public Mono getBodyAsString(Charset charset) { - return getBodyAsByteArray().map(bytes -> new String(bytes, charset)); - } - - @Override - public void close() { - connection.disconnect(); - } - - private InputStream getResponseStream() throws IOException { - try { - return connection.getInputStream(); - } catch (IOException e) { - return connection.getErrorStream(); - } - } - -} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/ProxyConfig.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/ProxyConfig.java deleted file mode 100644 index 171087bac6e9..000000000000 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/customtokenproxy/ProxyConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.identity.implementation.customtokenproxy; - -import java.net.URL; - -public class ProxyConfig { - private final URL tokenProxyUrl; - private final String sniName; - private final String caFile; - private final byte[] caData; - - public ProxyConfig(URL tokenProxyUrl, String sniName, String caFile, byte[] caData) { - this.tokenProxyUrl = tokenProxyUrl; - this.sniName = sniName; - this.caFile = caFile; - this.caData = caData; - } - - public URL getTokenProxyUrl() { - return tokenProxyUrl; - } - - public String getSniName() { - return sniName; - } - - public String getCaFile() { - return caFile; - } - - public byte[] getCaData() { - return caData; - } -} diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java deleted file mode 100644 index b5c4ac25672b..000000000000 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java +++ /dev/null @@ -1,637 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -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.util.TestUtils; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.ssl.SslProvider; -import io.netty.util.CharsetUtil; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import reactor.test.StepVerifier; - -import javax.net.ssl.SSLEngine; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.time.OffsetDateTime; -import java.util.Base64; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.Collection; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * E2E test for WorkloadIdentityCredential with AKS Custom Token Proxy using Netty mock server. - * The certificate is loaded from pre-generated certificate from resources, which was generated as follows- - * keytool -genkeypair -alias test-cert -keyalg RSA -keysize 2048 -validity 3650 -keystore src/test/resources/test-aks-cert.p12 -storetype PKCS12 - * -storepass password -keypass password -dname "CN=AKS-Proxy-Test" -ext "SAN=DNS:test-aks-proxy.ests.aks" - * - */ -public class WorkloadIdentityCredentialIdentityBindingTest { - - private static final String AKS_SNI_NAME = "test-aks-proxy.ests.aks"; - - private static final String TEST_CLIENT_ID = "test-client-id"; - private static final String TEST_TENANT_ID = "test-tenant-id"; - private static final String MOCK_ACCESS_TOKEN = "mock_access_token_from_aks_proxy"; - private static final String MISMATCHED_CERT_SAN = "wrong-hostname.example.com"; - - @TempDir - Path tempDir; - - private NioEventLoopGroup bossGroup; - private NioEventLoopGroup workerGroup; - private Channel serverChannel; - private String serverBaseUrl; - private Path tokenFilePath; - private Path caCertFilePath; - private String caCertPemData; - private AtomicInteger tokenRequestCount; - private AtomicInteger metadataRequestCount; - private X509Certificate certificate; - - @BeforeEach - public void setUp() throws Exception { - tokenRequestCount = new AtomicInteger(0); - metadataRequestCount = new AtomicInteger(0); - - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - try (InputStream keyStoreStream = getClass().getResourceAsStream("/test-aks-cert.p12")) { - if (keyStoreStream == null) { - throw new IllegalStateException("Test certificate not found in resources: /test-aks-cert.p12"); - } - keyStore.load(keyStoreStream, "password".toCharArray()); - } - - PrivateKey privateKey = (PrivateKey) keyStore.getKey("test-cert", "password".toCharArray()); - certificate = (X509Certificate) keyStore.getCertificate("test-cert"); - - caCertPemData = toPemFormat(certificate); - - caCertFilePath = tempDir.resolve("ca-cert.pem"); - Files.write(caCertFilePath, caCertPemData.getBytes(StandardCharsets.UTF_8)); - - tokenFilePath = tempDir.resolve("token.jwt"); - String mockJwt = createMockFederatedToken(); - Files.write(tokenFilePath, mockJwt.getBytes(StandardCharsets.UTF_8)); - - startNettyHttpsServer(privateKey, certificate); - } - - @AfterEach - public void cleanup() { - if (serverChannel != null) { - serverChannel.close().syncUninterruptibly(); - } - if (bossGroup != null) { - bossGroup.shutdownGracefully(); - } - if (workerGroup != null) { - workerGroup.shutdownGracefully(); - } - } - - @Test - public void testAksProxyWithCaFile() throws CertificateParsingException { - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - List dnsNames = fetchCertificateSAN(certificate); - - assertTrue(dnsNames.contains("test-aks-proxy.ests.aks"), - "Certificate should contain the SNI name 'test-aks-proxy.ests.aks'"); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - AccessToken token = credential.getTokenSync(request); - - assertNotNull(token, "Access token should not be null"); - assertEquals(MOCK_ACCESS_TOKEN, token.getToken(), "Token value should match mock response"); - assertTrue(token.getExpiresAt().isAfter(OffsetDateTime.now()), "Token should not be expired"); - assertEquals(1, tokenRequestCount.get(), "Server should have received exactly one token request"); - } - - @Test - public void testAksProxyWithCaFileAsync() { - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - StepVerifier.create(credential.getToken(request)) - .expectNextMatches(token -> MOCK_ACCESS_TOKEN.equals(token.getToken()) - && token.getExpiresAt().isAfter(OffsetDateTime.now())) - .verifyComplete(); - - assertEquals(1, tokenRequestCount.get(), "Server should have received exactly one token request"); - } - - @Test - public void testAksProxyWithCaData() { - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_DATA", caCertPemData) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - AccessToken token = credential.getTokenSync(request); - - assertNotNull(token, "Access token should not be null"); - assertEquals(MOCK_ACCESS_TOKEN, token.getToken(), "Token value should match mock response"); - assertEquals(1, tokenRequestCount.get(), "Server should have received exactly one token request"); - } - - @Test - public void testAksProxyWithInvalidTokenFile() { - Path nonExistentTokenFile = tempDir.resolve("non-existent-token.jwt"); - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", nonExistentTokenFile.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(nonExistentTokenFile.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - Exception exception = assertThrows(Exception.class, () -> credential.getTokenSync(request)); - - assertNotNull(exception, "Should throw exception when token file doesn't exist"); - assertEquals(0, tokenRequestCount.get(), "No token request should reach the server with invalid token file"); - } - - @Test - public void testAksProxyWithInvalidCaCertificate() throws Exception { - String invalidCertData = "-----BEGIN CERTIFICATE-----\nINVALID_BASE64_DATA\n-----END CERTIFICATE-----"; - Path invalidCaCertFile = tempDir.resolve("invalid-ca.pem"); - Files.write(invalidCaCertFile, invalidCertData.getBytes(StandardCharsets.UTF_8)); - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", invalidCaCertFile.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - Exception exception = assertThrows(Exception.class, () -> { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - credential.getTokenSync(request); - }); - - assertNotNull(exception, "Should throw exception when CA certificate is invalid"); - assertEquals(0, tokenRequestCount.get(), "No token request should succeed with invalid CA certificate"); - } - - @Test - public void testAksProxyWithHttpScheme() { - String httpProxyUrl = serverBaseUrl.replace("https://", "http://"); - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", httpProxyUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - Exception exception = assertThrows(Exception.class, () -> { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(httpProxyUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - credential.getTokenSync(request); - }); - - assertNotNull(exception, "Should throw exception when proxy URL uses HTTP instead of HTTPS"); - assertEquals(0, tokenRequestCount.get(), "No token request should be made with HTTP proxy URL"); - } - - @Test - public void testAksProxyWithMalformedUrl() { - String malformedUrl = "not-a-valid-url-at-all"; - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", malformedUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - Exception exception = assertThrows(Exception.class, () -> { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(malformedUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - credential.getTokenSync(request); - }); - - assertNotNull(exception, "Should throw exception when proxy URL is malformed"); - assertEquals(0, tokenRequestCount.get(), "No token request should be made with malformed proxy URL"); - } - - @Test - public void testAksProxyUnreachable() { - String unreachableProxyUrl = "https://localhost:19999"; - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", unreachableProxyUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(unreachableProxyUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - Exception exception = assertThrows(Exception.class, () -> credential.getTokenSync(request)); - - assertNotNull(exception, "Should throw exception when proxy server is unreachable"); - assertEquals(0, tokenRequestCount.get(), "No token request should succeed when proxy is unreachable"); - } - - @Test - public void testAksProxyWithEmptyTokenFile() throws Exception { - Path emptyTokenFile = tempDir.resolve("empty-token.jwt"); - Files.write(emptyTokenFile, new byte[0]); - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", emptyTokenFile.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(emptyTokenFile.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - Exception exception = assertThrows(Exception.class, () -> credential.getTokenSync(request)); - - assertNotNull(exception, "Should throw exception when token file is empty"); - assertEquals(0, tokenRequestCount.get(), "No token request should be made with empty token file"); - } - - @Test - public void testAksProxyWithUrlEncodedCharactersInPath() throws Exception { - String encodedPath = "/api%2Fv1/token"; - String proxyUrlWithEncoding = serverBaseUrl + encodedPath; - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", proxyUrlWithEncoding) - .put("AZURE_KUBERNETES_CA_FILE", caCertFilePath.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(proxyUrlWithEncoding) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - AccessToken token = credential.getTokenSync(request); - - assertNotNull(token, "Token should not be null"); - assertEquals(MOCK_ACCESS_TOKEN, token.getToken(), "Token value should match mock token"); - assertTrue(token.getExpiresAt().isAfter(OffsetDateTime.now()), "Token should not be expired"); - assertEquals(1, tokenRequestCount.get(), "Token request should be made once"); - } - - @Test - public void testAksProxyWithCaFileButNoSni() throws Exception { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - try (InputStream keyStoreStream = getClass().getResourceAsStream("/test-aks-cert-no-sni.p12")) { - assertNotNull(keyStoreStream, "Mismatched certificate not found: /test-aks-cert-no-sni.p12"); - keyStore.load(keyStoreStream, "password".toCharArray()); - } - - PrivateKey privateKey = (PrivateKey) keyStore.getKey("test-cert", "password".toCharArray()); - X509Certificate certificate = (X509Certificate) keyStore.getCertificate("test-cert"); - - String noSniCertPemData = toPemFormat(certificate); - Path noSniCaCertFile = tempDir.resolve("ca-cert-no-sni.pem"); - Files.write(noSniCaCertFile, noSniCertPemData.getBytes(StandardCharsets.UTF_8)); - - startNettyHttpsServer(privateKey, certificate); - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", noSniCaCertFile.toString()) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - AccessToken token = credential.getTokenSync(request); - - assertNotNull(token, "Access token should not be null"); - assertEquals(MOCK_ACCESS_TOKEN, token.getToken(), "Token value should match mock response"); - assertEquals(1, tokenRequestCount.get(), "Server should have received exactly one token request"); - } - - @Test - public void testAksProxyWithMismatchedSniAndCertificate() throws Exception { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - try (InputStream keyStoreStream = getClass().getResourceAsStream("/test-aks-cert-mismatch.p12")) { - assertNotNull(keyStoreStream, "Mismatched certificate not found: /test-aks-cert-mismatch.p12"); - keyStore.load(keyStoreStream, "password".toCharArray()); - } - - PrivateKey privateKey = (PrivateKey) keyStore.getKey("test-cert", "password".toCharArray()); - X509Certificate certificate = (X509Certificate) keyStore.getCertificate("test-cert"); - - List dnsNames = fetchCertificateSAN(certificate); - - assertTrue(dnsNames.contains(MISMATCHED_CERT_SAN), - "Certificate should contain DNS name 'wrong-hostname.example.com'"); - assertFalse(dnsNames.contains(AKS_SNI_NAME), - "Certificate should NOT contain the SNI name 'test-aks-proxy.ests.aks'"); - - String mismatchCertPemData = toPemFormat(certificate); - Path mismatchCaCertFile = tempDir.resolve("ca-cert-mismatch.pem"); - Files.write(mismatchCaCertFile, mismatchCertPemData.getBytes(StandardCharsets.UTF_8)); - - startNettyHttpsServer(privateKey, certificate); - - Configuration configuration = TestUtils - .createTestConfiguration(new TestConfigurationSource().put("AZURE_KUBERNETES_TOKEN_PROXY", serverBaseUrl) - .put("AZURE_KUBERNETES_CA_FILE", mismatchCaCertFile.toString()) - .put("AZURE_KUBERNETES_SNI_NAME", AKS_SNI_NAME) - .put(Configuration.PROPERTY_AZURE_CLIENT_ID, TEST_CLIENT_ID) - .put(Configuration.PROPERTY_AZURE_TENANT_ID, TEST_TENANT_ID) - .put("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath.toString())); - - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId(TEST_TENANT_ID) - .clientId(TEST_CLIENT_ID) - .tokenFilePath(tokenFilePath.toString()) - .configuration(configuration) - .authorityHost(serverBaseUrl) - .enableAzureTokenProxy() - .disableInstanceDiscovery() - .build(); - - TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - - Exception exception = assertThrows(Exception.class, () -> credential.getTokenSync(request)); - - assertNotNull(exception, "Should throw exception when SNI doesn't match certificate SAN"); - - String exceptionMessage = exception.toString().toLowerCase(); - assertTrue(exceptionMessage.contains("failed to create connection"), - "Exception should be connection failure related, got: " + exception.getMessage()); - - assertEquals(0, tokenRequestCount.get(), "No token request should succeed when SNI doesn't match certificate"); - } - - private List fetchCertificateSAN(X509Certificate certificate) throws CertificateParsingException { - Collection> sanCollection = certificate.getSubjectAlternativeNames(); - - List dnsNames = new ArrayList<>(); - for (List san : sanCollection) { - if (san.size() >= 2 && san.get(0).equals(2)) { - dnsNames.add((String) san.get(1)); - } - } - return dnsNames; - } - - private void startNettyHttpsServer(PrivateKey privateKey, X509Certificate certificate) throws Exception { - SslContext sslContext - = SslContextBuilder.forServer(privateKey, certificate).sslProvider(SslProvider.OPENSSL).build(); - - bossGroup = new NioEventLoopGroup(1); - workerGroup = new NioEventLoopGroup(); - - ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .childHandler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); - - SslHandler sslHandler = sslContext.newHandler(ch.alloc()); - - sslHandler.handshakeFuture().addListener(future -> { - if (future.isSuccess()) { - SSLEngine engine = sslHandler.engine(); - } - }); - - pipeline.addLast(sslHandler); - pipeline.addLast(new HttpServerCodec()); - pipeline.addLast(new HttpObjectAggregator(65536)); - pipeline.addLast(new HttpRequestHandler()); - } - }); - - serverChannel = bootstrap.bind("localhost", 0).sync().channel(); - InetSocketAddress socketAddress = (InetSocketAddress) serverChannel.localAddress(); - int port = socketAddress.getPort(); - serverBaseUrl = "https://localhost:" + port; - } - - private class HttpRequestHandler extends SimpleChannelInboundHandler { - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { - String uri = request.uri(); - String responseBody; - - if (uri.contains("/.well-known/openid-configuration")) { - metadataRequestCount.incrementAndGet(); - String base = serverBaseUrl + "/" + TEST_TENANT_ID; - responseBody = String - .format("{%n" + " \"token_endpoint\": \"%s/oauth2/v2.0/token\",%n" + " \"issuer\": \"%s/v2.0\",%n" - + " \"authorization_endpoint\": \"%s/oauth2/v2.0/authorize\"%n" + "}", base, base, base); - } else { - tokenRequestCount.incrementAndGet(); - responseBody = String.format( - "{\"token_type\":\"Bearer\",\"expires_in\":3600,\"ext_expires_in\":3600,\"access_token\":\"%s\"}", - MOCK_ACCESS_TOKEN); - } - - ByteBuf content = Unpooled.copiedBuffer(responseBody, CharsetUtil.UTF_8); - FullHttpResponse response - = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); - - ctx.writeAndFlush(response); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } - } - - private String toPemFormat(X509Certificate certificate) throws Exception { - Base64.Encoder encoder = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)); - byte[] encoded = encoder.encode(certificate.getEncoded()); - return "-----BEGIN CERTIFICATE-----\n" + new String(encoded, StandardCharsets.UTF_8) - + "\n-----END CERTIFICATE-----\n"; - } - - private String createMockFederatedToken() { - String header = Base64.getUrlEncoder() - .withoutPadding() - .encodeToString("{\"alg\":\"RS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8)); - - long exp = System.currentTimeMillis() / 1000 + 3600; - String payload = Base64.getUrlEncoder() - .withoutPadding() - .encodeToString(String.format( - "{\"aud\":\"api://AzureADTokenExchange\",\"exp\":%d,\"iss\":\"kubernetes.io/serviceaccount\",\"sub\":\"system:serviceaccount:default:workload-identity-sa\"}", - exp).getBytes(StandardCharsets.UTF_8)); - - String signature - = Base64.getUrlEncoder().withoutPadding().encodeToString("mock-signature".getBytes(StandardCharsets.UTF_8)); - - return header + "." + payload + "." + signature; - } -} 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 5e5d76948e8f..d2404d9b302b 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 @@ -35,23 +35,6 @@ public class WorkloadIdentityCredentialTest { private static final String CLIENT_ID = UUID.randomUUID().toString(); - private static final String ENV_PROXY_URL = "AZURE_KUBERNETES_TOKEN_PROXY"; - private static final String ENV_CA_FILE = "AZURE_KUBERNETES_CA_FILE"; - private static final String ENV_CA_DATA = "AZURE_KUBERNETES_CA_DATA"; - private static final String ENV_SNI_NAME = "AZURE_KUBERNETES_SNI_NAME"; - private static final String CA_DATA - = "-----BEGIN CERTIFICATE-----\n" + "MIICMzCCAZygAwIBAgIJALiPnVsvq8dsMA0GCSqGSIb3DQEBBQUAMFMxCzAJBgNV\n" - + "BAYTAlVTMQwwCgYDVQQIEwNmb28xDDAKBgNVBAcTA2ZvbzEMMAoGA1UEChMDZm9v\n" - + "MQwwCgYDVQQLEwNmb28xDDAKBgNVBAMTA2ZvbzAeFw0xMzAzMTkxNTQwMTlaFw0x\n" - + "ODAzMTgxNTQwMTlaMFMxCzAJBgNVBAYTAlVTMQwwCgYDVQQIEwNmb28xDDAKBgNV\n" - + "BAcTA2ZvbzEMMAoGA1UEChMDZm9vMQwwCgYDVQQLEwNmb28xDDAKBgNVBAMTA2Zv\n" - + "bzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzdGfxi9CNbMf1UUcvDQh7MYB\n" - + "OveIHyc0E0KIbhjK5FkCBU4CiZrbfHagaW7ZEcN0tt3EvpbOMxxc/ZQU2WN/s/wP\n" - + "xph0pSfsfFsTKM4RhTWD2v4fgk+xZiKd1p0+L4hTtpwnEw0uXRVd0ki6muwV5y/P\n" - + "+5FHUeldq+pgTcgzuK8CAwEAAaMPMA0wCwYDVR0PBAQDAgLkMA0GCSqGSIb3DQEB\n" - + "BQUAA4GBAJiDAAtY0mQQeuxWdzLRzXmjvdSuL9GoyT3BF/jSnpxz5/58dba8pWen\n" - + "v3pj4P3w5DoOso0rzkZy2jEsEitlVM2mLSbQpMM+MUVQCQoiG6W9xuCFuxSrwPIS\n" - + "pAqEAuV4DNoxQKKWmhVv+J0ptMWD25Pnpxeq5sXzghfJnslJlQND\n" + "-----END CERTIFICATE-----\n"; @Test public void testWorkloadIdentityFlow(@TempDir Path tempDir) throws IOException { @@ -209,340 +192,4 @@ public void testFileReadingError(@TempDir Path tempDir) { assertTrue(error.getCause() instanceof IOException); // Original IOException from Files.readAllBytes }).verify(); } - - @Test - public void testProxyEnabledWithProxyUrlGetsToken(@TempDir Path tempDir) throws IOException { - // setup - String endpoint = "https://localhost"; - String token1 = "token1"; - String proxyUrl = "https://token-proxy.example.com"; - - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, proxyUrl)); - - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - try (MockedConstruction identityClientMock - = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); - when(identityClient.authenticateWithConfidentialClient(any(TokenRequestContext.class))) - .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - })) { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - - StepVerifier.create(credential.getToken(request1)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && expiresAt.getSecond() == token.getExpiresAt().getSecond()) - .verifyComplete(); - - assertNotNull(identityClientMock); - } - } - - @Test - public void testProxyEnabledWithoutProxyUrlGetsToken(@TempDir Path tempDir) throws IOException { - // setup - String endpoint = "https://localhost"; - String token1 = "token1"; - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint)); - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - try (MockedConstruction identityClientMock - = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); - when(identityClient.authenticateWithConfidentialClient(any(TokenRequestContext.class))) - .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - })) { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - - StepVerifier.create(credential.getToken(request1)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && expiresAt.getSecond() == token.getExpiresAt().getSecond()) - .verifyComplete(); - - assertNotNull(identityClientMock); - } - } - - @Test - public void testProxyEnabledInvalidProxyUrlSchemeFailure(@TempDir Path tempDir) throws IOException { - String endpoint = "https://localhost"; - - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "http://not-https.example.com")); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - }); - } - - @Test - public void testProxyUrlWithQueryFailure(@TempDir Path tempDir) throws IOException { - String endpoint = "https://login.microsoftonline.com"; - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "https://proxy.example.com?x=y")); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - }); - } - - @Test - public void testProxyUrlWithFragmentFailure(@TempDir Path tempDir) throws IOException { - String endpoint = "https://login.microsoftonline.com"; - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "https://proxy.example.com#frag")); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - }); - } - - @Test - public void testProxyUrlWithUserInfoFailure(@TempDir Path tempDir) throws IOException { - String endpoint = "https://login.microsoftonline.com"; - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "https://user:pass@proxy.example.com")); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - }); - } - - @Test - public void testCaFileAndCaDataPresentFailure(@TempDir Path tempDir) throws IOException { - String endpoint = "https://login.microsoftonline.com"; - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - Path caFile = tempDir.resolve("ca.crt"); - Files.write(caFile, - "-----BEGIN CERTIFICATE-----\nMIIB...==\n-----END CERTIFICATE-----\n".getBytes(StandardCharsets.UTF_8)); - - String caData = "-----BEGIN CERTIFICATE-----\nMIIB...==\n-----END CERTIFICATE-----"; - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "https://proxy.example.com") - .put(ENV_CA_FILE, caFile.toString()) - .put(ENV_CA_DATA, caData)); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - }); - } - - @Test - public void testProxyEnabledWithProxyUrlGetsTokenSync(@TempDir Path tempDir) throws IOException { - // setup - String endpoint = "https://localhost"; - String token1 = "token1"; - String proxyUrl = "https://token-proxy.example.com"; - - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, proxyUrl)); - - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - try (MockedConstruction identitySyncClientMock - = mockConstruction(IdentitySyncClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithConfidentialClientCache(any())) - .thenThrow(new IllegalStateException("Test")); - when(identityClient.authenticateWithConfidentialClient(any(TokenRequestContext.class))) - .thenReturn(TestUtils.getMockAccessTokenSync(token1, expiresAt)); - })) { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - - AccessToken token = credential.getTokenSync(request1); - - assertTrue(token1.equals(token.getToken())); - assertTrue(expiresAt.getSecond() == token.getExpiresAt().getSecond()); - assertNotNull(identitySyncClientMock); - } - } - - @Test - public void testProxyUrlWithCaDataAcquiresToken(@TempDir Path tempDir) throws IOException { - String endpoint = "https://login.microsoftonline.com"; - String token1 = "token-ca-data"; - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "https://token-proxy.example.com") - .put(ENV_CA_DATA, CA_DATA)); - - try (MockedConstruction mocked - = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); - when(identityClient.authenticateWithConfidentialClient(any(TokenRequestContext.class))) - .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - })) { - WorkloadIdentityCredential cred = new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - - StepVerifier.create(cred.getToken(request1)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && token.getExpiresAt().getSecond() == expiresAt.getSecond()) - .verifyComplete(); - - assertNotNull(mocked); - } - } - - @Test - public void testProxyUrlWithCaFileGetsToken(@TempDir Path tempDir) throws IOException { - String endpoint = "https://login.microsoftonline.com"; - String token1 = "tok-ca-file"; - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - Path caFile = tempDir.resolve("ca.crt"); - - Files.write(caFile, CA_DATA.getBytes(StandardCharsets.UTF_8)); - - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, "https://token-proxy.example.com") - .put(ENV_CA_FILE, caFile.toString())); - - try (MockedConstruction mocked - = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); - when(identityClient.authenticateWithConfidentialClient(any(TokenRequestContext.class))) - .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - })) { - WorkloadIdentityCredential cred = new WorkloadIdentityCredentialBuilder().tenantId("tenant") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - - StepVerifier.create(cred.getToken(request1)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && token.getExpiresAt().getSecond() == expiresAt.getSecond()) - .verifyComplete(); - - assertNotNull(mocked); - } - } - - @Test - public void testProxyEnabledWithSniNameGetsToken(@TempDir Path tempDir) throws IOException { - // setup - String endpoint = "https://localhost"; - String token1 = "token1"; - String proxyUrl = "https://token-proxy.example.com"; - String sniName = "615f3b8ad7eb011a09ed3b762e404de43ebc7ade0802a34c9fd322b688c3a655.ests.aks"; - - TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default"); - OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint) - .put(ENV_PROXY_URL, proxyUrl) - .put(ENV_SNI_NAME, sniName)); - - Path tokenFile = tempDir.resolve("token.txt"); - Files.write(tokenFile, "dummy-token".getBytes(StandardCharsets.UTF_8)); - - try (MockedConstruction identityClientMock - = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); - when(identityClient.authenticateWithConfidentialClient(any(TokenRequestContext.class))) - .thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); - })) { - WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid") - .clientId(CLIENT_ID) - .tokenFilePath(tokenFile.toString()) - .configuration(configuration) - .enableAzureTokenProxy() - .build(); - - StepVerifier.create(credential.getToken(request1)) - .expectNextMatches(token -> token1.equals(token.getToken()) - && expiresAt.getSecond() == token.getExpiresAt().getSecond()) - .verifyComplete(); - - assertNotNull(identityClientMock); - } - } - } diff --git a/sdk/identity/azure-identity/src/test/resources/test-aks-cert-mismatch.p12 b/sdk/identity/azure-identity/src/test/resources/test-aks-cert-mismatch.p12 deleted file mode 100644 index 6c7fc720babb..000000000000 Binary files a/sdk/identity/azure-identity/src/test/resources/test-aks-cert-mismatch.p12 and /dev/null differ diff --git a/sdk/identity/azure-identity/src/test/resources/test-aks-cert-no-sni.p12 b/sdk/identity/azure-identity/src/test/resources/test-aks-cert-no-sni.p12 deleted file mode 100644 index 1beb531f69fc..000000000000 Binary files a/sdk/identity/azure-identity/src/test/resources/test-aks-cert-no-sni.p12 and /dev/null differ diff --git a/sdk/identity/azure-identity/src/test/resources/test-aks-cert.p12 b/sdk/identity/azure-identity/src/test/resources/test-aks-cert.p12 deleted file mode 100644 index 5191a8ac662a..000000000000 Binary files a/sdk/identity/azure-identity/src/test/resources/test-aks-cert.p12 and /dev/null differ diff --git a/sdk/identity/live-test-apps/identity-test-container/pom.xml b/sdk/identity/live-test-apps/identity-test-container/pom.xml index 97313c8c350c..21c706f9ae31 100644 --- a/sdk/identity/live-test-apps/identity-test-container/pom.xml +++ b/sdk/identity/live-test-apps/identity-test-container/pom.xml @@ -18,7 +18,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 diff --git a/sdk/identity/live-test-apps/identity-test-function/pom.xml b/sdk/identity/live-test-apps/identity-test-function/pom.xml index af64e98ce2f0..b33947531f6f 100644 --- a/sdk/identity/live-test-apps/identity-test-function/pom.xml +++ b/sdk/identity/live-test-apps/identity-test-function/pom.xml @@ -24,7 +24,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 com.azure diff --git a/sdk/identity/live-test-apps/identity-test-vm/pom.xml b/sdk/identity/live-test-apps/identity-test-vm/pom.xml index b8e29689d724..08c2b82db7ad 100644 --- a/sdk/identity/live-test-apps/identity-test-vm/pom.xml +++ b/sdk/identity/live-test-apps/identity-test-vm/pom.xml @@ -18,7 +18,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 diff --git a/sdk/identity/live-test-apps/identity-test-webapp/pom.xml b/sdk/identity/live-test-apps/identity-test-webapp/pom.xml index 19656db302b7..c23be3382417 100644 --- a/sdk/identity/live-test-apps/identity-test-webapp/pom.xml +++ b/sdk/identity/live-test-apps/identity-test-webapp/pom.xml @@ -29,7 +29,7 @@ com.azure azure-identity - 1.19.0-beta.2 + 1.18.2 com.azure