Skip to content

Commit

Permalink
Refactored StartTLS with LDAP.
Browse files Browse the repository at this point in the history
This change refactors initialization of LDAP context into explicit separate
steps:

1. Initialize context with LDAP connection properties only
2. Optionally send StartTLS extended request
3. Send bind

Previously both connection and authentication properties were set together
which triggers implicit authentication in the JDK LDAP and it happened
differently depending if StartTLS was used or not.

This change moves bind into explicit context.reconnect() call, which also
makes is possible in future to send LDAP control messages with bind.
That is not possible with implicit bind.

Signed-off-by: Tero Saarni <tero.saarni@est.tech>
  • Loading branch information
tsaarni committed Dec 16, 2024
1 parent 01b7a8e commit 5b8a237
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,6 @@ public final class LDAPContextManager implements AutoCloseable {
private final KeycloakSession session;
private final LDAPConfig ldapConfig;
private StartTlsResponse tlsResponse;

private VaultStringSecret vaultStringSecret = new VaultStringSecret() {
@Override
public Optional<String> get() {
return Optional.empty();
}

@Override
public void close() {

}
};

private LdapContext ldapContext;

public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperties) {
Expand All @@ -65,33 +52,33 @@ public static LDAPContextManager create(KeycloakSession session, LDAPConfig conn
return new LDAPContextManager(session, connectionProperties);
}

// Create connection and authenticate as admin user.
private void createLdapContext() throws NamingException {
Hashtable<Object, Object> connProp = getConnectionProperties(ldapConfig);

if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())) {
vaultStringSecret = getVaultSecret();
// Create connection but avoid triggering automatic bind request by not setting security principal and credentials yet.
// That allows us to send optional StartTLS request before binding.
ldapContext = new InitialLdapContext(LDAPContextManager.getNonAuthConnectionProperties(ldapConfig), null);

if (vaultStringSecret != null && !ldapConfig.isStartTls() && ldapConfig.getBindCredential() != null) {
connProp.put(SECURITY_CREDENTIALS, vaultStringSecret.get()
.orElse(ldapConfig.getBindCredential()).toCharArray());
}
}

ldapContext = new InitialLdapContext(connProp, null);
// Send StartTLS request and setup SSL context if needed.
if (ldapConfig.isStartTls()) {
SSLSocketFactory sslSocketFactory = null;
if (LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
sslSocketFactory = LDAPSSLSocketFactory.getDefault();
}

tlsResponse = startTLS(ldapContext, ldapConfig.getAuthType(), ldapConfig.getBindDN(),
vaultStringSecret.get().orElse(ldapConfig.getBindCredential()), sslSocketFactory);
tlsResponse = startTLS(ldapContext, sslSocketFactory);

// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) {
throw new NamingException("Wasn't able to establish LDAP connection through StartTLS");
}
}

setAdminConnectionAuthProperties(ldapContext);
if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())) {
// Explicitly send bind with given credentials.
// Throws AuthenticationException when authentication fails.
ldapContext.reconnect(null);
}
}

public LdapContext getLdapContext() throws NamingException {
Expand All @@ -100,70 +87,52 @@ public LdapContext getLdapContext() throws NamingException {
return ldapContext;
}

private VaultStringSecret getVaultSecret() {
return LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())
? null
: session.vault().getStringSecret(ldapConfig.getBindCredential());
// Get bind password from vault or from directly from configuration, may be null.
private String getBindPassword() {
VaultStringSecret vaultSecret = session.vault().getStringSecret(ldapConfig.getBindCredential());
return vaultSecret.get().orElse(ldapConfig.getBindCredential());
}

public static StartTlsResponse startTLS(LdapContext ldapContext, String authType, String bindDN, String bindCredential, SSLSocketFactory sslSocketFactory) throws NamingException {
public static StartTlsResponse startTLS(LdapContext ldapContext, SSLSocketFactory sslSocketFactory) throws NamingException {
StartTlsResponse tls = null;

try {
tls = (StartTlsResponse) ldapContext.extendedOperation(new StartTlsRequest());
tls.negotiate(sslSocketFactory);

ldapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, authType);

if (!LDAPConstants.AUTH_TYPE_NONE.equals(authType)) {
ldapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, bindDN);
ldapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, bindCredential != null ? bindCredential.toCharArray() : null);
}
} catch (Exception e) {
logger.error("Could not negotiate TLS", e);
NamingException ne = new AuthenticationException("Could not negotiate TLS");
ne.setRootCause(e);
throw ne;
}

// throws AuthenticationException when authentication fails
ldapContext.lookup("");

return tls;
}

// Get connection properties of admin connection
private Hashtable<Object, Object> getConnectionProperties(LDAPConfig ldapConfig) {
Hashtable<Object, Object> env = getNonAuthConnectionProperties(ldapConfig);

if(!ldapConfig.isStartTls()) {
String authType = ldapConfig.getAuthType();

if (authType != null) env.put(Context.SECURITY_AUTHENTICATION, authType);

String bindDN = ldapConfig.getBindDN();

char[] bindCredential = null;
// Fill in the connection properties for admin connection
private void setAdminConnectionAuthProperties(LdapContext ldapContext) throws NamingException {
String authType = ldapConfig.getAuthType();
if (authType != null) {
ldapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, authType);
}

if (ldapConfig.getBindCredential() != null) {
bindCredential = ldapConfig.getBindCredential().toCharArray();
}
String bindPassword = getBindPassword();
if (bindPassword != null) {
ldapContext.addToEnvironment(SECURITY_CREDENTIALS, bindPassword);
}

if (!LDAPConstants.AUTH_TYPE_NONE.equals(authType)) {
if (bindDN != null) env.put(Context.SECURITY_PRINCIPAL, bindDN);
if (bindCredential != null) env.put(Context.SECURITY_CREDENTIALS, bindCredential);
}
String bindDN = ldapConfig.getBindDN();
if (bindDN != null) {
ldapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, bindDN);
}

if (logger.isDebugEnabled()) {
Map<Object, Object> copyEnv = new Hashtable<>(env);
Map<Object, Object> copyEnv = new Hashtable<>(ldapContext.getEnvironment());
if (copyEnv.containsKey(Context.SECURITY_CREDENTIALS)) {
copyEnv.put(Context.SECURITY_CREDENTIALS, "**************************************");
}
logger.debugf("Creating LdapContext using properties: [%s]", copyEnv);
}

return env;
}


Expand Down Expand Up @@ -243,7 +212,6 @@ public static Hashtable<Object, Object> getNonAuthConnectionProperties(LDAPConfi

@Override
public void close() {
if (vaultStringSecret != null) vaultStringSecret.close();
if (tlsResponse != null) {
try {
tlsResponse.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,11 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce
// Never use connection pool to prevent password caching
env.put("com.sun.jndi.ldap.connect.pool", "false");

if(!this.config.isStartTls()) {
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, dn.toString());
env.put(Context.SECURITY_CREDENTIALS, password);
}

// Create connection but avoid triggering automatic bind request by not setting security principal and credentials yet.
// That allows us to send optional StartTLS request before binding.
authCtx = new InitialLdapContext(env, null);

// Send StartTLS request and setup SSL context if needed.
if (config.isStartTls()) {
SSLSocketFactory sslSocketFactory = null;
if (LDAPUtil.shouldUseTruststoreSpi(config)) {
Expand All @@ -508,13 +506,22 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce
sslSocketFactory = LDAPSSLSocketFactory.getDefault();
}

tlsResponse = LDAPContextManager.startTLS(authCtx, "simple", dn.toString(), password, sslSocketFactory);
tlsResponse = LDAPContextManager.startTLS(authCtx, sslSocketFactory);

// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) {
throw new AuthenticationException("Null TLS Response returned from the authentication");
}
}

// Configure given credentials.
authCtx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
authCtx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn.toString());
authCtx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);

// Send bind request. Throws AuthenticationException when authentication fails.
authCtx.reconnect(null);

} catch (AuthenticationException ae) {
if (logger.isDebugEnabled()) {
logger.debugf(ae, "Authentication failed for DN [%s]", dn);
Expand Down

0 comments on commit 5b8a237

Please sign in to comment.