Skip to content

Commit

Permalink
Merge pull request gitblit-org#1160 from fzs/sshLdapAuthenticator
Browse files Browse the repository at this point in the history
LDAP SSH key manager
  • Loading branch information
fzs committed Dec 18, 2016
2 parents 34b98a0 + 1afeccc commit d6ddafd
Show file tree
Hide file tree
Showing 14 changed files with 2,276 additions and 625 deletions.
16 changes: 16 additions & 0 deletions src/main/distrib/data/defaults.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,22 @@ realm.ldap.email = email
# SINCE 1.0.0
realm.ldap.uid = uid

# Attribute on the USER record that indicates their public SSH key.
# Leave blank when public SSH keys shall not be retrieved from LDAP.
#
# This setting is only relevant when a public key manager is used that
# retrieves SSH keys from LDAP (e.g. com.gitblit.transport.ssh.LdapKeyManager).
#
# The accepted format of the value is dependent on the public key manager used.
# Examples:
# sshPublicKey - Use the attribute 'sshPublicKey' on the user record.
# altSecurityIdentities:SshKey - Use the attribute 'altSecurityIdentities'
# on the user record, for which the record value
# starts with 'SshKey:', followed by the SSH key entry.
#
# SINCE 1.9.0
realm.ldap.sshPublicKey =

# Defines whether to synchronize all LDAP users and teams into the user service
# This requires either anonymous LDAP access or that a specific account is set
# in realm.ldap.username and realm.ldap.password, that has permission to read
Expand Down
284 changes: 10 additions & 274 deletions src/main/java/com/gitblit/auth/LdapAuthProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
*/
package com.gitblit.auth;

import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
Expand All @@ -33,28 +30,20 @@
import com.gitblit.Constants.Role;
import com.gitblit.Keys;
import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
import com.gitblit.ldap.LdapConnection;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.service.LdapSyncService;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
import com.unboundid.util.ssl.SSLUtil;
import com.unboundid.util.ssl.TrustAllTrustManager;

/**
* Implementation of an LDAP user service.
Expand Down Expand Up @@ -109,7 +98,7 @@ public synchronized void sync() {
if (enabled) {
logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
LdapConnection ldapConnection = new LdapConnection();
LdapConnection ldapConnection = new LdapConnection(settings);
if (ldapConnection.connect()) {
if (ldapConnection.bind() == null) {
ldapConnection.close();
Expand All @@ -118,9 +107,9 @@ public synchronized void sync() {
}

try {
String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
String accountBase = ldapConnection.getAccountBase();
String accountPattern = ldapConnection.getAccountPattern();
accountPattern = StringUtils.replace(accountPattern, "${username}", "*");

SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
Expand Down Expand Up @@ -265,7 +254,7 @@ public AccountType getAccountType() {
public UserModel authenticate(String username, char[] password) {
String simpleUsername = getSimpleUsername(username);

LdapConnection ldapConnection = new LdapConnection();
LdapConnection ldapConnection = new LdapConnection(settings);
if (ldapConnection.connect()) {

// Try to bind either to the "manager" account,
Expand All @@ -286,11 +275,7 @@ public UserModel authenticate(String username, char[] password) {

try {
// Find the logging in user's DN
String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));

SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
SearchResult result = ldapConnection.searchUser(simpleUsername);
if (result != null && result.getEntryCount() == 1) {
SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
String loggingInUserDN = loggingInUser.getDN();
Expand Down Expand Up @@ -441,12 +426,12 @@ private void getTeamsFromLdap(LdapConnection ldapConnection, String simpleUserna
String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");

groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", LdapConnection.escapeLDAPSearchFilter(loggingInUserDN));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", LdapConnection.escapeLDAPSearchFilter(simpleUsername));

// Fill in attributes into groupMemberPattern
for (Attribute userAttribute : loggingInUser.getAttributes()) {
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", LdapConnection.escapeLDAPSearchFilter(userAttribute.getValue()));
}

SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
Expand Down Expand Up @@ -538,6 +523,7 @@ private SearchResult doSearch(LdapConnection ldapConnection, String base, String




/**
* Returns a simple username without any domain prefixes.
*
Expand All @@ -553,34 +539,6 @@ protected String getSimpleUsername(String username) {
return username;
}

// From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private static final String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}

private void configureSyncService() {
LdapSyncService ldapSyncService = new LdapSyncService(settings, this);
if (ldapSyncService.isReady()) {
Expand All @@ -593,226 +551,4 @@ private void configureSyncService() {
logger.info("Ldap sync service is disabled.");
}
}



private class LdapConnection {
private LDAPConnection conn;
private SimpleBindRequest currentBindRequest;
private SimpleBindRequest managerBindRequest;
private SimpleBindRequest userBindRequest;


public LdapConnection() {
String bindUserName = settings.getString(Keys.realm.ldap.username, "");
String bindPassword = settings.getString(Keys.realm.ldap.password, "");
if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
this.managerBindRequest = new SimpleBindRequest();
}
this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
}


boolean connect() {
try {
URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
String ldapHost = ldapUrl.getHost();
int ldapPort = ldapUrl.getPort();

if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
// SSL
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
if (ldapPort == -1) {
ldapPort = 636;
}
} else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
// no encryption or StartTLS
conn = new LDAPConnection();
if (ldapPort == -1) {
ldapPort = 389;
}
} else {
logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
return false;
}

conn.connect(ldapHost, ldapPort);

if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
ExtendedResult extendedResult = conn.processExtendedOperation(
new StartTLSExtendedRequest(sslUtil.createSSLContext()));
if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
throw new LDAPException(extendedResult.getResultCode());
}
}

return true;

} catch (URISyntaxException e) {
logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
} catch (GeneralSecurityException e) {
logger.error("Unable to create SSL Connection", e);
} catch (LDAPException e) {
logger.error("Error Connecting to LDAP", e);
}

return false;
}


void close() {
if (conn != null) {
conn.close();
}
}


SearchResult search(SearchRequest request) {
try {
return conn.search(request);
} catch (LDAPSearchException e) {
logger.error("Problem Searching LDAP [{}]", e.getResultCode());
return e.getSearchResult();
}
}


SearchResult search(String base, boolean dereferenceAliases, String filter, List<String> attributes) {
try {
SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
if (dereferenceAliases) {
searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
}
if (attributes != null) {
searchRequest.setAttributes(attributes);
}
SearchResult result = search(searchRequest);
return result;

} catch (LDAPException e) {
logger.error("Problem creating LDAP search", e);
return null;
}
}



/**
* Bind using the manager credentials set in realm.ldap.username and ..password
* @return A bind result, or null if binding failed.
*/
BindResult bind() {
BindResult result = null;
try {
result = conn.bind(managerBindRequest);
currentBindRequest = managerBindRequest;
} catch (LDAPException e) {
logger.error("Error authenticating to LDAP with manager account to search the directory.");
logger.error(" Please check your settings for realm.ldap.username and realm.ldap.password.");
logger.debug(" Received exception when binding to LDAP", e);
return null;
}
return result;
}


/**
* Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
* create the DN.
* @return A bind result, or null if binding failed.
*/
BindResult bind(String bindPattern, String simpleUsername, String password) {
BindResult result = null;
try {
String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
result = conn.bind(request);
userBindRequest = request;
currentBindRequest = userBindRequest;
} catch (LDAPException e) {
logger.error("Error authenticating to LDAP with user account to search the directory.");
logger.error(" Please check your settings for realm.ldap.bindpattern.");
logger.debug(" Received exception when binding to LDAP", e);
return null;
}
return result;
}


boolean rebindAsUser() {
if (userBindRequest == null || currentBindRequest == userBindRequest) {
return false;
}
try {
conn.bind(userBindRequest);
currentBindRequest = userBindRequest;
} catch (LDAPException e) {
conn.close();
logger.error("Error rebinding to LDAP with user account.", e);
return false;
}
return true;
}


boolean isAuthenticated(String userDn, String password) {
verifyCurrentBinding();

// If the currently bound DN is already the DN of the logging in user, authentication has already happened
// during the previous bind operation. We accept this and return with the current bind left in place.
// This could also be changed to always retry binding as the logging in user, to make sure that the
// connection binding has not been tampered with in between. So far I see no way how this could happen
// and thus skip the repeated binding.
// This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
// when searching the user entry.
String boundDN = currentBindRequest.getBindDN();
if (boundDN != null && boundDN.equals(userDn)) {
return true;
}

// Bind a the logging in user to check for authentication.
// Afterwards, bind as the original bound DN again, to restore the previous authorization.
boolean isAuthenticated = false;
try {
// Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
conn.bind(ubr);
isAuthenticated = true;
userBindRequest = ubr;
} catch (LDAPException e) {
logger.error("Error authenticating user ({})", userDn, e);
}

try {
conn.bind(currentBindRequest);
} catch (LDAPException e) {
logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
e.getResultCode(), e);
}
return isAuthenticated;
}



private boolean verifyCurrentBinding() {
BindRequest lastBind = conn.getLastBindRequest();
if (lastBind == currentBindRequest) {
return true;
}
logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);

String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
String boundDN = currentBindRequest.getBindDN();
logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
logger.warn("Updated binding information in LDAP connection.");
currentBindRequest = (SimpleBindRequest)lastBind;
return false;
}
return true;
}
}
}
Loading

0 comments on commit d6ddafd

Please sign in to comment.