Repository: snowflakedb/snowflake-jdbc
Pull request #2158: SNOW-1902245: Accept v1 and v2 Entra ID issuers
Author: login: sfc-gh-pmansour
Created: 2025-04-17T16:58:24Z | Merged: 2025-04-18T09:49:25Z

# Overview

SNOW-1902245 Accept both v1 and v2 formats of Entra ID issuers

## Pre-review self checklist
- [x] PR branch is updated with all the changes from `master` branch
- [x] The code is correctly formatted (run `mvn -P check-style validate`)
- [x] New public API is not unnecessary exposed (run `mvn verify` and inspect `target/japicmp/japicmp.html`)
- [x] The pull request name is prefixed with `SNOW-XXXX: `
- [x] Code is in compliance with internal logging requirements

## External contributors - please answer these questions before submitting a pull request. Thanks!

1. What GitHub issue is this PR addressing? Make sure that there is an accompanying issue to your PR.

   Issue: #NNNN


2. Fill out the following pre-review checklist:

   - [ ] I am adding a new automated test(s) to verify correctness of my new code
   - [ ] I am adding new logging messages
   - [ ] I am modifying authorization mechanisms
   - [ ] I am adding new credentials
   - [ ] I am modifying OCSP code
   - [ ] I am adding a new dependency or upgrading an existing one
   - [ ] I am adding new public/protected component not marked with `@SnowflakeJdbcInternalApi` (note that public/protected methods/fields in classes marked with this annotation are already internal)

3. Please describe how your code solves the related issue.

   Please write a short description of how your code change solves the related issue.



Diff:
diff --git a/src/main/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreator.java b/src/main/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreator.java
index 80bfa1df9a..e700b31172 100644
--- a/src/main/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreator.java
+++ b/src/main/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreator.java
@@ -7,6 +7,9 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Strings;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 import net.snowflake.client.core.SFLoginInput;
 import net.snowflake.client.core.SnowflakeJdbcInternalApi;
 import net.snowflake.client.log.SFLogger;
@@ -20,7 +23,9 @@ public class AzureIdentityAttestationCreator implements WorkloadIdentityAttestat
       SFLoggerFactory.getLogger(AzureIdentityAttestationCreator.class);
   public static final ObjectMapper objectMapper = new ObjectMapper();
 
-  private static final String EXPECTED_AZURE_TOKEN_ISSUER_PREFIX = "https://sts.windows.net/";
+  private static final Set<String> ALLOWED_AZURE_TOKEN_ISSUER_PREFIXES =
+      new HashSet<>(
+          Arrays.asList("https://sts.windows.net/", "https://login.microsoftonline.com/"));
   private static final String DEFAULT_WORKLOAD_IDENTITY_ENTRA_RESOURCE =
       "api://fd3f753b-eed3-462c-b6a7-a4b5bb650aad";
 
@@ -80,7 +85,10 @@ public WorkloadIdentityAttestation createAttestation() {
       logger.error("Could not extract claims from token");
       return null;
     }
-    if (!claims.getIssuer().startsWith(EXPECTED_AZURE_TOKEN_ISSUER_PREFIX)) {
+    boolean hasAllowedPrefix =
+        ALLOWED_AZURE_TOKEN_ISSUER_PREFIXES.stream()
+            .anyMatch(prefix -> claims.getIssuer().startsWith(prefix));
+    if (!hasAllowedPrefix) {
       logger.error("Unexpected Azure token issuer: {}", claims.getIssuer());
       return null;
     }
diff --git a/src/test/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreatorLatestIT.java b/src/test/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreatorLatestIT.java
index 7dc7c2a3c1..e1c1fc707d 100644
--- a/src/test/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreatorLatestIT.java
+++ b/src/test/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreatorLatestIT.java
@@ -28,6 +28,15 @@ public class AzureIdentityAttestationCreatorLatestIT extends BaseWiremockTest {
   private static final String SUCCESSFUL_FLOW_BASIC_SCENARIO_MAPPINGS =
       SCENARIOS_BASE_DIR + "/successful_flow_basic.json";
 
+  /*
+   * {
+   *     "iss": "https://login.microsoftonline.com/fa15d692-e9c7-4460-a743-29f29522229/",
+   *     "sub": "77213E30-E8CB-4595-B1B6-5F050E8308FD"
+   * }
+   */
+  private static final String SUCCESSFUL_FLOW_V2_ISSUER_SCENARIO_MAPPINGS =
+      SCENARIOS_BASE_DIR + "/successful_flow_v2_issuer.json";
+
   /*
    * {
    *     "iss": "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f29522229/",
@@ -37,6 +46,15 @@ public class AzureIdentityAttestationCreatorLatestIT extends BaseWiremockTest {
   private static final String SUCCESSFUL_FLOW_AZURE_FUNCTIONS_SCENARIO_MAPPINGS =
       SCENARIOS_BASE_DIR + "/successful_flow_azure_functions.json";
 
+  /*
+   * {
+   *     "iss": "https://login.microsoftonline.com/fa15d692-e9c7-4460-a743-29f29522229/",
+   *     "sub": "77213E30-E8CB-4595-B1B6-5F050E8308FD"
+   * }
+   */
+  private static final String SUCCESSFUL_FLOW_AZURE_FUNCTIONS_V2_ISSUER_SCENARIO_MAPPINGS =
+      SCENARIOS_BASE_DIR + "/successful_flow_azure_functions_v2_issuer.json";
+
   /*
    * {
    *     "iss": "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f29522229/",
@@ -100,6 +118,17 @@ public void successfulFlowBasicScenario() {
     executeAndAssertCorrectAttestation(attestationServiceMock, loginInput);
   }
 
+  @Test
+  public void successfulFlowV2IssuerScenario() {
+    importMappingFromResources(SUCCESSFUL_FLOW_V2_ISSUER_SCENARIO_MAPPINGS);
+    SFLoginInput loginInput = createLoginInputStub();
+    AzureAttestationService attestationServiceMock = createAttestationServiceSpyForBasicFLow();
+    executeAndAssertCorrectAttestationWithIssuer(
+        attestationServiceMock,
+        loginInput,
+        "https://login.microsoftonline.com/fa15d692-e9c7-4460-a743-29f29522229/");
+  }
+
   @Test
   public void successfulFlowAzureFunctionsScenario() {
     importMappingFromResources(SUCCESSFUL_FLOW_AZURE_FUNCTIONS_SCENARIO_MAPPINGS);
@@ -114,6 +143,23 @@ public void successfulFlowAzureFunctionsScenario() {
     executeAndAssertCorrectAttestation(attestationServiceSpy, loginInput);
   }
 
+  @Test
+  public void successfulFlowAzureFunctionsWithV2IssuerScenario() {
+    importMappingFromResources(SUCCESSFUL_FLOW_AZURE_FUNCTIONS_V2_ISSUER_SCENARIO_MAPPINGS);
+    SFLoginInput loginInput = createLoginInputStub();
+    AzureAttestationService attestationServiceSpy = Mockito.spy(AzureAttestationService.class);
+    Mockito.when(attestationServiceSpy.getIdentityEndpoint())
+        .thenReturn(getBaseUrl() + "metadata/identity/endpoint/from/env");
+    Mockito.when(attestationServiceSpy.getIdentityHeader())
+        .thenReturn("some-identity-header-from-env");
+    Mockito.when(attestationServiceSpy.getClientId()).thenReturn("managed-client-id-from-env");
+
+    executeAndAssertCorrectAttestationWithIssuer(
+        attestationServiceSpy,
+        loginInput,
+        "https://login.microsoftonline.com/fa15d692-e9c7-4460-a743-29f29522229/");
+  }
+
   @Test
   public void successfulFlowAzureFunctionsNoClientIdScenario() {
     importMappingFromResources(SUCCESSFUL_FLOW_AZURE_FUNCTIONS_NO_CLIENT_ID_SCENARIO_MAPPINGS);
@@ -204,6 +250,16 @@ private static AzureAttestationService createAttestationServiceSpyForBasicFLow()
 
   private void executeAndAssertCorrectAttestation(
       AzureAttestationService attestationServiceMock, SFLoginInput loginInput) {
+    executeAndAssertCorrectAttestationWithIssuer(
+        attestationServiceMock,
+        loginInput,
+        "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f29522229/");
+  }
+
+  private void executeAndAssertCorrectAttestationWithIssuer(
+      AzureAttestationService attestationServiceMock,
+      SFLoginInput loginInput,
+      String expectedIssuer) {
     AzureIdentityAttestationCreator attestationCreator =
         new AzureIdentityAttestationCreator(attestationServiceMock, loginInput, getBaseUrl());
 
@@ -213,9 +269,7 @@ private void executeAndAssertCorrectAttestation(
     assertEquals(
         "77213E30-E8CB-4595-B1B6-5F050E8308FD",
         attestation.getUserIdentifierComponents().get("sub"));
-    assertEquals(
-        "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f29522229/",
-        attestation.getUserIdentifierComponents().get("iss"));
+    assertEquals(expectedIssuer, attestation.getUserIdentifierComponents().get("iss"));
     assertNotNull(attestation.getCredential());
   }
 
diff --git a/src/test/resources/wiremock/mappings/wif/azure/successful_flow_azure_functions_v2_issuer.json b/src/test/resources/wiremock/mappings/wif/azure/successful_flow_azure_functions_v2_issuer.json
new file mode 100644
index 0000000000..42b8c20059
--- /dev/null
+++ b/src/test/resources/wiremock/mappings/wif/azure/successful_flow_azure_functions_v2_issuer.json
@@ -0,0 +1,32 @@
+{
+  "mappings": [
+    {
+      "request": {
+        "urlPattern": "/metadata/identity/endpoint/from/env.*",
+        "queryParameters": {
+          "api-version": {
+            "equalTo": "2019-08-01"
+          },
+          "resource": {
+            "equalTo": "api://fd3f753b-eed3-462c-b6a7-a4b5bb650aad"
+          },
+          "client_id": {
+            "equalTo": "managed-client-id-from-env"
+          }
+        },
+        "method": "GET",
+        "headers": {
+          "X-IDENTITY-HEADER": {
+            "equalTo": "some-identity-header-from-env"
+          }
+        }
+      },
+      "response": {
+        "status": 200,
+        "jsonBody": {
+          "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhcGk6Ly9mZDNmNzUzYi1lZWQzLTQ2MmMtYjZhNy1hNGI1YmI2NTBhYWQiLCJleHAiOjE3NDQ3MTYwNTEsImlhdCI6MTc0NDcxMjQ1MSwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tL2ZhMTVkNjkyLWU5YzctNDQ2MC1hNzQzLTI5ZjI5NTIyMjI5LyIsImp0aSI6Ijg3MTMzNzcwMDk0MTZmYmFhNDM0MmFkMjMxZGUwMDBkIiwic3ViIjoiNzcyMTNFMzAtRThDQi00NTk1LUIxQjYtNUYwNTBFODMwOEZEIn0.5mAlEPkzHLR7YbllpKgk-8ZEd88XfzA15DUK8u1rLWs"
+        }
+      }
+    }
+  ]
+}
diff --git a/src/test/resources/wiremock/mappings/wif/azure/successful_flow_v2_issuer.json b/src/test/resources/wiremock/mappings/wif/azure/successful_flow_v2_issuer.json
new file mode 100644
index 0000000000..87aef24142
--- /dev/null
+++ b/src/test/resources/wiremock/mappings/wif/azure/successful_flow_v2_issuer.json
@@ -0,0 +1,29 @@
+{
+  "mappings": [
+    {
+      "request": {
+        "urlPattern": "/metadata/identity/oauth2/token.*",
+        "queryParameters": {
+          "api-version": {
+            "equalTo": "2018-02-01"
+          },
+          "resource": {
+            "equalTo": "api://fd3f753b-eed3-462c-b6a7-a4b5bb650aad"
+          }
+        },
+        "method": "GET",
+        "headers": {
+          "Metadata": {
+            "equalTo": "True"
+          }
+        }
+      },
+      "response": {
+        "status": 200,
+        "jsonBody": {
+          "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhcGk6Ly9mZDNmNzUzYi1lZWQzLTQ2MmMtYjZhNy1hNGI1YmI2NTBhYWQiLCJleHAiOjE3NDQ3MTYwNTEsImlhdCI6MTc0NDcxMjQ1MSwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tL2ZhMTVkNjkyLWU5YzctNDQ2MC1hNzQzLTI5ZjI5NTIyMjI5LyIsImp0aSI6Ijg3MTMzNzcwMDk0MTZmYmFhNDM0MmFkMjMxZGUwMDBkIiwic3ViIjoiNzcyMTNFMzAtRThDQi00NTk1LUIxQjYtNUYwNTBFODMwOEZEIn0.5mAlEPkzHLR7YbllpKgk-8ZEd88XfzA15DUK8u1rLWs"
+        }
+      }
+    }
+  ]
+}


Reviews:
user: login: sfc-gh-dheyman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T17:21:15Z
user: login: sfc-gh-dheyman
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-17T17:23:46Z
user: login: sfc-gh-jszczerbinski
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:24:05Z
user: login: sfc-gh-pmansour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:36:05Z

Review comments:
diff_hunk: @@ -80,7 +85,14 @@ public WorkloadIdentityAttestation createAttestation() {
       logger.error("Could not extract claims from token");
       return null;
     }
-    if (!claims.getIssuer().startsWith(EXPECTED_AZURE_TOKEN_ISSUER_PREFIX)) {
+    boolean hasAllowedPrefix = false;
+    for (String prefix : ALLOWED_AZURE_TOKEN_ISSUER_PREFIXES) {
path: src/main/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreator.java
user: login: sfc-gh-dheyman
body: nit, but seems cleaner
`boolean hasAllowedPrefix = ALLOWED_AZURE_TOKEN_ISSUER_PREFIXES.stream()
            .anyMatch(prefix -> claims.getIssuer().startsWith(prefix));`
created_at: 2025-04-17T17:21:14Z
author_association: COLLABORATOR
diff_hunk: @@ -80,7 +85,14 @@ public WorkloadIdentityAttestation createAttestation() {
       logger.error("Could not extract claims from token");
       return null;
     }
-    if (!claims.getIssuer().startsWith(EXPECTED_AZURE_TOKEN_ISSUER_PREFIX)) {
+    boolean hasAllowedPrefix = false;
+    for (String prefix : ALLOWED_AZURE_TOKEN_ISSUER_PREFIXES) {
path: src/main/java/net/snowflake/client/core/auth/wif/AzureIdentityAttestationCreator.java
user: login: sfc-gh-pmansour
body: good suggestion, done
created_at: 2025-04-17T17:36:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: sfc-gh-snowflakedb-snyk-sa
created_at: 2025-04-17T16:58:37Z
body: ![](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### :tada: **Snyk checks have passed. No issues have been found so far.**

:white_check_mark: **security/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/snowflakedb-sca-scanning-public-repo/pr-checks/d807e0b4-6578-455a-9670-272dbf1d27a5?refs=9761a40be50f80987ac2a23a2d28de5673e4e3c8&source=prChecksComment)

:white_check_mark: **license/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/snowflakedb-sca-scanning-public-repo/pr-checks/d807e0b4-6578-455a-9670-272dbf1d27a5/license?refs=9761a40be50f80987ac2a23a2d28de5673e4e3c8&source=prChecksComment)



author_association: CONTRIBUTOR
user: login: gitguardian[bot]
created_at: 2025-04-17T16:58:59Z
body: #### ⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

<details>
<summary>🔎 Detected hardcoded secret in your pull request</summary>
<br>

| GitGuardian id | GitGuardian status | Secret                         | Commit           | Filename        |                      |
| -------------- | ------------------ | ------------------------------ | ---------------- | --------------- | -------------------- |
| [-](https://dashboard.gitguardian.com/workspace/265002/incidents/secrets) | - | Generic High Entropy Secret | 2b9d03dd916b035630cbb077a43d449aad7917d4 | src/test/resources/wiremock/mappings/wif/azure/successful_flow_azure_functions_v2_issuer.json | [View secret](https://github.com/snowflakedb/snowflake-jdbc/commit/2b9d03dd916b035630cbb077a43d449aad7917d4#diff-7fdb5d9d6adf8f006cae39df301d7ddaaae48bfabaf26d9fe9ce58b7e6d5d67dR27) |



</details>

<details>
<summary>🛠 Guidelines to remediate hardcoded secrets</summary>
<br>

The above secret(s) have been detected in your PR. Please take an appropriate action for each secret:

- If it’s a **true positive**, remove the secret from source code, revoke it and migrate to a secure way of storing and accessing secrets (see http://go/secrets-and-code). Once that’s done, go to the incidents page linked in the “GitGuardian id” column (log in using SnowBiz Okta) and resolve the incident.
- If it’s a **false positive**, go to the incidents page linked in the “GitGuardian id” column (log in using SnowBiz Okta) and ignore the incident.
- If you didn&#x27;t add this secret - and only then - you may ignore this check as it&#x27;s non-blocking. If you *did* add the secret and you ignore this check, you&#x27;ll be assigned a &quot;Security Finding&quot; ticket in Jira in a few days.

Note:
- **A secret is considered leaked from the moment it touches GitHub.** Rewriting git history by force pushing or other means is not necessary and doesn’t change the fact that the secret has to be revoked.
- This check has a “Skip: false positive” button. **Don’t use it.** It will mark all detected secrets as false positives but only in the context of this specific run - it won’t remember this action in subsequent check runs.

If you encounter any problems you can reach out to us on Slack: #gitguardian-secret-scanning-help


</details>

---

<sup>🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/></sup>
author_association: NONE
user: login: sfc-gh-pmansour
created_at: 2025-04-17T21:04:32Z
body: The failing CI tests don't look related to this change.

@sfc-gh-dheyman and others, what's the best way to proceed here?
author_association: CONTRIBUTOR
user: login: sfc-gh-dheyman
created_at: 2025-04-18T09:48:39Z
body: We're working currently on fixing this failing test, but this is something out of scope of this PR.
author_association: COLLABORATOR