Skip to content

Commit ef4bbed

Browse files
Merge branch 'master' into SNOW-1016470-increase-code-coverage-to-at-least-90
2 parents e4df842 + cabd6c3 commit ef4bbed

22 files changed

+740
-14
lines changed

src/main/java/net/snowflake/client/core/SFLoginInput.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class SFLoginInput {
6262
// Workload Identity Federation
6363
private String workloadIdentityProvider;
6464
private WorkloadIdentityAttestation workloadIdentityAttestation;
65+
private String workloadIdentityEntraResource;
6566

6667
// OAuth
6768
private int redirectUriPort = -1;
@@ -612,4 +613,13 @@ public void setWorkloadIdentityAttestation(
612613
public WorkloadIdentityAttestation getWorkloadIdentityAttestation() {
613614
return workloadIdentityAttestation;
614615
}
616+
617+
public String getWorkloadIdentityEntraResource() {
618+
return this.workloadIdentityEntraResource;
619+
}
620+
621+
public SFLoginInput setWorkloadIdentityEntraResource(String workloadIdentityEntraResource) {
622+
this.workloadIdentityEntraResource = workloadIdentityEntraResource;
623+
return this;
624+
}
615625
}

src/main/java/net/snowflake/client/core/SFSession.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,9 @@ public synchronized void open() throws SFException, SnowflakeSQLException {
723723
.setOauthLoginInput(oauthLoginInput)
724724
.setWorkloadIdentityProvider(
725725
(String) connectionPropertiesMap.get(SFSessionProperty.WORKLOAD_IDENTITY_PROVIDER))
726+
.setWorkloadIdentityEntraResource(
727+
(String)
728+
connectionPropertiesMap.get(SFSessionProperty.WORKLOAD_IDENTITY_ENTRA_RESOURCE))
726729
.setPrivateKeyBase64(
727730
(String) connectionPropertiesMap.get(SFSessionProperty.PRIVATE_KEY_BASE64))
728731
.setPrivateKeyPwd(

src/main/java/net/snowflake/client/core/SFSessionProperty.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public enum SFSessionProperty {
2727
OAUTH_AUTHORIZATION_URL("oauthAuthorizationUrl", false, String.class),
2828
OAUTH_TOKEN_REQUEST_URL("oauthTokenRequestUrl", false, String.class),
2929
WORKLOAD_IDENTITY_PROVIDER("workloadIdentityProvider", false, String.class),
30+
WORKLOAD_IDENTITY_ENTRA_RESOURCE("workloadIdentityEntraResource", false, String.class),
3031
WAREHOUSE("warehouse", false, String.class),
3132
LOGIN_TIMEOUT("loginTimeout", false, Integer.class),
3233
NETWORK_TIMEOUT("networkTimeout", false, Integer.class),

src/main/java/net/snowflake/client/core/SessionUtil.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import net.snowflake.client.core.auth.oauth.TokenResponseDTO;
3535
import net.snowflake.client.core.auth.wif.AwsAttestationService;
3636
import net.snowflake.client.core.auth.wif.AwsIdentityAttestationCreator;
37+
import net.snowflake.client.core.auth.wif.AzureAttestationService;
3738
import net.snowflake.client.core.auth.wif.AzureIdentityAttestationCreator;
3839
import net.snowflake.client.core.auth.wif.GcpIdentityAttestationCreator;
3940
import net.snowflake.client.core.auth.wif.OidcIdentityAttestationCreator;
@@ -378,7 +379,7 @@ private static WorkloadIdentityAttestation getWorkloadIdentityAttestation(SFLogi
378379
new WorkloadIdentityAttestationProvider(
379380
new AwsIdentityAttestationCreator(new AwsAttestationService()),
380381
new GcpIdentityAttestationCreator(loginInput),
381-
new AzureIdentityAttestationCreator(),
382+
new AzureIdentityAttestationCreator(new AzureAttestationService(), loginInput),
382383
new OidcIdentityAttestationCreator(loginInput.getToken()));
383384
return attestationProvider.getAttestation(loginInput.getWorkloadIdentityProvider());
384385
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package net.snowflake.client.core.auth.wif;
2+
3+
import net.snowflake.client.core.SFLoginInput;
4+
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
5+
import net.snowflake.client.jdbc.SnowflakeUtil;
6+
import net.snowflake.client.log.SFLogger;
7+
import net.snowflake.client.log.SFLoggerFactory;
8+
import org.apache.http.client.methods.HttpRequestBase;
9+
10+
@SnowflakeJdbcInternalApi
11+
public class AzureAttestationService {
12+
13+
private static final SFLogger logger = SFLoggerFactory.getLogger(AzureAttestationService.class);
14+
15+
// Expected to be set in Azure Functions environment
16+
String getIdentityEndpoint() {
17+
return SnowflakeUtil.systemGetEnv("IDENTITY_ENDPOINT");
18+
}
19+
20+
// Expected to be set in Azure Functions environment
21+
String getIdentityHeader() {
22+
return SnowflakeUtil.systemGetEnv("IDENTITY_HEADER");
23+
}
24+
25+
// Expected to be set in Azure Functions environment
26+
String getClientId() {
27+
return SnowflakeUtil.systemGetEnv("MANAGED_IDENTITY_CLIENT_ID");
28+
}
29+
30+
String fetchTokenFromMetadataService(HttpRequestBase tokenRequest, SFLoginInput loginInput) {
31+
try {
32+
return WorkloadIdentityUtil.performIdentityRequest(tokenRequest, loginInput);
33+
} catch (Exception e) {
34+
logger.debug("Azure metadata server request was not successful: {}", e);
35+
return null;
36+
}
37+
}
38+
}
Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,127 @@
11
package net.snowflake.client.core.auth.wif;
22

3+
import static net.snowflake.client.core.auth.wif.WorkloadIdentityUtil.DEFAULT_METADATA_SERVICE_BASE_URL;
4+
import static net.snowflake.client.core.auth.wif.WorkloadIdentityUtil.SubjectAndIssuer;
5+
import static net.snowflake.client.core.auth.wif.WorkloadIdentityUtil.extractClaimsWithoutVerifyingSignature;
6+
7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.google.common.base.Strings;
10+
import net.snowflake.client.core.SFLoginInput;
311
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
412
import net.snowflake.client.log.SFLogger;
513
import net.snowflake.client.log.SFLoggerFactory;
14+
import org.apache.http.client.methods.HttpGet;
615

716
@SnowflakeJdbcInternalApi
817
public class AzureIdentityAttestationCreator implements WorkloadIdentityAttestationCreator {
918

1019
private static final SFLogger logger =
1120
SFLoggerFactory.getLogger(AzureIdentityAttestationCreator.class);
21+
public static final ObjectMapper objectMapper = new ObjectMapper();
22+
23+
private static final String EXPECTED_AZURE_TOKEN_ISSUER_PREFIX = "https://sts.windows.net/";
24+
private static final String DEFAULT_WORKLOAD_IDENTITY_ENTRA_RESOURCE =
25+
"api://fd3f753b-eed3-462c-b6a7-a4b5bb650aad";
26+
27+
private final AzureAttestationService azureAttestationService;
28+
private final SFLoginInput loginInput;
29+
private final String workloadIdentityEntraResource;
30+
private final String azureMetadataServiceBaseUrl;
31+
32+
public AzureIdentityAttestationCreator(
33+
AzureAttestationService azureAttestationService, SFLoginInput loginInput) {
34+
this.azureAttestationService = azureAttestationService;
35+
this.azureMetadataServiceBaseUrl = DEFAULT_METADATA_SERVICE_BASE_URL;
36+
this.loginInput = loginInput;
37+
this.workloadIdentityEntraResource = getEntraResource(loginInput);
38+
}
39+
40+
/** Only for testing purpose */
41+
public AzureIdentityAttestationCreator(
42+
AzureAttestationService azureAttestationService,
43+
SFLoginInput loginInput,
44+
String azureMetadataServiceBaseUrl) {
45+
this.azureAttestationService = azureAttestationService;
46+
this.azureMetadataServiceBaseUrl = azureMetadataServiceBaseUrl;
47+
this.loginInput = loginInput;
48+
this.workloadIdentityEntraResource = getEntraResource(loginInput);
49+
}
1250

1351
@Override
1452
public WorkloadIdentityAttestation createAttestation() {
15-
throw new RuntimeException("Azure Workload Identity not supported");
53+
logger.debug("Creating Azure identity attestation...");
54+
String identityEndpoint = azureAttestationService.getIdentityEndpoint();
55+
HttpGet request;
56+
if (Strings.isNullOrEmpty(identityEndpoint)) {
57+
request = createAzureVMIdentityRequest();
58+
} else {
59+
String identityHeader = azureAttestationService.getIdentityHeader();
60+
if (Strings.isNullOrEmpty(identityHeader)) {
61+
logger.warn("Managed identity is not enabled on this Azure function.");
62+
return null;
63+
}
64+
request =
65+
createAzureFunctionsIdentityRequest(
66+
identityEndpoint, identityHeader, azureAttestationService.getClientId());
67+
}
68+
String tokenJson = azureAttestationService.fetchTokenFromMetadataService(request, loginInput);
69+
if (tokenJson == null) {
70+
logger.debug("Could not fetch Azure token.");
71+
return null;
72+
}
73+
String token = extractTokenFromJson(tokenJson);
74+
if (token == null) {
75+
logger.error("No access token found in Azure response.");
76+
return null;
77+
}
78+
SubjectAndIssuer claims = extractClaimsWithoutVerifyingSignature(token);
79+
if (claims == null) {
80+
logger.error("Could not extract claims from token");
81+
return null;
82+
}
83+
if (!claims.getIssuer().startsWith(EXPECTED_AZURE_TOKEN_ISSUER_PREFIX)) {
84+
logger.error("Unexpected Azure token issuer: {}", claims.getIssuer());
85+
return null;
86+
}
87+
return new WorkloadIdentityAttestation(
88+
WorkloadIdentityProviderType.AZURE, token, claims.toMap());
89+
}
90+
91+
private String getEntraResource(SFLoginInput loginInput) {
92+
if (!Strings.isNullOrEmpty(loginInput.getWorkloadIdentityEntraResource())) {
93+
return loginInput.getWorkloadIdentityEntraResource();
94+
} else {
95+
return DEFAULT_WORKLOAD_IDENTITY_ENTRA_RESOURCE;
96+
}
97+
}
98+
99+
private String extractTokenFromJson(String tokenJson) {
100+
try {
101+
JsonNode jsonNode = objectMapper.readTree(tokenJson);
102+
return jsonNode.get("access_token").asText();
103+
} catch (Exception e) {
104+
logger.error("Unable to extract token from Azure metadata response: {}", e.getMessage());
105+
return null;
106+
}
107+
}
108+
109+
private HttpGet createAzureFunctionsIdentityRequest(
110+
String identityEndpoint, String identityHeader, String managedIdentityClientId) {
111+
String queryParams = "api-version=2019-08-01&resource=" + workloadIdentityEntraResource;
112+
if (managedIdentityClientId != null) {
113+
queryParams += "&client_id=" + managedIdentityClientId;
114+
}
115+
HttpGet request = new HttpGet(String.format("%s?%s", identityEndpoint, queryParams));
116+
request.addHeader("X-IDENTITY-HEADER", identityHeader);
117+
return request;
118+
}
119+
120+
private HttpGet createAzureVMIdentityRequest() {
121+
String urlWithoutQueryString = azureMetadataServiceBaseUrl + "/metadata/identity/oauth2/token?";
122+
String queryParams = "api-version=2018-02-01&resource=" + workloadIdentityEntraResource;
123+
HttpGet request = new HttpGet(urlWithoutQueryString + queryParams);
124+
request.setHeader("Metadata", "True");
125+
return request;
16126
}
17127
}

src/main/java/net/snowflake/client/core/auth/wif/GcpIdentityAttestationCreator.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package net.snowflake.client.core.auth.wif;
22

3+
import static net.snowflake.client.core.auth.wif.WorkloadIdentityUtil.DEFAULT_METADATA_SERVICE_BASE_URL;
4+
import static net.snowflake.client.core.auth.wif.WorkloadIdentityUtil.performIdentityRequest;
5+
36
import java.util.Collections;
4-
import net.snowflake.client.core.HttpUtil;
57
import net.snowflake.client.core.SFLoginInput;
68
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
79
import net.snowflake.client.log.SFLogger;
@@ -25,7 +27,7 @@ public class GcpIdentityAttestationCreator implements WorkloadIdentityAttestatio
2527

2628
public GcpIdentityAttestationCreator(SFLoginInput loginInput) {
2729
this.loginInput = loginInput;
28-
gcpMetadataServiceBaseUrl = DEFAULT_GCP_METADATA_SERVICE_BASE_URL;
30+
gcpMetadataServiceBaseUrl = DEFAULT_METADATA_SERVICE_BASE_URL;
2931
}
3032

3133
/** Only for testing purpose */
@@ -36,6 +38,7 @@ public GcpIdentityAttestationCreator(SFLoginInput loginInput) {
3638

3739
@Override
3840
public WorkloadIdentityAttestation createAttestation() {
41+
logger.debug("Creating GCP identity attestation...");
3942
String token = fetchTokenFromMetadataService();
4043
if (token == null) {
4144
logger.debug("No GCP token was found.");
@@ -71,15 +74,9 @@ private String fetchTokenFromMetadataService() {
7174
HttpGet tokenRequest = new HttpGet(uri);
7275
tokenRequest.setHeader(METADATA_FLAVOR_HEADER_NAME, METADATA_FLAVOR);
7376
try {
74-
return HttpUtil.executeGeneralRequestOmitRequestGuid(
75-
tokenRequest,
76-
loginInput.getLoginTimeout(),
77-
3, // 3s timeout
78-
loginInput.getSocketTimeoutInMillis(),
79-
0,
80-
loginInput.getHttpClientSettingsKey());
77+
return performIdentityRequest(tokenRequest, loginInput);
8178
} catch (Exception e) {
82-
logger.debug("GCP metadata server request was not successful: " + e.getMessage());
79+
logger.debug("GCP metadata server request was not successful: {}" + e);
8380
return null;
8481
}
8582
}

src/main/java/net/snowflake/client/core/auth/wif/OidcIdentityAttestationCreator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public OidcIdentityAttestationCreator(String token) {
1818

1919
@Override
2020
public WorkloadIdentityAttestation createAttestation() {
21+
logger.debug("Creating OIDC identity attestation...");
2122
if (token == null) {
2223
logger.debug("No OIDC token was specified");
2324
return null;

src/main/java/net/snowflake/client/core/auth/wif/WorkloadIdentityAttestation.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,17 @@ public String getCredential() {
3030
public Map<String, String> getUserIdentifierComponents() {
3131
return userIdentifierComponents;
3232
}
33+
34+
@Override
35+
public String toString() {
36+
return "WorkloadIdentityAttestation{"
37+
+ "provider="
38+
+ provider
39+
+ ", credential='"
40+
+ credential
41+
+ '\''
42+
+ ", userIdentifierComponents="
43+
+ userIdentifierComponents
44+
+ '}';
45+
}
3346
}

src/main/java/net/snowflake/client/core/auth/wif/WorkloadIdentityUtil.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
import com.nimbusds.jwt.JWT;
44
import com.nimbusds.jwt.JWTParser;
5+
import java.io.IOException;
56
import java.util.HashMap;
67
import java.util.Map;
8+
import net.snowflake.client.core.HttpUtil;
9+
import net.snowflake.client.core.SFLoginInput;
10+
import net.snowflake.client.jdbc.SnowflakeSQLException;
711
import net.snowflake.client.log.SFLogger;
812
import net.snowflake.client.log.SFLoggerFactory;
13+
import org.apache.http.client.methods.HttpRequestBase;
914

1015
class WorkloadIdentityUtil {
1116

@@ -14,6 +19,20 @@ class WorkloadIdentityUtil {
1419
static final String SNOWFLAKE_AUDIENCE_HEADER_NAME = "X-Snowflake-Audience";
1520
static final String SNOWFLAKE_AUDIENCE = "snowflakecomputing.com";
1621

22+
// Address commonly used by AWS, Azure & GCP to host instance metadata service
23+
static final String DEFAULT_METADATA_SERVICE_BASE_URL = "http://169.254.169.254";
24+
25+
static String performIdentityRequest(HttpRequestBase tokenRequest, SFLoginInput loginInput)
26+
throws SnowflakeSQLException, IOException {
27+
return HttpUtil.executeGeneralRequestOmitRequestGuid(
28+
tokenRequest,
29+
loginInput.getLoginTimeout(),
30+
3, // 3s timeout
31+
loginInput.getSocketTimeoutInMillis(),
32+
0,
33+
loginInput.getHttpClientSettingsKey());
34+
}
35+
1736
static SubjectAndIssuer extractClaimsWithoutVerifyingSignature(String token) {
1837
Map<String, Object> claims = extractClaimsMap(token);
1938
if (claims == null) {
@@ -38,7 +57,7 @@ private static Map<String, Object> extractClaimsMap(String token) {
3857
JWT jwt = JWTParser.parse(token);
3958
return jwt.getJWTClaimsSet().getClaims();
4059
} catch (Exception e) {
41-
logger.debug("Unable to extract JWT claims from token", e);
60+
logger.error("Unable to extract JWT claims from token: {}", e);
4261
return null;
4362
}
4463
}

0 commit comments

Comments
 (0)