Skip to content

Commit 4ee6b22

Browse files
authored
Enable Custom Proxy in WorkloadIdentityCredential (#47041)
* changes to add proxy in wic * integration with wicbuilder and wic * review comments * added wic testcases and some review comments * unit test * live testing based updates * spotless, spotbugs, checkstyle and local server test * addressed review comments * updated unit tests * updated test to not use bouncycastle * spotless apply * TC review comment * move validation to builder * removed an accidental logging statement
1 parent 9b36340 commit 4ee6b22

File tree

12 files changed

+1695
-0
lines changed

12 files changed

+1695
-0
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 1.19.0-beta.1 (Unreleased)
44

55
### Features Added
6+
- 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`).
67

78
### Breaking Changes
89

sdk/identity/azure-identity/src/main/java/com/azure/identity/WorkloadIdentityCredentialBuilder.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import com.azure.core.util.Configuration;
77
import com.azure.core.util.CoreUtils;
88
import com.azure.core.util.logging.ClientLogger;
9+
import com.azure.identity.implementation.customtokenproxy.CustomTokenProxyConfiguration;
10+
import com.azure.identity.implementation.customtokenproxy.CustomTokenProxyHttpClient;
11+
import com.azure.identity.implementation.customtokenproxy.ProxyConfig;
912
import com.azure.identity.implementation.util.ValidationUtil;
1013

1114
import static com.azure.identity.ManagedIdentityCredential.AZURE_FEDERATED_TOKEN_FILE;
@@ -47,6 +50,7 @@
4750
public class WorkloadIdentityCredentialBuilder extends AadCredentialBuilderBase<WorkloadIdentityCredentialBuilder> {
4851
private static final ClientLogger LOGGER = new ClientLogger(WorkloadIdentityCredentialBuilder.class);
4952
private String tokenFilePath;
53+
private boolean enableTokenProxy;
5054

5155
/**
5256
* Creates an instance of a WorkloadIdentityCredentialBuilder.
@@ -66,6 +70,19 @@ public WorkloadIdentityCredentialBuilder tokenFilePath(String tokenFilePath) {
6670
return this;
6771
}
6872

73+
/**
74+
* Enables the custom token proxy feature for clusters running in Azure.
75+
* When enabled, the credential will attempt to use a custom token proxy configured through
76+
* environment variables (AZURE_KUBERNETES_TOKEN_PROXY, AZURE_KUBERNETES_CA_FILE,
77+
* AZURE_KUBERNETES_CA_DATA, AZURE_KUBERNETES_SNI_NAME).
78+
*
79+
* @return An updated instance of this builder with Azure token proxy enabled.
80+
*/
81+
public WorkloadIdentityCredentialBuilder enableAzureTokenProxy() {
82+
this.enableTokenProxy = true;
83+
return this;
84+
}
85+
6986
/**
7087
* Creates new {@link WorkloadIdentityCredential} with the configured options set.
7188
*
@@ -88,6 +105,13 @@ public WorkloadIdentityCredential build() {
88105
ValidationUtil.validate(this.getClass().getSimpleName(), LOGGER, "Client ID", clientIdInput, "Tenant ID",
89106
tenantIdInput, "Service Token File Path", federatedTokenFilePathInput);
90107

108+
if (enableTokenProxy) {
109+
ProxyConfig proxyConfig = CustomTokenProxyConfiguration.parseAndValidate(configuration);
110+
if (proxyConfig != null) {
111+
identityClientOptions.setHttpClient(new CustomTokenProxyHttpClient(proxyConfig));
112+
}
113+
}
114+
91115
return new WorkloadIdentityCredential(tenantIdInput, clientIdInput, federatedTokenFilePathInput,
92116
identityClientOptions.clone());
93117
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.identity.implementation;
5+
6+
import com.azure.core.util.CoreUtils;
7+
import javax.net.ssl.SNIServerName;
8+
import javax.net.ssl.SSLParameters;
9+
import javax.net.ssl.SSLSocket;
10+
import javax.net.ssl.SSLSocketFactory;
11+
import java.io.IOException;
12+
import java.net.Socket;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.Collections;
15+
import java.net.InetAddress;
16+
17+
public final class SniSslSocketFactory extends SSLSocketFactory {
18+
private final SSLSocketFactory sslSocketFactory;
19+
private final String sniName;
20+
21+
public SniSslSocketFactory(SSLSocketFactory sslSocketFactory, String sniName) {
22+
this.sslSocketFactory = sslSocketFactory;
23+
this.sniName = sniName;
24+
}
25+
26+
@Override
27+
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
28+
Socket sslSocket = (SSLSocket) sslSocketFactory.createSocket(s, host, port, autoClose);
29+
configureSni(sslSocket);
30+
return sslSocket;
31+
}
32+
33+
@Override
34+
public Socket createSocket(String host, int port) throws IOException {
35+
Socket sslSocket = sslSocketFactory.createSocket(host, port);
36+
configureSni(sslSocket);
37+
return sslSocket;
38+
}
39+
40+
@Override
41+
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException {
42+
Socket sslSocket = sslSocketFactory.createSocket(host, port, localAddress, localPort);
43+
configureSni(sslSocket);
44+
return sslSocket;
45+
}
46+
47+
@Override
48+
public Socket createSocket(InetAddress host, int port) throws IOException {
49+
Socket sslSocket = sslSocketFactory.createSocket(host, port);
50+
configureSni(sslSocket);
51+
return sslSocket;
52+
}
53+
54+
@Override
55+
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
56+
throws IOException {
57+
Socket sslSocket = sslSocketFactory.createSocket(address, port, localAddress, localPort);
58+
configureSni(sslSocket);
59+
return sslSocket;
60+
}
61+
62+
@Override
63+
public String[] getDefaultCipherSuites() {
64+
return sslSocketFactory.getDefaultCipherSuites();
65+
}
66+
67+
@Override
68+
public String[] getSupportedCipherSuites() {
69+
return sslSocketFactory.getSupportedCipherSuites();
70+
}
71+
72+
private void configureSni(Socket socket) {
73+
if (socket instanceof SSLSocket && !CoreUtils.isNullOrEmpty(sniName)) {
74+
SSLSocket sslSocket = (SSLSocket) socket;
75+
SSLParameters sslParameters = sslSocket.getSSLParameters();
76+
sslParameters.setServerNames(Collections.singletonList(new RawSniServerName(sniName)));
77+
sslSocket.setSSLParameters(sslParameters);
78+
}
79+
}
80+
81+
private static final class RawSniServerName extends SNIServerName {
82+
RawSniServerName(String sniHost) {
83+
super(0, sniHost.getBytes(StandardCharsets.UTF_8));
84+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.identity.implementation.customtokenproxy;
5+
6+
import com.azure.core.util.logging.ClientLogger;
7+
8+
import java.net.URI;
9+
import java.net.URL;
10+
import java.nio.charset.StandardCharsets;
11+
import java.net.URISyntaxException;
12+
13+
import com.azure.core.util.Configuration;
14+
import com.azure.core.util.CoreUtils;
15+
16+
public final class CustomTokenProxyConfiguration {
17+
18+
private static final ClientLogger LOGGER = new ClientLogger(CustomTokenProxyConfiguration.class);
19+
20+
private static final String AZURE_KUBERNETES_TOKEN_PROXY = "AZURE_KUBERNETES_TOKEN_PROXY";
21+
private static final String AZURE_KUBERNETES_CA_FILE = "AZURE_KUBERNETES_CA_FILE";
22+
private static final String AZURE_KUBERNETES_CA_DATA = "AZURE_KUBERNETES_CA_DATA";
23+
private static final String AZURE_KUBERNETES_SNI_NAME = "AZURE_KUBERNETES_SNI_NAME";
24+
25+
private CustomTokenProxyConfiguration() {
26+
}
27+
28+
public static ProxyConfig parseAndValidate(Configuration configuration) {
29+
String tokenProxyUrl = configuration.get(AZURE_KUBERNETES_TOKEN_PROXY);
30+
String caFile = configuration.get(AZURE_KUBERNETES_CA_FILE);
31+
String caData = configuration.get(AZURE_KUBERNETES_CA_DATA);
32+
String sniName = configuration.get(AZURE_KUBERNETES_SNI_NAME);
33+
34+
if (CoreUtils.isNullOrEmpty(tokenProxyUrl)) {
35+
if (!CoreUtils.isNullOrEmpty(sniName)
36+
|| !CoreUtils.isNullOrEmpty(caFile)
37+
|| !CoreUtils.isNullOrEmpty(caData)) {
38+
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
39+
"AZURE_KUBERNETES_TOKEN_PROXY is not set but other custom endpoint-related environment variables are present"));
40+
}
41+
return null;
42+
}
43+
44+
if (!CoreUtils.isNullOrEmpty(caFile) && !CoreUtils.isNullOrEmpty(caData)) {
45+
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
46+
"Only one of AZURE_KUBERNETES_CA_FILE or AZURE_KUBERNETES_CA_DATA can be set."));
47+
}
48+
49+
URL proxyUrl = validateProxyUrl(tokenProxyUrl);
50+
51+
byte[] caCertBytes = null;
52+
if (!CoreUtils.isNullOrEmpty(caData)) {
53+
caCertBytes = caData.getBytes(StandardCharsets.UTF_8);
54+
}
55+
56+
ProxyConfig config = new ProxyConfig(proxyUrl, sniName, caFile, caCertBytes);
57+
return config;
58+
}
59+
60+
private static URL validateProxyUrl(String endpoint) {
61+
if (CoreUtils.isNullOrEmpty(endpoint)) {
62+
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Proxy endpoint cannot be null or empty"));
63+
}
64+
65+
try {
66+
URI tokenProxy = new URI(endpoint);
67+
68+
if (!"https".equals(tokenProxy.getScheme())) {
69+
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
70+
"Custom token endpoint must use https scheme, got: " + tokenProxy.getScheme()));
71+
}
72+
73+
if (tokenProxy.getRawUserInfo() != null) {
74+
throw LOGGER.logExceptionAsError(
75+
new IllegalArgumentException("Custom token endpoint URL must not contain user info: " + endpoint));
76+
}
77+
78+
if (tokenProxy.getRawQuery() != null) {
79+
throw LOGGER.logExceptionAsError(
80+
new IllegalArgumentException("Custom token endpoint URL must not contain a query: " + endpoint));
81+
}
82+
83+
if (tokenProxy.getRawFragment() != null) {
84+
throw LOGGER.logExceptionAsError(
85+
new IllegalArgumentException("Custom token endpoint URL must not contain a fragment: " + endpoint));
86+
}
87+
88+
if (tokenProxy.getRawPath() == null || tokenProxy.getRawPath().isEmpty()) {
89+
tokenProxy = new URI(tokenProxy.getScheme(), null, tokenProxy.getHost(), tokenProxy.getPort(), "/",
90+
null, null);
91+
}
92+
93+
return tokenProxy.toURL();
94+
95+
} catch (URISyntaxException | IllegalArgumentException e) {
96+
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Failed to normalize proxy URL path", e));
97+
} catch (Exception e) {
98+
throw new RuntimeException("Unexpected error while validating proxy URL: " + endpoint, e);
99+
}
100+
}
101+
102+
}

0 commit comments

Comments
 (0)