Repository: microsoft/mssql-jdbc
Pull request #2147 (2023-06-13 16:19:00)

Adding a new connection property "useDefaultJaasConfig" to coexist with solutions that overwrite the system JAAS
The existing feature of the JDBC driver to automatically authenticate using Kerberos without any external configuration is fantastic!  The complexity of JAAS can be intimidating.

This feature I am proposing is the simplest way I saw to allow the JDBC driver to perform Kerberos authentication without any additional external configuration, when the JDBC driver coexists in the same application with libraries that configure JAAS at the system level.  

This may not be a very common use case, but it is very useful in a corporate environment that provides, for instance, a Kafka Kerberos configuration, to let the JDBC Driver authenticate via Kerberos itself, without having to resort to configuring a common jaas.conf for all applications.  In the case where the user has not specified a jaasConfigurationName, it seems reasonable to assume they are hoping to directly connect to a SQLServer using the authentication schema that they specified in their connection properties.

I did think that the existing `jaasConfigurationName` feature could be useful if it did allow a separate jaas.conf filename, instead of a configuration name.  The documentation is a little confusing when it refers to this option as a _filename_ and not a _configuration name_, so this patch also tweaks the Javadoc for those settings.

I recognize that adding a new connection property is not something the project will want to accept lightly, and I am open to alternative proposals that could solve the same problem!  Thanks for taking the time to consider it.

microsoft-github-policy-service[bot]: 
@edanidzerda please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

```
@microsoft-github-policy-service agree [company="{your company}"]
```

> Options:
> - (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
>```
>@microsoft-github-policy-service agree
>```
> - (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
>```
>@microsoft-github-policy-service agree company="Microsoft"
>```

<details>
    <summary>Contributor License Agreement</summary>
    
### Contribution License Agreement
This Contribution License Agreement (**“Agreement”**) is agreed to by the party signing below (**“You”**),
and conveys certain license rights to Microsoft Corporation and its affiliates (“Microsoft”) for Your
contributions to Microsoft open source projects. This Agreement is effective as of the latest signature
date below.
1. **Definitions**.
**“Code”** means the computer software code, whether in human-readable or machine-executable form,
that is delivered by You to Microsoft under this Agreement.
**“Project”** means any of the projects owned or managed by Microsoft and offered under a license
approved by the Open Source Initiative (www.opensource.org).
**“Submit”** is the act of uploading, submitting, transmitting, or distributing code or other content to any
Project, including but not limited to communication on electronic mailing lists, source code control
systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of
discussing and improving that Project, but excluding communication that is conspicuously marked or
otherwise designated in writing by You as “Not a Submission.”
**“Submission”** means the Code and any other copyrightable material Submitted by You, including any
associated comments and documentation.
2. **Your Submission**. You must agree to the terms of this Agreement before making a Submission to any
Project. This Agreement covers any and all Submissions that You, now or in the future (except as
described in Section 4 below), Submit to any Project.
3. **Originality of Work**. You represent that each of Your Submissions is entirely Your original work.
Should You wish to Submit materials that are not Your original work, You may Submit them separately
to the Project if You (a) retain all copyright and license information that was in the materials as You
received them, (b) in the description accompanying Your Submission, include the phrase “Submission
containing materials of a third party:” followed by the names of the third party and any licenses or other
restrictions of which You are aware, and (c) follow any other instructions in the Project’s written
guidelines concerning Submissions.
4. **Your Employer**. References to “employer” in this Agreement include Your employer or anyone else
for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your
Submission is made in the course of Your work for an employer or Your employer has intellectual
property rights in Your Submission by contract or applicable law, You must secure permission from Your
employer to make the Submission before signing this Agreement. In that case, the term “You” in this
Agreement will refer to You and the employer collectively. If You change employers in the future and
desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement
and secure permission from the new employer before Submitting those Submissions.
5. **Licenses**.
  * **Copyright License**. You grant Microsoft, and those who receive the Submission directly or
indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license in the
Submission to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute
the Submission and such derivative works, and to sublicense any or all of the foregoing rights to third
parties.
  * **Patent License**. You grant Microsoft, and those who receive the Submission directly or
indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under
Your patent claims that are necessarily infringed by the Submission or the combination of the
Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and
import or otherwise dispose of the Submission alone or with the Project.
  * **Other Rights Reserved**. Each party reserves all rights not expressly granted in this Agreement.
No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are
granted by implication, exhaustion, estoppel or otherwise.
6. **Representations and Warranties**. You represent that You are legally entitled to grant the above
licenses. You represent that each of Your Submissions is entirely Your original work (except as You may
have disclosed under Section 3). You represent that You have secured permission from Your employer to
make the Submission in cases where Your Submission is made in the course of Your work for Your
employer or Your employer has intellectual property rights in Your Submission by contract or applicable
law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You
have the necessary authority to bind the listed employer to the obligations contained in this Agreement.
You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS
REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES
EXPRESSLY STATED IN SECTIONS 3, 4, AND 6, THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS
PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
7. **Notice to Microsoft**. You agree to notify Microsoft in writing of any facts or circumstances of which
You later become aware that would make Your representations in this Agreement inaccurate in any
respect.
8. **Information about Submissions**. You agree that contributions to Projects and information about
contributions may be maintained indefinitely and disclosed publicly, including Your name and other
information that You submit with Your Submission.
9. **Governing Law/Jurisdiction**. This Agreement is governed by the laws of the State of Washington, and
the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County,
Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to
exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all
defenses of lack of personal jurisdiction and forum non-conveniens.
10. **Entire Agreement/Assignment**. This Agreement is the entire agreement between the parties, and
supersedes any and all prior agreements, understandings or communications, written or oral, between
the parties relating to the subject matter hereof. This Agreement may be assigned by Microsoft.

</details>
<!-- Policy app identification https://img.shields.io/static/v1?label=ContributorLicenseAgreement. -->
edanidzerda: @microsoft-github-policy-service agree
Jeffery-Wasty: HI @edanidzerda,

Thank you for the PR, we'll take a look into it.
edanidzerda: Thanks for agreeing to look into it!  I've been researching the issue further, and I feel very confident now that this would be an great addition for anyone combining SQL Server Kerberos authentication alongside Spring Kafka with Kerberos, as well as any other libraries that attempt to auto-configure the JAAS configuration.

For a little more background, the conflict we see in testing is that when Spring Kafka's beans are instantiated BEFORE a data source using mssql-jdbc, [Spring Kafka creates a Kafka security configuration and overwrites the JVM's JAAS Configuration ](https://github.com/spring-projects/spring-kafka/blob/bbefd04f953680c6346575cbf51fdcc6690a1968/spring-kafka/src/main/java/org/springframework/kafka/security/jaas/KafkaJaasLoginModuleInitializer.java#L127-L144
)


```
			configurationEntries.put(KAFKA_CLIENT_CONTEXT_NAME,
					new AppConfigurationEntry[] { kafkaClientConfigurationEntry });
			Configuration.setConfiguration(new InternalConfiguration(configurationEntries));
```

When mssql-jdbc starts up after this, the clever `JaasConfiguration` uses Spring Kafka's configuration as the "delegate" and Kerberos authentication continues to work for both SQL Server and Kafka.

```
static {
        // Overrides the default JAAS configuration loader.
        // This one will forward to the default one in all cases but the default configuration is empty.
        Configuration.setConfiguration(new JaasConfiguration(Configuration.getConfiguration()));
    }
```

Unfortunately, if mssql-jdbc starts first, then Spring Kafka overwrites the Configuration object when it starts.  While initial SQL connections succeed, connections shortly after startup fail because of `No LoginModules configured for SQLJDBCDriver at javax.security.auth.login.LoginContext.init(LoginContext.java:264)`

In the old days, we could configure a `jaas.conf` for both SQL and Kafka on our physical server or VM with Puppet and go grab a beer.  In the current world of containers everywhere, having a configuration option to `ignoreSystemJaas` would be a great help for applications trying to use multiple libraries that may have conflicting approaches to setting the JAAS configuration.

Thanks again!  😃 

David-Engel: Thanks for the clarification. This sounds like a useful addition. I was trying to understand the use case so we can appropriately document the setting. My initial thought is to reverse the setting logic (i.e. instead of `ignoreSystemJaas`, call it something like `useDefaultJaasConfig`) but with the added detail, I understand why you've suggested `ignore`. I'm not crazy about the term `ignore` but I'm not sure which option is better, `ignoreSystemJaas` or `useDefaultJaasConfig` (slightly favoring ignore at this point). Did you consider other options for the property name?
tkyc: @edanidzerda Everything looks good on our end. We'd like to include this in a preview release before a stable release so this change won't be slated in for our upcoming stable release, but rather the one after. Thanks for contribution and patience.
edanidzerda: That's great!  I completely understand your need to move slowly with this project.  

I see you added it to 12.5.0 milestone; are you open to backport to 10.2.x or 11 at some point?

Let me know if there's anything else I can do to help!  Thanks again!  😄 
tkyc: I'll check with the team again and let you know if it's possible to do a backport.
edanidzerda: Thanks for your consideration!  I squashed my patch into a single commit to make backporting a more palatable task 😅 
tkyc: @edanidzerda Sorry, after checking with the team, it looks like we've decided not to do a backport as this is a new feature eg. connection string property so it wouldn't make sense to do a backport.
edanidzerda: Thanks @tkyc for discussing and getting back to me!  You all have been great to work with.

Patch:
diff --git a/README.md b/README.md
index 8dd8a9ae..600d05a5 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ To build the jar files, you must use minimum version of Java 11 with Maven. You
 * Maven:
 	1. If you have not already done so, add the environment variable `mssql_jdbc_test_connection_properties` in your system with the connection properties for your SQL Server or SQL DB instance.
 	2. Run one of the commands below to build a JRE 11 and newer versions compatible jar or JRE 8 compatible jar in the `\target` directory. 
-        * Run `mvn install -Pjre20`. This creates JRE 20 compatible jar in `\target` directory which is JDBC 4.3 compliant (Build with JDK 20).
+        * Run `mvn install -Pjre21`. This creates JRE 21 compatible jar in `\target` directory which is JDBC 4.3 compliant (Build with JDK 21).
         * Run `mvn install -Pjre17`. This creates JRE 17 compatible jar in `\target` directory which is JDBC 4.3 compliant (Build with JDK 17+).
         * Run `mvn install -Pjre11`. This creates JRE 11 compatible jar in `\target` directory which is JDBC 4.3 compliant (Build with JDK 11+).
         * Run `mvn install -Pjre8`. This creates JRE 8 compatible jar in `\target` directory which is JDBC 4.2 compliant (Build with JDK 11+).
@@ -52,7 +52,7 @@ To build the jar files, you must use minimum version of Java 11 with Maven. You
 * Gradle:
 	1. If you have not already done so, add the environment variable `mssql_jdbc_test_connection_properties` in your system with the connection properties for your SQL Server or SQL DB instance.
 	2. Run one of the commands below to build a JRE 11 and newer versions compatible jar or JRE 8 compatible jar in the `\build\libs` directory. 
-        * Run `gradle build -PbuildProfile=jre20`. This creates JRE 20 compatible jar in `\build\libs` directory which is JDBC 4.3 compliant (Build with JDK 20).
+        * Run `gradle build -PbuildProfile=jre21`. This creates JRE 21 compatible jar in `\build\libs` directory which is JDBC 4.3 compliant (Build with JDK 21).
         * Run `gradle build -PbuildProfile=jre17`. This creates JRE 17 compatible jar in `\build\libs` directory which is JDBC 4.3 compliant (Build with JDK 17+).
         * Run `gradle build -PbuildProfile=jre11`. This creates JRE 11 compatible jar in `\build\libs` directory which is JDBC 4.3 compliant (Build with JDK 11+).
         * Run `gradle build -PbuildProfile=jre8`. This creates JRE 8 compatible jar in `\build\libs` directory which is JDBC 4.2 compliant (Build with JDK 11+).
diff --git a/build.gradle b/build.gradle
index 4ce995b8..13a17efe 100644
--- a/build.gradle
+++ b/build.gradle
@@ -33,17 +33,17 @@ test {
     }
 }
 
-if (!hasProperty('buildProfile') || (hasProperty('buildProfile') && buildProfile == "jre20")) {
+if (!hasProperty('buildProfile') || (hasProperty('buildProfile') && buildProfile == "jre21")) {
 
-	jreVersion = "jre20"
+	jreVersion = "jre21"
 	excludedFile = 'com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java'
 	jar {
 		manifest {
 			attributes 'Automatic-Module-Name': 'com.microsoft.sqlserver.jdbc'
 		}
 	}
-	sourceCompatibility = 20
-	targetCompatibility = 20
+	sourceCompatibility = 21
+	targetCompatibility = 21
 }
 
 if (hasProperty('buildProfile') && buildProfile == "jre17") {
diff --git a/pom.xml b/pom.xml
index c7fb25e1..ceb27f69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,13 +42,14 @@
 			xAzureSQLDB - - - - For tests not compatible with Azure SQL Database - - 
 			xAzureSQLDW - - - - For tests not compatible with Azure Data Warehouse - 
 			xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance 
-			NTLM - - - - - - For tests using NTLM Authentication mode (excluded by default) 
-			reqExternalSetup - For tests requiring external setup (excluded by default) 
+			NTLM  - - - - - - - For tests using NTLM Authentication mode (excluded by default)
+			Kerberos -  - - - - For tests using Kerberos authentication (excluded by default)
+			reqExternalSetup - For tests requiring external setup (excluded by default)
 			clientCertAuth - - For tests requiring client certificate authentication 
 			setup (excluded by default) - - - - - - - - - - - - - - - - - - - - - - - 
 			- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 			Default testing enabled with SQL Server 2019 (SQLv15) -->
-		<excludedGroups>xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth</excludedGroups>
+		<excludedGroups>xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth,kerberos</excludedGroups>
 		<!-- Use -preview for preview release, leave empty for official release. -->
 		<releaseExt>-preview</releaseExt>
 		<!-- Driver Dependencies -->
@@ -345,12 +346,12 @@
 			</build>
 		</profile>
 		<profile>
-			<id>jre20</id>
+			<id>jre21</id>
 			<activation>
 				<activeByDefault>true</activeByDefault>
 			</activation>
 			<build>
-				<finalName>${project.artifactId}-${project.version}.jre20${releaseExt}</finalName>
+				<finalName>${project.artifactId}-${project.version}.jre21${releaseExt}</finalName>
 				<plugins>
 					<plugin>
 						<groupId>org.apache.maven.plugins</groupId>
@@ -360,8 +361,8 @@
 							<excludes>
 								<exclude>**/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java</exclude>
 							</excludes>
-							<source>20</source>
-							<target>20</target>
+							<source>21</source>
+							<target>21</target>
 						</configuration>
 					</plugin>
 					<plugin>
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java b/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java
index 9d525186..c21c1863 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java
@@ -7,6 +7,8 @@ package com.microsoft.sqlserver.jdbc;
 
 import java.text.MessageFormat;
 import java.util.logging.Level;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 
 class FedAuthDllInfo {
@@ -37,6 +39,8 @@ final class AuthenticationJNI extends SSPIAuthentication {
 
     private static final UnsatisfiedLinkError linkError;
 
+    private static final Lock lock = new ReentrantLock();
+
     static int getMaxSSPIBlobSize() {
         return sspiBlobMaxlen;
     }
@@ -82,8 +86,14 @@ final class AuthenticationJNI extends SSPIAuthentication {
 
     static FedAuthDllInfo getAccessTokenForWindowsIntegrated(String stsURL, String servicePrincipalName,
             String clientConnectionId, String clientId, long expirationFileTime) throws DLLException {
-        return ADALGetAccessTokenForWindowsIntegrated(stsURL, servicePrincipalName, clientConnectionId, clientId,
-                expirationFileTime, authLogger);
+        try {
+            lock.lock();
+
+            return ADALGetAccessTokenForWindowsIntegrated(stsURL, servicePrincipalName, clientConnectionId, clientId,
+                    expirationFileTime, authLogger);
+        } finally {
+            lock.unlock();
+        }
     }
 
     // InitDNSName should be called to initialize the DNSName before calling this function
@@ -154,13 +164,13 @@ final class AuthenticationJNI extends SSPIAuthentication {
 
     private static native int GetDNSName(String address, String[] DNSName, java.util.logging.Logger log);
 
-    private static synchronized native FedAuthDllInfo ADALGetAccessTokenForWindowsIntegrated(String stsURL,
+    private static native FedAuthDllInfo ADALGetAccessTokenForWindowsIntegrated(String stsURL,
             String servicePrincipalName, String clientConnectionId, String clientId, long expirationFileTime,
             java.util.logging.Logger log);
 
-    static synchronized native byte[] DecryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm,
+    static native byte[] DecryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm,
             byte[] encryptedColumnEncryptionKey) throws DLLException;
 
-    static synchronized native boolean VerifyColumnMasterKeyMetadata(String keyPath, boolean allowEnclaveComputations,
+    static native boolean VerifyColumnMasterKeyMetadata(String keyPath, boolean allowEnclaveComputations,
             byte[] signature) throws DLLException;
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index a435666e..019b7999 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -802,7 +802,7 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
     int getSocketTimeout();
 
     /**
-     * Sets the login configuration file for Kerberos authentication. This overrides the default configuration <i>
+     * Sets the login configuration name for Kerberos authentication. This overrides the default configuration <i>
      * SQLJDBCDriver </i>
      * 
      * @param configurationName
@@ -814,7 +814,7 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
     void setJASSConfigurationName(String configurationName);
 
     /**
-     * Returns the login configuration file for Kerberos authentication.
+     * Returns the login configuration name for Kerberos authentication.
      *
      * 
      * @return login configuration file name
@@ -825,7 +825,7 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
     String getJASSConfigurationName();
 
     /**
-     * Sets the login configuration file for Kerberos authentication. This overrides the default configuration <i>
+     * Sets the login configuration name for Kerberos authentication. This overrides the default configuration <i>
      * SQLJDBCDriver </i>
      * 
      * 
@@ -835,12 +835,27 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
     void setJAASConfigurationName(String configurationName);
 
     /**
-     * Returns the login configuration file for Kerberos authentication.
+     * Returns the login configuration name for Kerberos authentication.
      * 
-     * @return login configuration file name
+     * @return login configuration name
      */
     String getJAASConfigurationName();
 
+    /**
+     * Returns whether the default JAAS Configuration should be used
+     *
+     * @return useDefaultJaasConfig boolean value
+     */
+    boolean getUseDefaultJaasConfig();
+
+    /**
+     * Sets whether the default JAAS Configuration will be used.  This means the system-wide JAAS configuration
+     * is ignored to avoid conflicts with libraries that override the JAAS configuration.
+     *
+     * @param useDefaultJaasConfig
+     *         boolean property to use the default JAAS configuration
+     */
+    void setUseDefaultJaasConfig(boolean useDefaultJaasConfig);
     /**
      * Sets whether Fips Mode should be enabled/disabled on the connection. For FIPS enabled JVM this property should be
      * true.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java
index ffc01644..b58bb9fa 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java
@@ -73,13 +73,31 @@ final class KerbAuthentication extends SSPIAuthentication {
                 String configName = con.activeConnectionProperties.getProperty(
                         SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(),
                         SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue());
+                boolean useDefaultJaas = Boolean.parseBoolean(con.activeConnectionProperties.getProperty(
+                        SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.toString(),
+                        Boolean.toString(SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.getDefaultValue())));
+
+                if (!configName.equals(
+                        SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue()) && useDefaultJaas) {
+                    // Reset configName to default -- useDefaultJaas setting takes priority over jaasConfigName
+                    if (authLogger.isLoggable(Level.WARNING)) {
+                        authLogger.warning(toString() + String.format(
+                                "Using default JAAS configuration, configured %s=%s will not be used.",
+                                SQLServerDriverStringProperty.JAAS_CONFIG_NAME, configName));
+                    }
+                    configName = SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue();
+                }
                 Subject currentSubject;
                 KerbCallback callback = new KerbCallback(con);
                 try {
                     AccessControlContext context = AccessController.getContext();
                     currentSubject = Subject.getSubject(context);
                     if (null == currentSubject) {
-                        lc = new LoginContext(configName, callback);
+                        if (useDefaultJaas) {
+                            lc = new LoginContext(configName, null, callback, new JaasConfiguration(null));
+                        } else {
+                            lc = new LoginContext(configName, callback);
+                        }
                         lc.login();
                         // per documentation LoginContext will instantiate a new subject.
                         currentSubject = lc.getSubject();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
index 48160ee7..c1e45928 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
@@ -1724,53 +1724,57 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
         SQLServerStatement stmt = null;
         String metaDataQuery = null;
 
-        try {
-            if (null != destinationTableMetadata) {
-                rs = (SQLServerResultSet) destinationTableMetadata;
-            } else {
-                stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
-                        ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting);
+        if (null == destColumnMetadata || destColumnMetadata.isEmpty()) {
+            try {
+                if (null != destinationTableMetadata) {
+                    rs = (SQLServerResultSet) destinationTableMetadata;
+                } else {
+                    stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
+                            ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting);
 
-                // Get destination metadata
-                rs = stmt.executeQueryInternal(
-                        "sp_executesql N'SET FMTONLY ON SELECT * FROM " + escapedDestinationTableName + " '");
-            }
+                    // Get destination metadata
+                    rs = stmt.executeQueryInternal(
+                            "sp_executesql N'SET FMTONLY ON SELECT * FROM " + escapedDestinationTableName + " '");
+                }
 
-            destColumnCount = rs.getMetaData().getColumnCount();
-            destColumnMetadata = new HashMap<>();
-            destCekTable = rs.getCekTable();
+                destColumnCount = rs.getMetaData().getColumnCount();
+                destColumnMetadata = new HashMap<>();
+                destCekTable = rs.getCekTable();
 
-            if (!connection.getServerSupportsColumnEncryption()) {
-                metaDataQuery = "select collation_name from sys.columns where " + "object_id=OBJECT_ID('"
-                        + escapedDestinationTableName + "') " + "order by column_id ASC";
-            } else {
-                metaDataQuery = "select collation_name, encryption_type from sys.columns where "
-                        + "object_id=OBJECT_ID('" + escapedDestinationTableName + "') " + "order by column_id ASC";
-            }
+                if (!connection.getServerSupportsColumnEncryption()) {
+                    metaDataQuery = "select collation_name from sys.columns where " + "object_id=OBJECT_ID('"
+                            + escapedDestinationTableName + "') " + "order by column_id ASC";
+                } else {
+                    metaDataQuery = "select collation_name, encryption_type from sys.columns where "
+                            + "object_id=OBJECT_ID('" + escapedDestinationTableName + "') " + "order by column_id ASC";
+                }
 
-            try (SQLServerStatement statementMoreMetadata = (SQLServerStatement) connection.createStatement();
-                    SQLServerResultSet rsMoreMetaData = statementMoreMetadata.executeQueryInternal(metaDataQuery)) {
-                for (int i = 1; i <= destColumnCount; ++i) {
-                    if (rsMoreMetaData.next()) {
-                        String bulkCopyEncryptionType = null;
-                        if (connection.getServerSupportsColumnEncryption()) {
-                            bulkCopyEncryptionType = rsMoreMetaData.getString("encryption_type");
+                try (SQLServerStatement statementMoreMetadata = (SQLServerStatement) connection.createStatement();
+                     SQLServerResultSet rsMoreMetaData = statementMoreMetadata.executeQueryInternal(metaDataQuery)) {
+                    for (int i = 1; i <= destColumnCount; ++i) {
+                        if (rsMoreMetaData.next()) {
+                            String bulkCopyEncryptionType = null;
+                            if (connection.getServerSupportsColumnEncryption()) {
+                                bulkCopyEncryptionType = rsMoreMetaData.getString("encryption_type");
+                            }
+                            destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i),
+                                    rsMoreMetaData.getString("collation_name"), bulkCopyEncryptionType));
+                        } else {
+                            destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i)));
                         }
-                        destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i),
-                                rsMoreMetaData.getString("collation_name"), bulkCopyEncryptionType));
-                    } else {
-                        destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i)));
                     }
                 }
+            } catch (SQLException e) {
+                // Unable to retrieve metadata for destination
+                throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
+            } finally {
+                if (null != rs) {
+                    rs.close();
+                }
+                if (null != stmt) {
+                    stmt.close();
+                }
             }
-        } catch (SQLException e) {
-            // Unable to retrieve metadata for destination
-            throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
-        } finally {
-            if (null != rs)
-                rs.close();
-            if (null != stmt)
-                stmt.close();
         }
     }
 
@@ -1779,41 +1783,43 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
      * BulkColumnMetaData object helps to access source metadata from the same place for both ResultSet and File.
      */
     private void getSourceMetadata() throws SQLServerException {
-        srcColumnMetadata = new HashMap<>();
-        int currentColumn;
-        if (null != sourceResultSet) {
-            try {
-                srcColumnCount = sourceResultSetMetaData.getColumnCount();
-                for (int i = 1; i <= srcColumnCount; ++i) {
-                    srcColumnMetadata.put(i,
-                            new BulkColumnMetaData(sourceResultSetMetaData.getColumnName(i),
-                                    (ResultSetMetaData.columnNoNulls != sourceResultSetMetaData.isNullable(i)),
-                                    sourceResultSetMetaData.getPrecision(i), sourceResultSetMetaData.getScale(i),
-                                    sourceResultSetMetaData.getColumnType(i), null));
+        if (null == srcColumnMetadata || srcColumnMetadata.isEmpty()) {
+            srcColumnMetadata = new HashMap<>();
+            int currentColumn;
+            if (null != sourceResultSet) {
+                try {
+                    srcColumnCount = sourceResultSetMetaData.getColumnCount();
+                    for (int i = 1; i <= srcColumnCount; ++i) {
+                        srcColumnMetadata.put(i,
+                                new BulkColumnMetaData(sourceResultSetMetaData.getColumnName(i),
+                                        (ResultSetMetaData.columnNoNulls != sourceResultSetMetaData.isNullable(i)),
+                                        sourceResultSetMetaData.getPrecision(i), sourceResultSetMetaData.getScale(i),
+                                        sourceResultSetMetaData.getColumnType(i), null));
+                    }
+                } catch (SQLException e) {
+                    // Unable to retrieve meta data for destination
+                    throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
                 }
-            } catch (SQLException e) {
-                // Unable to retrieve meta data for destination
-                throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
-            }
-        } else if (null != serverBulkData) {
-            Set<Integer> columnOrdinals = serverBulkData.getColumnOrdinals();
-            if (null == columnOrdinals || columnOrdinals.isEmpty()) {
-                throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null);
-            } else {
-                srcColumnCount = columnOrdinals.size();
-                for (Integer columnOrdinal : columnOrdinals) {
-                    currentColumn = columnOrdinal;
-                    srcColumnMetadata.put(currentColumn, new BulkColumnMetaData(
-                            serverBulkData.getColumnName(currentColumn), true,
-                            serverBulkData.getPrecision(currentColumn), serverBulkData.getScale(currentColumn),
-                            serverBulkData.getColumnType(currentColumn),
-                            ((serverBulkData instanceof SQLServerBulkCSVFileRecord) ? ((SQLServerBulkCSVFileRecord) serverBulkData)
-                                    .getColumnDateTimeFormatter(currentColumn) : null)));
+            } else if (null != serverBulkData) {
+                Set<Integer> columnOrdinals = serverBulkData.getColumnOrdinals();
+                if (null == columnOrdinals || columnOrdinals.isEmpty()) {
+                    throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null);
+                } else {
+                    srcColumnCount = columnOrdinals.size();
+                    for (Integer columnOrdinal : columnOrdinals) {
+                        currentColumn = columnOrdinal;
+                        srcColumnMetadata.put(currentColumn, new BulkColumnMetaData(
+                                serverBulkData.getColumnName(currentColumn), true,
+                                serverBulkData.getPrecision(currentColumn), serverBulkData.getScale(currentColumn),
+                                serverBulkData.getColumnType(currentColumn),
+                                ((serverBulkData instanceof SQLServerBulkCSVFileRecord) ? ((SQLServerBulkCSVFileRecord) serverBulkData)
+                                        .getColumnDateTimeFormatter(currentColumn) : null)));
+                    }
                 }
+            } else {
+                // Unable to retrieve meta data for source
+                throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null);
             }
-        } else {
-            // Unable to retrieve meta data for source
-            throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null);
         }
     }
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index acb062ce..2dfcdfb0 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -1859,7 +1859,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     if (0 == connectRetryCount) {
                         // connection retry disabled
                         throw e;
-                    } else if (++connectRetryAttempt > connectRetryCount) {
+                    } else if (connectRetryAttempt++ > connectRetryCount) {
                         // maximum connection retry count reached
                         if (connectionlogger.isLoggable(Level.FINE)) {
                             connectionlogger.fine("Connection failed. Maximum connection retry count "
@@ -3249,7 +3249,6 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                         || (SQLServerException.ERROR_SOCKET_TIMEOUT == driverErrorCode // socket timeout
                                 && (!isDBMirroring || attemptNumber > 0)) // If mirroring, only close after failover has been tried (attempt >= 1)
                         || timerHasExpired(timerExpire)
-                        || (state.equals(State.CONNECTED) && !isDBMirroring)
                 // for non-dbmirroring cases, do not retry after tcp socket connection succeeds
                 ) {
                     // close the connection and throw the error back
@@ -6329,9 +6328,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             serverName = currentConnectPlaceHolder.getFullServerName();
         } else {
             serverName = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.SERVER_NAME.toString());
-            if (null != activeConnectionProperties.getProperty(SQLServerDriverStringProperty.INSTANCE_NAME.toString())) {
-                serverName += "\\" +
-                        activeConnectionProperties.getProperty(SQLServerDriverStringProperty.INSTANCE_NAME.toString());
+            if (null != activeConnectionProperties
+                    .getProperty(SQLServerDriverStringProperty.INSTANCE_NAME.toString())) {
+                serverName += "\\" + activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.INSTANCE_NAME.toString());
             }
         }
 
@@ -7390,7 +7390,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     String replaceParameterMarkers(String sqlSrc, int[] paramPositions, Parameter[] params,
             boolean isReturnValueSyntax) {
         final int MAX_PARAM_NAME_LEN = 6;
-        char[] sqlDst = new char[sqlSrc.length() + (params.length * (MAX_PARAM_NAME_LEN + OUT.length)) + (params.length * 2)];
+        char[] sqlDst = new char[sqlSrc.length() + (params.length * (MAX_PARAM_NAME_LEN + OUT.length))
+                + (params.length * 2)];
         int dstBegin = 0;
         int srcBegin = 0;
         int nParam = 0;
@@ -8100,19 +8101,18 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             return supportsTransactions;
         }
 
-        supportsTransactions = true;
-
         try {
             this.connectionCommand("SELECT @@TRANCOUNT", "SQLServerConnection.supportsTransactions");
         } catch (SQLServerException e) {
-            if (e.getMessage().equals(SQLServerException.getErrString("R_transactionsNotSupported"))) {
+            if (e.getMessage().trim().equals(SQLServerException.getErrString("R_transactionsNotSupported"))) {
                 supportsTransactions = false;
                 return false;
             }
             throw e;
         }
 
-        return supportsTransactions;
+        supportsTransactions = true;
+        return true;
     }
 
     /**
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index b7e01811..216ef569 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -1030,6 +1030,18 @@ public class SQLServerDataSource
                 SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue());
     }
 
+    @Override
+    public boolean getUseDefaultJaasConfig() {
+        return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.toString(),
+                SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.getDefaultValue());
+    }
+
+    @Override
+    public void setUseDefaultJaasConfig(boolean useDefaultJaasConfig) {
+        setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.toString(),
+                useDefaultJaasConfig);
+    }
+
     /**
      * @deprecated This method is deprecated. Use {@link SQLServerDataSource#setUser(String user)} instead.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 1bd92024..966ffb07 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -693,7 +693,8 @@ enum SQLServerDriverBooleanProperty {
     USE_BULK_COPY_FOR_BATCH_INSERT("useBulkCopyForBatchInsert", false),
     USE_FMT_ONLY("useFmtOnly", false),
     SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY("sendTemporalDataTypesAsStringForBulkCopy", true),
-    DELAY_LOADING_LOBS("delayLoadingLobs", true);
+    DELAY_LOADING_LOBS("delayLoadingLobs", true),
+    USE_DEFAULT_JAAS_CONFIG("useDefaultJaasConfig", false);
 
     private final String name;
     private final boolean defaultValue;
@@ -892,6 +893,9 @@ public final class SQLServerDriver implements java.sql.Driver {
                     null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(),
                     SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue(), false, null),
+            new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.toString(),
+                    Boolean.toString(SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.getDefaultValue()), false,
+                    TRUE_FALSE),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SSL_PROTOCOL.toString(),
                     SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false,
                     new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(),
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
index 52044cdf..c8f2fb34 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
@@ -24,6 +24,8 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.logging.Level;
 
 import javax.security.auth.kerberos.KerberosPrincipal;
@@ -69,14 +71,18 @@ class SQLServerMSAL4JUtils {
         throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported"));
     }
 
-    static synchronized SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user,
-            String password, String authenticationString) throws SQLServerException {
+    private static final Lock lock = new ReentrantLock();
+
+    static SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password,
+            String authenticationString) throws SQLServerException {
         ExecutorService executorService = Executors.newSingleThreadExecutor();
 
         if (logger.isLoggable(Level.FINEST)) {
             logger.finest(LOGCONTEXT + authenticationString + ": get FedAuth token for user: " + user);
         }
 
+        lock.lock();
+
         try {
             final PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
@@ -104,12 +110,13 @@ class SQLServerMSAL4JUtils {
         } catch (MalformedURLException | ExecutionException e) {
             throw getCorrectedException(e, user, authenticationString);
         } finally {
+            lock.unlock();
             executorService.shutdown();
         }
     }
 
-    static synchronized SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuthInfo,
-            String aadPrincipalID, String aadPrincipalSecret, String authenticationString) throws SQLServerException {
+    static SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuthInfo, String aadPrincipalID,
+            String aadPrincipalSecret, String authenticationString) throws SQLServerException {
         ExecutorService executorService = Executors.newSingleThreadExecutor();
 
         if (logger.isLoggable(Level.FINEST)) {
@@ -122,6 +129,8 @@ class SQLServerMSAL4JUtils {
         Set<String> scopes = new HashSet<>();
         scopes.add(scope);
 
+        lock.lock();
+
         try {
             IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret);
             ConfidentialClientApplication clientApplication = ConfidentialClientApplication
@@ -148,11 +157,12 @@ class SQLServerMSAL4JUtils {
         } catch (MalformedURLException | ExecutionException e) {
             throw getCorrectedException(e, aadPrincipalID, authenticationString);
         } finally {
+            lock.unlock();
             executorService.shutdown();
         }
     }
 
-    static synchronized SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthInfo fedAuthInfo,
+    static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthInfo fedAuthInfo,
             String aadPrincipalID, String certFile, String certPassword, String certKey, String certKeyPassword,
             String authenticationString) throws SQLServerException {
         ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -168,6 +178,8 @@ class SQLServerMSAL4JUtils {
         Set<String> scopes = new HashSet<>();
         scopes.add(scope);
 
+        lock.lock();
+
         try {
             ConfidentialClientApplication clientApplication = null;
 
@@ -249,11 +261,12 @@ class SQLServerMSAL4JUtils {
             throw getCorrectedException(e, aadPrincipalID, authenticationString);
 
         } finally {
+            lock.unlock();
             executorService.shutdown();
         }
     }
 
-    static synchronized SqlAuthenticationToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo,
+    static SqlAuthenticationToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo,
             String authenticationString) throws SQLServerException {
         ExecutorService executorService = Executors.newSingleThreadExecutor();
 
@@ -269,6 +282,8 @@ class SQLServerMSAL4JUtils {
                     + "realm name:" + kerberosPrincipal.getRealm());
         }
 
+        lock.lock();
+
         try {
             final PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
@@ -296,11 +311,12 @@ class SQLServerMSAL4JUtils {
         } catch (IOException | ExecutionException e) {
             throw getCorrectedException(e, user, authenticationString);
         } finally {
+            lock.unlock();
             executorService.shutdown();
         }
     }
 
-    static synchronized SqlAuthenticationToken getSqlFedAuthTokenInteractive(SqlFedAuthInfo fedAuthInfo, String user,
+    static SqlAuthenticationToken getSqlFedAuthTokenInteractive(SqlFedAuthInfo fedAuthInfo, String user,
             String authenticationString) throws SQLServerException {
         ExecutorService executorService = Executors.newSingleThreadExecutor();
 
@@ -308,6 +324,8 @@ class SQLServerMSAL4JUtils {
             logger.finest(LOGCONTEXT + authenticationString + ": get FedAuth token interactive for user: " + user);
         }
 
+        lock.lock();
+
         try {
             PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
@@ -384,6 +402,7 @@ class SQLServerMSAL4JUtils {
         } catch (MalformedURLException | URISyntaxException | ExecutionException e) {
             throw getCorrectedException(e, user, authenticationString);
         } finally {
+            lock.unlock();
             executorService.shutdown();
         }
     }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 44db5331..9aa64877 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -451,7 +451,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_kerberosLoginFailedForUsername", "Cannot login with Kerberos principal {0}, check your credentials. {1}"},
         {"R_kerberosLoginFailed", "Kerberos Login failed: {0} due to {1} ({2})"},
         {"R_StoredProcedureNotFound", "Could not find stored procedure ''{0}''."},
-        {"R_jaasConfigurationNamePropertyDescription", "Login configuration file for Kerberos authentication."},
+        {"R_jaasConfigurationNamePropertyDescription", "Login configuration name for Kerberos authentication."},
+        {"R_useDefaultJaasConfigPropertyDescription", "Use the default JAAS configuration for Kerberos authentication."},
         {"R_AKVKeyNotFound", "Key not found: {0}"},
         {"R_SQLVariantSupport", "SQL_VARIANT is not supported in versions of SQL Server before 2008."},
         {"R_invalidProbbytes", "SQL_VARIANT: invalid probBytes for {0} type."},


Test patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java
index 46a3fa05..c0e2e2ad 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java
@@ -6,6 +6,8 @@
 package com.microsoft.sqlserver.jdbc;
 
 import java.util.Locale;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 
 /**
@@ -25,6 +27,8 @@ public final class SQLServerColumnEncryptionCertificateStoreProvider extends SQL
     static final String CURRENT_USER_DIRECTORY = "CurrentUser";
     static final String MY_CERTIFICATE_STORE = "My";
 
+    private static final Lock lock = new ReentrantLock();
+
     static {
         if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows")) {
             isWindows = true;
@@ -78,21 +82,29 @@ public final class SQLServerColumnEncryptionCertificateStoreProvider extends SQL
     public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allowEnclaveComputations,
             byte[] signature) throws SQLServerException {
         try {
+            lock.lock();
+
             return AuthenticationJNI.VerifyColumnMasterKeyMetadata(masterKeyPath, allowEnclaveComputations, signature);
         } catch (DLLException e) {
             DLLException.buildException(e.getErrCode(), e.getParam1(), e.getParam2(), e.getParam3());
             return false;
+        } finally {
+            lock.unlock();
         }
     }
 
     private byte[] decryptColumnEncryptionKeyWindows(String masterKeyPath, String encryptionAlgorithm,
             byte[] encryptedColumnEncryptionKey) throws SQLServerException {
         try {
+            lock.lock();
+
             return AuthenticationJNI.DecryptColumnEncryptionKey(masterKeyPath, encryptionAlgorithm,
                     encryptedColumnEncryptionKey);
         } catch (DLLException e) {
             DLLException.buildException(e.getErrCode(), e.getParam1(), e.getParam2(), e.getParam3());
             return null;
+        } finally {
+            lock.unlock();
         }
     }
 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/BulkCopySendTemporalDataTypesAsStringAETest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/BulkCopySendTemporalDataTypesAsStringAETest.java
index d8552ad3..6dd177ab 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/BulkCopySendTemporalDataTypesAsStringAETest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/BulkCopySendTemporalDataTypesAsStringAETest.java
@@ -57,6 +57,7 @@ import com.microsoft.sqlserver.testframework.PrepUtil;
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xAzureSQLDB)
 @Tag(Constants.xAzureSQLDW)
+@Tag(Constants.reqExternalSetup)
 public class BulkCopySendTemporalDataTypesAsStringAETest extends AESetup {
     static String inputFile = "BulkCopyCSVSendTemporalDataTypesAsStringForBulkCopy.csv";
     static String encoding = "UTF-8";
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java
index 58ed2e63..95531d69 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java
@@ -53,6 +53,7 @@ import microsoft.sql.DateTimeOffset;
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xAzureSQLDW)
 @Tag(Constants.xAzureSQLDB)
+@Tag(Constants.reqExternalSetup)
 public class CallableStatementTest extends AESetup {
 
     private static String multiStatementsProcedure = AbstractSQLGenerator
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java
index d4c2771c..7376baca 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java
@@ -64,6 +64,7 @@ import microsoft.sql.DateTimeOffset;
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xAzureSQLDW)
 @Tag(Constants.xAzureSQLDB)
+@Tag(Constants.reqExternalSetup)
 public class JDBCEncryptionDecryptionTest extends AESetup {
     private boolean nullable = false;
 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java
index c3332158..2619a79b 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java
@@ -58,6 +58,7 @@ import com.microsoft.sqlserver.testframework.PrepUtil;
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xAzureSQLDW)
 @Tag(Constants.xAzureSQLDB)
+@Tag(Constants.reqExternalSetup)
 public class MultiUserAKVTest extends AESetup {
 
     private static Map<String, SQLServerColumnEncryptionKeyStoreProvider> requiredKeyStoreProvider = new HashMap<>();
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/ParameterMetaDataCacheTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/ParameterMetaDataCacheTest.java
index 5f84e7cb..cef219d4 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/ParameterMetaDataCacheTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/ParameterMetaDataCacheTest.java
@@ -32,6 +32,7 @@ import com.microsoft.sqlserver.testframework.PrepUtil;
 @Tag(Constants.xSQLv11)
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xSQLv14)
+@Tag(Constants.reqExternalSetup)
 public class ParameterMetaDataCacheTest extends AESetup {
 
     @BeforeAll
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java
index 5343b1fc..162fe8ce 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java
@@ -42,6 +42,7 @@ import com.microsoft.sqlserver.testframework.PrepUtil;
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xAzureSQLDW)
 @Tag(Constants.xAzureSQLDB)
+@Tag(Constants.reqExternalSetup)
 public class PrecisionScaleTest extends AESetup {
     private static java.util.Date date = null;
     private static int offsetFromGMT = 0;
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/RegressionAlwaysEncryptedTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/RegressionAlwaysEncryptedTest.java
index 87584eb7..82ec693e 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/RegressionAlwaysEncryptedTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/RegressionAlwaysEncryptedTest.java
@@ -29,6 +29,7 @@ import com.microsoft.sqlserver.testframework.PrepUtil;
 @Tag(Constants.xSQLv12)
 @Tag(Constants.xAzureSQLDW)
 @Tag(Constants.xAzureSQLDB)
+@Tag(Constants.reqExternalSetup)
 public class RegressionAlwaysEncryptedTest extends AESetup {
     static String numericTable[][] = {{"Bit", "bit"}, {"Tinyint", "tinyint"}, {"Smallint", "smallint"},};
 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/KerberosTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/KerberosTest.java
new file mode 100644
index 00000000..8755f468
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/KerberosTest.java
@@ -0,0 +1,105 @@
+package com.microsoft.sqlserver.jdbc;
+
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(JUnitPlatform.class)
+public class KerberosTest extends AbstractTest {
+
+    private static String kerberosAuth = "KERBEROS";
+
+    @BeforeAll
+    public static void setupTests() throws Exception {
+        setConnection();
+    }
+
+    @Tag(Constants.Kerberos)
+    @Test
+    public void testUseDefaultJaasConfigConnectionStringPropertyTrue() throws Exception {
+        String connectionStringUseDefaultJaasConfig = connectionStringKerberos + ";useDefaultJaasConfig=true;";
+
+        // Initial connection should succeed with default JAAS config
+        try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionStringUseDefaultJaasConfig)) {
+            ResultSet rs = conn.createStatement().executeQuery("select auth_scheme from sys.dm_exec_connections where session_id=@@spid");
+            rs.next();
+            Assertions.assertEquals(kerberosAuth, rs.getString(1));
+        }
+
+        // Attempt to overwrite JAAS config. Since useDefaultJaasConfig=true, this should have no effect
+        // and subsequent connections should succeed.
+        overwriteJaasConfig();
+
+        // New connection should successfully connect and continue to use the default JAAS config.
+        try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionStringUseDefaultJaasConfig)) {
+            ResultSet rs = conn.createStatement().executeQuery("select auth_scheme from sys.dm_exec_connections where session_id=@@spid");
+            rs.next();
+            Assertions.assertEquals(kerberosAuth, rs.getString(1));
+        }
+    }
+
+    @Tag(Constants.Kerberos)
+    @Test
+    public void testUseDefaultJaasConfigConnectionStringPropertyFalse() throws Exception {
+
+        // useDefaultJaasConfig=false by default
+        // Initial connection should succeed with default JAAS config
+        try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionStringKerberos)) {
+            ResultSet rs = conn.createStatement().executeQuery("select auth_scheme from sys.dm_exec_connections where session_id=@@spid");
+            rs.next();
+            Assertions.assertEquals(kerberosAuth, rs.getString(1));
+        }
+
+        // Overwrite JAAS config. Since useDefaultJaasConfig=false, overwriting should succeed and have an effect.
+        // Subsequent connections will fail.
+        overwriteJaasConfig();
+
+        // New connection should fail as it is attempting to connect using an overwritten JAAS config.
+        try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionStringKerberos)) {
+            Assertions.fail(TestResource.getResource("R_expectedExceptionNotThrown"));
+        } catch (SQLServerException e) {
+            Assertions.assertTrue(e.getMessage()
+                    .contains(TestResource.getResource("R_noLoginModulesConfiguredForJdbcDriver")));
+        }
+    }
+
+    /**
+     * Overwrites the default JAAS config. Call before making a connection.
+     */
+    private static void overwriteJaasConfig() {
+        AppConfigurationEntry kafkaClientConfigurationEntry = new AppConfigurationEntry(
+                "com.sun.security.auth.module.Krb5LoginModule",
+                AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+                new HashMap<>());
+        Map<String, AppConfigurationEntry[]> configurationEntries = new HashMap<>();
+        configurationEntries.put("KAFKA_CLIENT_CONTEXT_NAME",
+                new AppConfigurationEntry[] { kafkaClientConfigurationEntry });
+        Configuration.setConfiguration(new InternalConfiguration(configurationEntries));
+    }
+
+    private static class InternalConfiguration extends Configuration {
+        private final Map<String, AppConfigurationEntry[]> configurationEntries;
+
+        InternalConfiguration(Map<String, AppConfigurationEntry[]> configurationEntries) {
+            this.configurationEntries = configurationEntries;
+        }
+
+        @Override
+        public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+            return this.configurationEntries.get(name);
+        }
+
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index 690a7e3f..e4ff9bc8 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -138,6 +138,9 @@ public class SQLServerConnectionTest extends AbstractTest {
         ds.setJAASConfigurationName(stringPropValue);
         assertEquals(stringPropValue, ds.getJAASConfigurationName(), TestResource.getResource("R_valuesAreDifferent"));
 
+        ds.setUseDefaultJaasConfig(booleanPropValue);
+        assertEquals(booleanPropValue, ds.getUseDefaultJaasConfig(), TestResource.getResource("R_valuesAreDifferent"));
+
         ds.setMSIClientId(stringPropValue);
         assertEquals(stringPropValue, ds.getMSIClientId(), TestResource.getResource("R_valuesAreDifferent"));
 
@@ -478,39 +481,6 @@ public class SQLServerConnectionTest extends AbstractTest {
         }
     }
 
-    /**
-     * Tests whether connectRetryCount and connectRetryInterval are properly respected in the login loop. As well, tests
-     * that connection is retried the proper number of times. This is for cases with zero retries.
-     */
-    @Test
-    public void testConnectCountInLoginAndCorrectRetryCountWithZeroRetry() {
-        long timerStart = 0;
-
-        int connectRetryCount = 0;
-        int connectRetryInterval = 60;
-        int longLoginTimeout = loginTimeOutInSeconds * 3; // 90 seconds
-
-        try {
-            SQLServerDataSource ds = new SQLServerDataSource();
-            ds.setURL(connectionString);
-            ds.setLoginTimeout(longLoginTimeout);
-            ds.setConnectRetryCount(connectRetryCount);
-            ds.setConnectRetryInterval(connectRetryInterval);
-            ds.setDatabaseName(RandomUtil.getIdentifier("DataBase"));
-            timerStart = System.currentTimeMillis();
-
-            try (Connection con = ds.getConnection()) {
-                assertTrue(con == null, TestResource.getResource("R_shouldNotConnect"));
-            }
-        } catch (Exception e) {
-            assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage());
-            long totalTime = System.currentTimeMillis() - timerStart;
-
-            // Maximum is unknown, but is needs to be less than longLoginTimeout or else this is an issue.
-            assertTrue(totalTime < (longLoginTimeout * 1000L), TestResource.getResource("R_executionTooLong"));
-        }
-    }
-
     @Test
     @Tag(Constants.xAzureSQLDW)
     @Tag(Constants.xAzureSQLDB)
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index 1df53941..c21f5610 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -203,5 +203,6 @@ public final class TestResource extends ListResourceBundle {
             {"R_connectTimedOut", "connect timed out"},
             {"R_sessionKilled", "Cannot continue the execution because the session is in the kill state"},
             {"R_failedFedauth", "Failed to acquire fedauth token: "},
+            {"R_noLoginModulesConfiguredForJdbcDriver", "javax.security.auth.login.LoginException (No LoginModules configured for SQLJDBCDriver)"},
             {"R_unexpectedThreadCount", "Thread count is higher than expected."}};
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java
index 2a381976..b3d6fdc3 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java
@@ -165,87 +165,6 @@ public class ResultSetsWithResiliencyTest extends AbstractTest {
         }
     }
 
-    /*
-     * Test killing a session while retrieving result set should result in exception thrown
-     */
-    @Test
-    public void testKillSession() throws Exception {
-        // setup test with big tables
-        String table1 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("killSessionTestTable1"));
-        String table2 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("killSessionTestTable2"));
-        String table3 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("killSessionTestTable3"));
-
-        try (Connection c = DriverManager.getConnection(connectionString); Statement s = c.createStatement();
-                PreparedStatement ps1 = c.prepareStatement("INSERT INTO " + table1 + " values (?)");
-                PreparedStatement ps2 = c.prepareStatement("INSERT INTO " + table2 + " values (?)");
-                PreparedStatement ps3 = c.prepareStatement("INSERT INTO " + table3 + " values (?)")) {
-            TestUtils.dropTableIfExists(table1, s);
-            TestUtils.dropTableIfExists(table2, s);
-            TestUtils.dropTableIfExists(table3, s);
-            s.execute("CREATE TABLE " + table1
-                    + " (ID int primary key IDENTITY(1,1) NOT NULL, NAME varchar(255) NOT NULL); CREATE TABLE " + table2
-                    + " (ID int primary key IDENTITY(1,1) NOT NULL, NAME varchar(255) NOT NULL); CREATE TABLE " + table3
-                    + "( ID int primary key IDENTITY(1,1) NOT NULL, NAME varchar(255) NOT NULL);");
-
-            for (int i = 0; i < 1000; i++) {
-                ps1.setString(1, "value" + i);
-                ps2.setString(1, "value" + i);
-                ps3.setString(1, "value" + i);
-
-                ps1.addBatch();
-                ps2.addBatch();
-                ps3.addBatch();
-            }
-
-            ps1.executeBatch();
-            ps2.executeBatch();
-            ps3.executeBatch();
-
-            c.commit();
-
-            try (Connection c2 = DriverManager.getConnection(connectionString)) {
-                int sessionId = ResiliencyUtils.getSessionId(c2);
-
-                Runnable r1 = () -> {
-                    try {
-                        ResiliencyUtils.killConnection(sessionId, connectionString, c2, 10);
-                    } catch (Exception e) {
-                        fail(e.getMessage());;
-                    }
-                };
-
-                Thread t1 = new Thread(r1);
-                t1.start();
-
-                // execute query which takes a long time and kill session in another thread
-                try (PreparedStatement ps = c2.prepareStatement("SELECT e1.* FROM " + table1 + " e1, " + table2
-                        + " e2, " + table3 + " e3, " + table1
-                        + " e4 where e1.name = 'abc' or e2.name = 'def'or e3.name = 'ghi' or e4.name = 'xxx' and e1.name not in (select name  FROM "
-                        + table2 + ") and e2.name not in (select name  FROM " + table1
-                        + " ) and e3.name not in (SELECT name FROM " + table2
-                        + ") and e4.name not in (SELECT name FROM " + table3 + ");");
-                        ResultSet rs = ps.executeQuery()) {
-
-                    fail(TestResource.getResource("R_expectedExceptionNotThrown"));
-                } catch (SQLException e) {
-                    // may get different error message depending on SQL servers.
-                    // Local servers will report a TDS error where as Azure servers will have a DONE error
-                    assertTrue(
-                            e.getMessage().matches(TestUtils.formatErrorMsg("R_serverError"))
-                                    || e.getMessage().contains(TestResource.getResource("R_sessionKilled"))
-                                    || e.getMessage().contains(TestResource.getResource("R_connectionReset")),
-                            e.getMessage());
-                }
-                t1.join();
-
-            } finally {
-                TestUtils.dropTableIfExists(table1, s);
-                TestUtils.dropTableIfExists(table2, s);
-                TestUtils.dropTableIfExists(table3, s);
-            }
-        }
-    }
-
     @Test
     public void testResultSetClientCursorInitializerOnDone() throws SQLException {
         try (Connection con = ResiliencyUtils.getConnection(connectionString); Statement stmt = con.createStatement()) {
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
index 1d9eb244..9113ca19 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
@@ -66,6 +66,10 @@ public abstract class AbstractTest {
     protected static String[] enclaveAttestationUrl = null;
     protected static String[] enclaveAttestationProtocol = null;
 
+
+    protected static String kerberosServer = null;
+    protected static String kerberosServerPort = null;
+
     protected static String clientCertificate = null;
     protected static String clientKey = null;
     protected static String clientKeyPassword = "";
@@ -103,6 +107,7 @@ public abstract class AbstractTest {
     protected static Connection connectionAzure = null;
     protected static String connectionString = null;
     protected static String connectionStringNTLM;
+    protected static String connectionStringKerberos;
 
     protected static ConfidentialClientApplication fedauthClientApp = null;
 
@@ -193,6 +198,9 @@ public abstract class AbstractTest {
 
         clientKeyPassword = getConfiguredProperty("clientKeyPassword", "");
 
+        kerberosServer = getConfiguredProperty("kerberosServer", null);
+        kerberosServerPort = getConfiguredProperty("kerberosServerPort", null);
+
         trustStore = getConfiguredProperty("trustStore", "");
         if (!trustStore.trim().isEmpty()) {
             connectionString = TestUtils.addOrOverrideProperty(connectionString, "trustStore", trustStore);
@@ -250,7 +258,7 @@ public abstract class AbstractTest {
     protected static void setupConnectionString() {
         connectionStringNTLM = connectionString;
 
-        // if these properties are defined then NTLM is desired, modify connection string accordingly
+        // If these properties are defined then NTLM is desired, modify connection string accordingly
         String domain = getConfiguredProperty("domainNTLM");
         String user = getConfiguredProperty("userNTLM");
         String password = getConfiguredProperty("passwordNTLM");
@@ -273,6 +281,14 @@ public abstract class AbstractTest {
             connectionStringNTLM = TestUtils.addOrOverrideProperty(connectionStringNTLM, "integratedSecurity", "true");
         }
 
+        if (null != kerberosServer && null != kerberosServerPort) {
+            connectionStringKerberos = "jdbc:sqlserver://" + kerberosServer + ":" + kerberosServerPort + ";";
+            connectionStringKerberos = TestUtils.addOrOverrideProperty(connectionStringKerberos, "authenticationScheme", "JavaKerberos");
+            connectionStringKerberos = TestUtils.addOrOverrideProperty(connectionStringKerberos, "integratedSecurity", "true");
+            connectionStringKerberos = TestUtils.addOrOverrideProperty(connectionStringKerberos, "trustServerCertificate", "true");
+            connectionStringKerberos = TestUtils.addOrOverrideProperty(connectionStringKerberos, "encrypt", "false");
+        }
+
         ds = updateDataSource(connectionString, new SQLServerDataSource());
         dsXA = updateDataSource(connectionString, new SQLServerXADataSource());
         dsPool = updateDataSource(connectionString, new SQLServerConnectionPoolDataSource());
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
index ce50791f..3b2d0872 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
@@ -25,6 +25,7 @@ public final class Constants {
      * xAzureSQLDW - - - - For tests not compatible with Azure Data Warehouse
      * xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance
      * NTLM  - - - - - - - For NTLM tests
+     * Kerberos  - - - - - For Kerberos tests
      * reqExternalSetup  - For tests requiring external setup
      * clientCertAuth  - - For tests requiring client certificate authentication setup
      * Fedauth - - - - - - For Fedauth tests
@@ -41,6 +42,7 @@ public final class Constants {
     public static final String xAzureSQLDW = "xAzureSQLDW";
     public static final String xAzureSQLMI = "xAzureSQLMI";
     public static final String NTLM = "NTLM";
+    public static final String Kerberos = "kerberos";
     public static final String MSI = "MSI";
     public static final String reqExternalSetup = "reqExternalSetup";
     public static final String clientCertAuth = "clientCertAuth";