Thanks to visit codestin.com
Credit goes to github.com

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
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,8 @@ public static void testLDAP(TestLdapConnectionRepresentation config, KeycloakSes
// is not needed anymore
try (LDAPContextManager ldapContextManager = LDAPContextManager.create(session, ldapConfig)) {
LdapContext ldapContext = ldapContextManager.getLdapContext();
if (TEST_AUTHENTICATION.equals(config.getAction()) && LDAPConstants.AUTH_TYPE_NONE.equals(config.getAuthType())) {
// reconnect to force an anonymous bind operation
if (TEST_AUTHENTICATION.equals(config.getAction())) {
// Reconnect to force bind operation.
ldapContext.reconnect(null);
}
} catch (Exception ne) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ public String getReferral() {
return config.getFirst(LDAPConstants.REFERRAL);
}

public boolean isEnableLdapPasswordPolicy() {
String enableLdapPasswordPolicy = config.getFirst(LDAPConstants.ENABLE_LDAP_PASSWORD_POLICY);
return Boolean.parseBoolean(enableLdapPasswordPolicy);
}

public void addBinaryAttribute(String attrName) {
binaryAttributeNames.add(attrName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.idm.store.ldap.control.PasswordPolicyPasswordChangeException;
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
Expand Down Expand Up @@ -822,6 +823,22 @@ public boolean validPassword(RealmModel realm, UserModel user, String password)
try {
ldapIdentityStore.validatePassword(ldapUser, password);
return true;
} catch (PasswordPolicyPasswordChangeException e) {
// LDAP password policy requires a forced password change.
// Check for (1) import enabled, so that we can persist required actions and
// (2) edit mode writable, so that user can modify LDAP password.
if (!model.isImportEnabled() || editMode != EditMode.WRITABLE) {
logger.debugf("User '%s' in realm '%s' is forced to change password but UPDATE_PASSWORD cannot be set: import not enabled or edit mode not writable. Failing login.", user.getUsername(), realm.getName());
return false;
}
if (user.getRequiredActionsStream()
.noneMatch(action -> Objects.equals(action, UserModel.RequiredAction.UPDATE_PASSWORD.name()))) {
logger.debugf("Adding requiredAction UPDATE_PASSWORD to user %s", user.getUsername());
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
} else {
logger.tracef("Skip adding required action UPDATE_PASSWORD. It was already set on user '%s' in realm '%s'", user.getUsername(), realm.getName());
}
return true;
} catch (AuthenticationException ae) {
AtomicReference<Boolean> processed = new AtomicReference<>(false);
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false")
.add()
.property().name(LDAPConstants.ENABLE_LDAP_PASSWORD_POLICY)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false")
.add()
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright 2024 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.nio.BufferUnderflowException;
import java.nio.ByteBuffer;


/**
* A decoder for the ASN.1 BER encoding.
*
* Very limited implementation, only supports what is needed by the current LDAP extension controls.
*/
public class BERDecoder {
// Universal tags.
public static final int TAG_SEQUENCE = 0x30;

// Tag classes.
public static final int TAG_CLASS_CONTEXT_SPECIFIC = 0x80;

// Tag forms.
public static final int TAG_FORM_PRIMITIVE = 0x00;

private ByteBuffer encoded;

public BERDecoder(byte[] encodedValue) {
this.encoded = ByteBuffer.wrap(encodedValue);
}

/**
* Start decoding a sequence.
*/
public void startSequence() throws DecodeException {
try {
byte tag = encoded.get();
if (tag != TAG_SEQUENCE) {
throw new DecodeException("Expected SEQUENCE (" + TAG_SEQUENCE + ") but got " + tag);
}
readLength();
} catch (BufferUnderflowException e) {
throw new DecodeException("Unexpected end of input");
}
}

/**
* Check if the next element matches with the given tag, but do not consume it.
*/
public boolean isNextTag(int clazz, int form, int tag) throws DecodeException {
encoded.mark();
try {
int expected = clazz | form | tag;
int unsignedTag = encoded.get() & 0xFF;
encoded.reset();
return unsignedTag == expected;
} catch (BufferUnderflowException e) {
throw new DecodeException("Unexpected end of input");
} finally {
encoded.reset();
}
}

/**
* Skip over the next element.
*/
public void skipElement() throws DecodeException {
try {
int length = readLength();
encoded.position(encoded.position() + length);
} catch (BufferUnderflowException e) {
throw new DecodeException("Unexpected end of input");
}
}

/**
* Drain the value bytes of the next element.
*/
public byte[] drainElementValue() throws DecodeException {
try {
int length = readLength();
byte[] value = new byte[length];
encoded.get(value);
return value;
} catch (BufferUnderflowException e) {
throw new DecodeException("Unexpected end of input");
}
}

private int readLength() throws DecodeException {
int length = encoded.get() & 0xFF;

// Short form.
if ((length & 0x80) == 0) {
return length;
}

// Long form.
int numBytes = length & 0x7F;
if (numBytes > 4) {
throw new DecodeException("Cannot handle more than 4 bytes of length, got " + numBytes + " bytes");
}

length = 0;
for (int i = 0; i < numBytes; i++) {
length = (length << 8) | (encoded.get() & 0xFF);
}

return length;
}

public static final class DecodeException extends IOException {
DecodeException(String message) {
super(message);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import javax.naming.AuthenticationException;
import javax.naming.Context;
Expand Down Expand Up @@ -35,19 +34,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 @@ -59,35 +45,30 @@ public static LDAPContextManager create(KeycloakSession session, LDAPConfig conn
return new LDAPContextManager(session, connectionProperties);
}

// Create connection that is authenticated as admin user.
private void createLdapContext() throws NamingException {
var tracing = session.getProvider(TracingProvider.class);
tracing.startSpan(LDAPContextManager.class, "createLdapContext");
try {
Hashtable<Object, Object> connProp = getConnectionProperties(ldapConfig);

if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())) {
vaultStringSecret = getVaultSecret();

if (vaultStringSecret != null && !ldapConfig.isStartTls() && ldapConfig.getBindCredential() != null) {
connProp.put(SECURITY_CREDENTIALS, vaultStringSecret.get()
.orElse(ldapConfig.getBindCredential()).toCharArray());
}
}
// Create the LDAP context without setting the security principal and credentials yet.
// This avoids triggering an automatic bind request, allowing us to send an optional StartTLS request before binding.
Hashtable<Object, Object> connProp = getNonAuthConnectionProperties(ldapConfig);

if (ldapConfig.isConnectionTrace()) {
connProp.put(LDAPConstants.CONNECTION_TRACE_BER, System.err);
}

ldapContext = new SessionBoundInitialLdapContext(session, connProp, null);

// Send StartTLS request and setup SSL context if needed.
if (ldapConfig.isStartTls()) {
SSLSocketFactory sslSocketFactory = null;
if (LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
sslSocketFactory = provider.getSSLSocketFactory();
}

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) {
Expand All @@ -100,6 +81,11 @@ private void createLdapContext() throws NamingException {
} finally {
tracing.endSpan();
}

setAdminConnectionAuthProperties(ldapContext);

// Bind will be automatically called when operations are executed on the context,
// or it can be explicitly called by invoking the reconnect() method (e.g., authentication test in LDAPServerCapabilitiesManager.testLDAP()).
}

public LdapContext getLdapContext() throws NamingException {
Expand All @@ -108,70 +94,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 to authenticate as admin.
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 @@ -251,7 +219,6 @@ public static Hashtable<Object, Object> getNonAuthConnectionProperties(LDAPConfi

@Override
public void close() {
if (vaultStringSecret != null) vaultStringSecret.close();
Copy link
Contributor

Choose a reason for hiding this comment

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

I see calling close is basically setting the secret to null but I'm not sure if we want to remove calling it for the sake of respecting the expectations around vault secrets where we might want to clean-up resources for whatever reason.

Copy link
Contributor Author

@tsaarni tsaarni Jun 10, 2024

Choose a reason for hiding this comment

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

I'm suggesting not having vaultStringSecret as a member variable at all, therefore removal of close(). Though, in case of vaultStringSecret.close() was overwritten as an empty method, so I think it had no effect.

I'm suggesting removal of vaultStringSecret member since Optional complicated finding the bind password unnecessarily in several places. I'm suggesting having new method that handles that in single place and has the vault secret as a local variable:

    // 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());
    }

if (tlsResponse != null) {
try {
tlsResponse.close();
Expand Down
Loading
Loading