Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 54 additions & 21 deletions src/main/java/com/mongodb/jdbc/MongoConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.mongodb.jdbc;

import static com.mongodb.AuthenticationMechanism.GSSAPI;
import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC;
import static com.mongodb.AuthenticationMechanism.MONGODB_X509;

Expand Down Expand Up @@ -184,28 +185,60 @@ private MongoClientSettings createMongoClientSettings(
if (credential != null) {
AuthenticationMechanism authMechanism = credential.getAuthenticationMechanism();

if (authMechanism != null && authMechanism.equals(MONGODB_OIDC)) {
// Handle OIDC authentication
OidcCallback oidcCallback = new JdbcOidcCallback(this.logger);
credential =
MongoCredential.createOidcCredential(
connectionProperties.getConnectionString().getUsername())
.withMechanismProperty(
MongoCredential.OIDC_HUMAN_CALLBACK_KEY, oidcCallback);
settingsBuilder.credential(credential);
} else if (authMechanism != null && authMechanism.equals(MONGODB_X509)) {
String pemPath = connectionProperties.getX509PemPath();
if (pemPath == null || pemPath.isEmpty()) {
pemPath = System.getenv(MONGODB_JDBC_X509_CLIENT_CERT_PATH);
}
if (pemPath == null || pemPath.isEmpty()) {
throw new IllegalStateException(
"PEM file path is required for X.509 authentication but was not provided.");
}
if (authMechanism != null) {
if (authMechanism.equals(MONGODB_OIDC)) {
// Handle OIDC authentication
OidcCallback oidcCallback = new JdbcOidcCallback(this.logger);
credential =
MongoCredential.createOidcCredential(
connectionProperties
.getConnectionString()
.getUsername())
.withMechanismProperty(
MongoCredential.OIDC_HUMAN_CALLBACK_KEY, oidcCallback);
settingsBuilder.credential(credential);
} else if (authMechanism.equals(GSSAPI)) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

String jaasPath = connectionProperties.getJaasConfigPath();
if (jaasPath != null && !jaasPath.isEmpty()) {
System.setProperty("java.security.auth.login.config", jaasPath);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf changes will affect more than our driver. It will affect everything currently running JVM.
This is a problem, especially with tools like Tableau. We need to talk about it a bit more.
Here is a starting point from Gemini on how we could use a local conf (possibly, we want to try it out to make sure this is true):

import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Logger;

public class MongoJaasProgrammatic {

    private static final Logger LOGGER = Logger.getLogger(MongoJaasProgrammatic.class.getName());

    public static void main(String[] args) {
        // Step 1: Set the custom JAAS Configuration programmatically
        // MyJaasConfig.loadConfiguration(); // Reuse the class from the previous answer

        Subject authenticatedSubject = null;
        try {
            LoginContext loginContext = new LoginContext("MyApplication");
            loginContext.login();
            authenticatedSubject = loginContext.getSubject();
        } catch (Exception e) {
            LOGGER.severe("JAAS login failed: " + e.getMessage());
            return;
        }

        if (authenticatedSubject == null) {
            LOGGER.severe("Authenticated subject is null. Exiting.");
            return;
        }

        // Step 2: Create a MongoCredential with the authenticated subject
        Map<String, String> properties = Collections.singletonMap(MongoCredential.JAVA_SUBJECT_KEY, authenticatedSubject.toString());
        MongoCredential credential = MongoCredential.createGSSAPICredential(properties);

        // Step 3: Build MongoClientSettings with the credential
        MongoClientSettings settings = MongoClientSettings.builder()
                .credential(credential)
                .build();
        
        // Step 4: Create MongoClient using the custom settings
        try (MongoClient mongoClient = MongoClients.create(settings)) {
            // Your database operations here
            System.out.println("Successfully connected to MongoDB using programmatic JAAS config.");
        } catch (Exception e) {
            LOGGER.severe("MongoDB connection failed: " + e.getMessage());
        }
    }
}

logger.log(Level.INFO, "Using custom JAAS config: " + jaasPath);
} else {
String existingConfig =
System.getProperty("java.security.auth.login.config");
if (existingConfig != null) {
logger.log(
Level.INFO,
"Using JAAS config from system property: " + existingConfig);
} else {
logger.log(
Level.INFO,
"No JAAS config specified. Relying on classpath or JVM defaults.");
}
}

String gssNative = connectionProperties.getGssNativeMode();
if (gssNative != null && !gssNative.isEmpty()) {
System.setProperty("sun.security.jgss.native", gssNative);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we want to look at using a local conf to make sure we don't impact other drivers when using Tableau for example.
Here is another Gemini quickstart:

import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;

import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

public class GssapiMongoNoSystemProp {

    private static final Logger LOGGER = Logger.getLogger(GssapiMongoNoSystemProp.class.getName());

    public static void main(String[] args) {

        // Step 1: Create a custom JAAS Configuration in memory.
        Configuration jaasConfig = new Configuration() {
            @Override
            public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
                if ("MongoConnection".equals(name)) {
                    Map<String, String> options = new HashMap<>();
                    options.put("useKeyTab", "true");
                    options.put("keyTab", "/path/to/your/keytab/file.keytab");
                    options.put("principal", "[email protected]");
                    options.put("storeKey", "true");
                    options.put("doNotPrompt", "true");
                    
                    return new AppConfigurationEntry[]{
                        new AppConfigurationEntry(
                            "com.sun.security.auth.module.Krb5LoginModule",
                            AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
                            options
                        )
                    };
                }
                return null;
            }
        };

        Subject authenticatedSubject = null;
        try {
            // Step 2: Use the custom Configuration to perform the login.
            // No need for a separate krb5.conf file if your
            // principal and keytab are correct. Kerberos will
            // try to auto-discover the KDC.
            LoginContext loginContext = new LoginContext("MongoConnection", authenticatedSubject, null, jaasConfig);
            loginContext.login();
            authenticatedSubject = loginContext.getSubject();
            LOGGER.info("Kerberos login successful using in-memory config.");
        } catch (Exception e) {
            LOGGER.severe("Kerberos login failed: " + e.getMessage());
            return;
        }

        if (authenticatedSubject == null) {
            LOGGER.severe("Authenticated subject is null. Cannot proceed.");
            return;
        }

        // Step 3: Create a MongoCredential with the authenticated subject.
        Map<String, Object> properties = Collections.singletonMap(
                MongoCredential.JAVA_SUBJECT_KEY, authenticatedSubject
        );

        MongoCredential credential = MongoCredential.createGSSAPICredential(
                // The principal must match the one used in the login module options.
                "[email protected]" 
        ).withMechanismProperties(properties);
        
        // Build MongoClientSettings
        MongoClientSettings settings = MongoClientSettings.builder()
                .credential(credential)
                .build();
        
        // Create MongoClient using the custom settings.
        try (MongoClient mongoClient = MongoClients.create(settings)) {
            System.out.println("Successfully connected to MongoDB with GSSAPI using programmatic configs.");
            mongoClient.listDatabaseNames().forEach(System.out::println);
        } catch (Exception e) {
            LOGGER.severe("MongoDB connection failed: " + e.getMessage());
        }
    }
}

logger.log(
Level.INFO, "Set sun.security.jgss.native = " + gssNative.trim());
}

X509Authentication x509Authentication = new X509Authentication(logger);
x509Authentication.configureX509Authentication(
settingsBuilder, pemPath, this.x509Passphrase);
settingsBuilder.credential(credential);
} else if (authMechanism.equals(MONGODB_X509)) {
String pemPath = connectionProperties.getX509PemPath();
if (pemPath == null || pemPath.isEmpty()) {
pemPath = System.getenv(MONGODB_JDBC_X509_CLIENT_CERT_PATH);
}
if (pemPath == null || pemPath.isEmpty()) {
throw new IllegalStateException(
"PEM file path is required for X.509 authentication but was not provided.");
}

X509Authentication x509Authentication = new X509Authentication(logger);
x509Authentication.configureX509Authentication(
settingsBuilder, pemPath, this.x509Passphrase);
}
}
}

Expand Down
18 changes: 16 additions & 2 deletions src/main/java/com/mongodb/jdbc/MongoConnectionProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class MongoConnectionProperties {
private String clientInfo;
private boolean extJsonMode;
private String x509PemPath;
private String jaasConfigPath;
private String gssNativeMode;

public MongoConnectionProperties(
ConnectionString connectionString,
Expand All @@ -36,14 +38,18 @@ public MongoConnectionProperties(
File logDir,
String clientInfo,
boolean extJsonMode,
String x509PemPath) {
String x509PemPath,
String jaasConfigPath,
String gssNativeMode) {
this.connectionString = connectionString;
this.database = database;
this.logLevel = logLevel;
this.logDir = logDir;
this.clientInfo = clientInfo;
this.extJsonMode = extJsonMode;
this.x509PemPath = x509PemPath;
this.x509PemPath = (x509PemPath != null) ? x509PemPath.trim() : null;
this.jaasConfigPath = (jaasConfigPath != null) ? jaasConfigPath.trim() : null;
this.gssNativeMode = gssNativeMode != null ? gssNativeMode : null;
}

public ConnectionString getConnectionString() {
Expand Down Expand Up @@ -74,6 +80,14 @@ public String getX509PemPath() {
return x509PemPath;
}

public String getJaasConfigPath() {
return jaasConfigPath;
}

public String getGssNativeMode() {
return gssNativeMode;
}

/*
* Generate a unique key for the connection properties. This key is used to identify the connection properties in the
* connection cache. Properties that do not differentiate a specific client such as the log level are not included in the key.
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/com/mongodb/jdbc/MongoDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ public enum MongoJDBCProperty {
LOG_DIR("logdir"),
EXT_JSON_MODE("extjsonmode"),
X509_PEM_PATH("x509pempath"),
DISABLE_CLIENT_CACHE("disableclientcache");
DISABLE_CLIENT_CACHE("disableclientcache"),
JAAS_CONFIG_PATH("jaasconfigpath"),
GSS_NATIVE_MODE("gssnativemode");

private final String propertyName;

Expand Down Expand Up @@ -486,6 +488,19 @@ private MongoConnection createConnection(
}
}

String gssNativeModeVal = info.getProperty(GSS_NATIVE_MODE.getPropertyName());
if (gssNativeModeVal != null) {
gssNativeModeVal = gssNativeModeVal.trim().toLowerCase();
if (!"true".equals(gssNativeModeVal) && !"false".equals(gssNativeModeVal)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to be less restrictive.

Suggested change
if (!"true".equals(gssNativeModeVal) && !"false".equals(gssNativeModeVal)) {
if (!"true".equalsIgnoreCase(gssNativeModeVal) && !"false".equalsIgnoreCase(gssNativeModeVal)) {

throw new SQLException(
"Invalid "
+ GSS_NATIVE_MODE.getPropertyName()
+ " property value: "
+ gssNativeModeVal
+ ". Valid values are: 'true', 'false'.");
}
}

MongoConnectionProperties mongoConnectionProperties =
new MongoConnectionProperties(
cs,
Expand All @@ -494,7 +509,9 @@ private MongoConnection createConnection(
logDir,
clientInfo,
extJsonMode,
info.getProperty(X509_PEM_PATH.getPropertyName()));
info.getProperty(X509_PEM_PATH.getPropertyName()),
info.getProperty(JAAS_CONFIG_PATH.getPropertyName()),
gssNativeModeVal);

String disableCacheVal =
info.getProperty(DISABLE_CLIENT_CACHE.getPropertyName(), "false").toLowerCase();
Expand Down Expand Up @@ -676,8 +693,10 @@ public static MongoConnectionConfig getConnectionSettings(String url, Properties
if (password == null && user != null) {
if (result.authMechanism == null
|| (!result.authMechanism.equals(MONGODB_X509)
&& !result.authMechanism.equals(MONGODB_OIDC))) {
// password is null, but user is not, we must prompt for the password.
&& !result.authMechanism.equals(MONGODB_OIDC)
&& !result.authMechanism.equals(GSSAPI))) {
// Password is null, but user is provided. If the authentication mechanism
// does not support password-less login, then the password must be prompted for.
mandatoryConnectionProperties.add(new DriverPropertyInfo(PASSWORD, null));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class MongoDatabaseMetaDataTest {
new MongoDatabaseMetaData(
new MongoConnection(
new MongoConnectionProperties(
uri, database, null, null, null, false, null)));
uri, database, null, null, null, false, null, null, null)));

// Report exception from MongoConnection
public MongoDatabaseMetaDataTest() throws Exception {}
Expand Down
73 changes: 73 additions & 0 deletions src/test/java/com/mongodb/jdbc/MongoDriverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -900,4 +900,77 @@ void testNullPropKey() throws Exception {
e.printStackTrace();
}
}

@Test
void testJaasConfigPathIsSetForGSSAPI() throws SQLException {
MongoDriver d = new MongoDriver();
Properties p = new Properties();
p.setProperty(DATABASE.getPropertyName(), "test");
String jaasPathWithSpaces = " /path/to/mongo/jaas.conf ";
String jaasPathTrimmed = jaasPathWithSpaces.trim();

p.setProperty(JAAS_CONFIG_PATH.getPropertyName(), jaasPathWithSpaces);

MongoConnection conn =
(MongoConnection)
d.getUnvalidatedConnection(userNoPWDURL + "?authMechanism=GSSAPI", p);
assertNotNull(conn);

// Verify system property was set
assertEquals(jaasPathTrimmed, System.getProperty("java.security.auth.login.config"));

System.clearProperty("java.security.auth.login.config");
}

@Test
void testGssNativeModeTrue_SetsSystemProperty() throws SQLException {
MongoDriver d = new MongoDriver();
Properties p = new Properties();
p.setProperty(DATABASE.getPropertyName(), "test");

String url = userNoPWDURL + "?authMechanism=GSSAPI";

p.setProperty(GSS_NATIVE_MODE.getPropertyName(), "true");

MongoConnection conn = (MongoConnection) d.getUnvalidatedConnection(url, p);
assertNotNull(conn);

// Verify system property was set to true
assertEquals("true", System.getProperty("sun.security.jgss.native"));

System.clearProperty("sun.security.jgss.native");
}

@Test
void testGssNativeModeFalse_SetsSystemProperty() throws SQLException {
MongoDriver d = new MongoDriver();
Properties p = new Properties();
p.setProperty(DATABASE.getPropertyName(), "test");

String url = userNoPWDURL + "?authMechanism=GSSAPI";
p.setProperty(GSS_NATIVE_MODE.getPropertyName(), "false");

MongoConnection conn = (MongoConnection) d.getUnvalidatedConnection(url, p);
assertNotNull(conn);

// Verify system property was set to false
assertEquals("false", System.getProperty("sun.security.jgss.native"));

System.clearProperty("sun.security.jgss.native");
}

@Test
void testGssNativeMode_InvalidValue_ThrowsSQLException() throws SQLException {
MongoDriver d = new MongoDriver();
Properties p = new Properties();
p.setProperty(DATABASE.getPropertyName(), "test");

String url = userNoPWDURL + "?authMechanism=GSSAPI";
p.setProperty(GSS_NATIVE_MODE.getPropertyName(), "invalid");

assertThrows(
SQLException.class,
() -> d.getUnvalidatedConnection(url, p),
"Invalid gssnativemode value should throw SQLException");
}
}
2 changes: 1 addition & 1 deletion src/test/java/com/mongodb/jdbc/MongoMock.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public abstract class MongoMock {
mongoConnection =
new MongoConnection(
new MongoConnectionProperties(
uri, database, null, null, null, false, null));
uri, database, null, null, null, false, null, null, null));
} catch (Exception e) {
// The connection initialization should not fail, but if it does, we log the error to have more info.
e.printStackTrace();
Expand Down