8000 Bound LDAP StartTLS shutdown with read timeout by atiqur-rahman-pro · Pull Request #53279 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
8000
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package org.keycloak.storage.ldap.idm.store.ldap;

import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import org.keycloak.models.KeycloakSession;
Expand All @@ -38,6 +40,8 @@ public final class LDAPContextManager implements AutoCloseable {
private final LDAPConfig ldapConfig;
private final Meter.MeterProvider<Timer> requestTimer;
private StartTlsResponse tlsResponse;
private final AtomicReference<SSLSocket> tlsSocket = new AtomicReference<>();
private final AtomicReference<Socket> tlsTransport = new AtomicReference<>();
private LdapContext ldapContext;

@Deprecated(forRemoval = true, since = "26.8")
Expand Down Expand Up @@ -106,7 +110,7 @@ private void createLdapContext() throws NamingException {
sslSocketFactory = provider.getSSLSocketFactory();
}

tlsResponse = startTLS(ldapContext, sslSocketFactory);
tlsResponse = startTLS(ldapContext, sslSocketFactory, tlsSocket, tlsTransport);

// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) {
Expand Down Expand Up @@ -143,11 +147,20 @@ private String getBindPassword() {
}

public static StartTlsResponse startTLS(LdapContext ldapContext, SSLSocketFactory sslSocketFactory) throws NamingException {
return startTLS(ldapContext, sslSocketFactory, null, null);
}

static StartTlsResponse startTLS(LdapContext ldapContext, SSLSocketFactory sslSocketFactory, AtomicReference<SSLSocket> tlsSocket, AtomicReference<Socket> tlsTransport) throws NamingException {
StartTlsResponse tls = null;

try {
tls = (StartTlsResponse) ldapContext.extendedOperation(new StartTlsRequest());
tls.negotiate(sslSocketFactory);
if (tlsSocket == null) {
tls.negotiate(sslSocketFactory);
} else {
SSLSocketFactory factory = sslSocketFactory == null ? (SSLSocketFactory) SSLSocketFactory.getDefault() : sslSocketFactory;
tls.negotiate(LDAPStartTlsClose.trackingFactory(factory, tlsSocket, tlsTransport));
}
} catch (Exception e) {
logger.error("Could not negotiate TLS", e);
NamingException ne = new AuthenticationException("Could not negotiate TLS");
Expand Down Expand Up @@ -294,19 +307,17 @@ public static Hashtable<Object, Object> getNonAuthConnectionProperties(LDAPConfi

@Override
public void close() {
if (tlsResponse != null) {
try {
tlsResponse.close();
} catch (IOException e) {
logger.error("Could not close Ldap tlsResponse.", e);
try {
if (tlsResponse != null) {
LDAPStartTlsClose.close(tlsResponse, tlsSocket.get(), tlsTransport.get());
}
}

if (ldapContext != null) {
try {
ldapContext.close();
} catch (NamingException e) {
logger.error("Could not close Ldap context.", e);
} finally {
if (ldapContext != null) {
try {
ldapContext.close();
} catch (NamingException e) {
logger.error("Could not close Ldap context.", e);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
package org.keycloak.storage.ldap.idm.store.ldap;

import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.naming.AuthenticationException;
import javax.naming.Binding;
import javax.naming.Context;
Expand All @@ -45,6 +47,7 @@
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import org.keycloak.common.util.Time;
Expand Down Expand Up @@ -533,6 +536,8 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce

LdapContext authCtx = null;
StartTlsResponse tlsResponse = null;
AtomicReference<SSLSocket> tlsSocket = new AtomicReference<>();
AtomicReference<Socket> tlsTransport = new AtomicReference<>();

var tracing = session.getProvider(TracingProvider.class);
tracing.startSpan(LDAPOperationManager.class, "authenticate");
Expand Down Expand Up @@ -562,7 +567,7 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce
sslSocketFactory = provider.getSSLSocketFactory();
}

tlsResponse = LDAPContextManager.startTLS(authCtx, sslSocketFactory);
tlsResponse = LDAPContextManager.startTLS(authCtx, sslSocketFactory, tlsSocket, tlsTransport);

// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) {
Expand Down Expand Up @@ -614,22 +619,23 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce
throw new AuthenticationException("Unexpected exception when validating password of user");
} finally {
recordLdapRequest("authenticate", success, startTimeNanos, errorName);
if (tlsResponse != null) {
try {
tlsResponse.close();
} catch (IOException e) {
e.printStackTrace();
try {
if (tlsResponse != null) {
LDAPStartTlsClose.close(tlsResponse, tlsSocket.get(), tlsTransport.get());
}
}

if (authCtx != null) {
} finally {
try {
authCtx.close();
} catch (NamingException e) {
e.printStackTrace();
if (authCtx != null) {
try {
authCtx.close();
} catch (NamingException e) {
e.printStackTrace();
}
}
} finally {
tracing.endSpan();
}
}
tracing.endSpan();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.storage.ldap.idm.store.ldap;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicReference;
import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import org.jboss.logging.Logger;

/**
* Tracks the TLS socket negotiated by JNDI and ensures StartTLS teardown does not block.
*
* <p>Closing the underlying transport socket before invoking {@link StartTlsResponse#close()}
* unblocks any JNDI reader thread that may be holding the TLS read lock, which would otherwise
* prevent {@code close()} from acquiring it and returning. {@code setSoTimeout} alone is
* insufficient because it does not interrupt a read already in progress.
*/
final class LDAPStartTlsClose {

private static final Logger logger = Logger.getLogger(LDAPStartTlsClose.class);

private LDAPStartTlsClose() {
}

static SSLSocketFactory trackingFactory(SSLSocketFactory delegate, AtomicReference<SSLSocket> socket, AtomicReference<Socket> transport) {
return new TrackingSSLSocketFactory(delegate, socket, transport);
}

static void close(StartTlsResponse tlsResponse, SSLSocket socket, Socket transport) {
if (tlsResponse == null) {
return;
}

// Close the underlying TCP transport first. This unblocks any JNDI reader thread
// that is blocked inside a TLS read and holds the lock that StartTlsResponse.close()
// needs to acquire. Without this, close() can block indefinitely regardless of
// any socket timeout set on the SSLSocket layer.
closeSocket(socket, transport);
Comment thread
Copilot marked this conversation as resolved.
Resolved

try {
tlsResponse.close();
} catch (IOException e) {
logger.debug("Could not close LDAP TLS response after transport was closed.", e);
}
}

private static void closeSocket(SSLSocket socket, Socket transport) {
try {
if (transport != null) {
transport.close();
} else if (socket != null) {
socket.close();
}
} catch (Exception e) {
logger.debug("Could not close LDAP TLS socket.", e);
}
}

private static final class TrackingSSLSocketFactory extends SSLSocketFactory {

private final SSLSocketFactory delegate;
private final AtomicReference<SSLSocket> socket;
private final AtomicReference<Socket> transport;

private TrackingSSLSocketFactory(SSLSocketFactory delegate, AtomicReference<SSLSocket> socket, AtomicReference<Socket> transport) {
this.delegate = delegate;
this.socket = socket;
this.transport = transport;
}

private Socket track(Socket created, Socket underlyingTransport) throws IOException {
if (!(created instanceof SSLSocket)) {
created.close();
throw new IOException("LDAP StartTLS socket factory did not create an SSLSocket");
}
socket.set((SSLSocket) created);
transport.set(underlyingTransport == null ? created : underlyingTransport);
return created;
}

@Override
public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}

@Override
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}

@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return track(delegate.createSocket(s, host, port, autoClose), s);
}

@Override
public Socket createSocket(String host, int port) throws IOException {
return track(delegate.createSocket(host, port), null);
}

@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return track(delegate.createSocket(host, port, localHost, localPort), null);
}

@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return track(delegate.createSocket(host, port), null);
}

@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return track(delegate.createSocket(address, port, localAddress, localPort), null);
}
}
}
Loading
Loading
0