Skip to content

Commit

Permalink
Add support for secure LDAP (StartTLS, LDAPS)
Browse files Browse the repository at this point in the history
- Add boolean setting `dbms.security.realms.ldap.use_starttls` to use
opportunistic TLS (upgrading an initially insecure connection to TLS)
- Change setting `dbms.security.realms.ldap.host` to string to support
specifying the protocol
  • Loading branch information
henriknyman committed Aug 31, 2016
1 parent 61cf94c commit 1b74de1
Show file tree
Hide file tree
Showing 4 changed files with 426 additions and 14 deletions.
Expand Up @@ -53,7 +53,10 @@
import javax.naming.directory.Attributes; import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult; import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext; import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;


import org.neo4j.kernel.api.security.AuthenticationResult; import org.neo4j.kernel.api.security.AuthenticationResult;
import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.configuration.Config;
Expand All @@ -71,6 +74,7 @@ public class LdapRealm extends JndiLdapRealm


private Boolean authenticationEnabled; private Boolean authenticationEnabled;
private Boolean authorizationEnabled; private Boolean authorizationEnabled;
private Boolean useStartTls;
private String userSearchBase; private String userSearchBase;
private String userSearchFilter; private String userSearchFilter;
private List<String> membershipAttributeNames; private List<String> membershipAttributeNames;
Expand Down Expand Up @@ -98,7 +102,72 @@ protected AuthenticationInfo queryForAuthenticationInfo( AuthenticationToken tok
LdapContextFactory ldapContextFactory ) LdapContextFactory ldapContextFactory )
throws NamingException throws NamingException
{ {
return authenticationEnabled ? super.queryForAuthenticationInfo( token, ldapContextFactory ) : null; if ( authenticationEnabled )
{
JndiLdapContextFactory jndiLdapContextFactory = (JndiLdapContextFactory) ldapContextFactory;
// TODO: Maybe change this to security event log once we have it
log.debug( "Authenticating user '%s' against LDAP server '%s'%s", token.getPrincipal(),
jndiLdapContextFactory.getUrl(),
useStartTls ? " using StartTLS" : "" );
try
{
return useStartTls ? queryForAuthenticationInfoUsingStartTls( token, ldapContextFactory ) :
super.queryForAuthenticationInfo( token, ldapContextFactory );
}
catch ( Exception e )
{
// TODO: Maybe change this to security event log once we have it
log.debug( "Authentication exception: [%s] %s", e.getClass().getName(), e.getMessage() );
throw e;
}
}
else
{
return null;
}
}

protected AuthenticationInfo queryForAuthenticationInfoUsingStartTls( AuthenticationToken token,
LdapContextFactory ldapContextFactory ) throws NamingException
{
JndiLdapContextFactory jndiLdapContextFactory = (JndiLdapContextFactory) ldapContextFactory;
Object principal = token.getPrincipal();
Object credentials = token.getCredentials();

principal = getLdapPrincipal(token);

LdapContext ctx = null;
Hashtable<String, Object> env = new Hashtable<>();
env.put( Context.INITIAL_CONTEXT_FACTORY, jndiLdapContextFactory.getContextFactoryClassName() );
env.put( Context.PROVIDER_URL, jndiLdapContextFactory.getUrl() );

try {
ctx = new InitialLdapContext( env, null );

StartTlsRequest startTlsRequest = new StartTlsRequest();
StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation( startTlsRequest );
try
{
tls.negotiate();
}
catch ( IOException e )
{
log.error( "Failed to negotiate TLS connection", e );
throw new CommunicationException( e.getMessage() );
}

ctx.addToEnvironment( Context.SECURITY_AUTHENTICATION, ((JndiLdapContextFactory) ldapContextFactory).getAuthenticationMechanism() );
ctx.addToEnvironment( Context.SECURITY_PRINCIPAL, principal );
ctx.addToEnvironment( Context.SECURITY_CREDENTIALS, credentials );

ctx.reconnect( ctx.getConnectControls() );

return createAuthenticationInfo(token, principal, credentials, ctx);
}
finally
{
LdapUtils.closeContext( ctx );
}
} }


@Override @Override
Expand Down Expand Up @@ -148,6 +217,8 @@ protected AuthenticationInfo createAuthenticationInfo( AuthenticationToken token
throws NamingException throws NamingException
{ {
// NOTE: This will be called only if authentication with the ldap context was successful // NOTE: This will be called only if authentication with the ldap context was successful
// TODO: Change this to security event log once we have it
log.debug( "Successfully authenticated user '%s' through LDAP", token.getPrincipal() );


// If authorization is enabled but useSystemAccountForAuthorization is disabled, we should perform // If authorization is enabled but useSystemAccountForAuthorization is disabled, we should perform
// the search for groups directly here while the user's authenticated ldap context is open. // the search for groups directly here while the user's authenticated ldap context is open.
Expand Down Expand Up @@ -199,7 +270,7 @@ public Collection<Permission> resolvePermissionsInRole( String roleString )
private void configureRealm( Config config ) private void configureRealm( Config config )
{ {
JndiLdapContextFactory contextFactory = new JndiLdapContextFactory(); JndiLdapContextFactory contextFactory = new JndiLdapContextFactory();
contextFactory.setUrl( "ldap://" + config.get( SecuritySettings.ldap_server ) ); contextFactory.setUrl( parseLdapServerUrl( config.get( SecuritySettings.ldap_server ) ) );
contextFactory.setAuthenticationMechanism( config.get( SecuritySettings.ldap_auth_mechanism ) ); contextFactory.setAuthenticationMechanism( config.get( SecuritySettings.ldap_auth_mechanism ) );
contextFactory.setReferral( config.get( SecuritySettings.ldap_referral ) ); contextFactory.setReferral( config.get( SecuritySettings.ldap_referral ) );
contextFactory.setSystemUsername( config.get( SecuritySettings.ldap_system_username ) ); contextFactory.setSystemUsername( config.get( SecuritySettings.ldap_system_username ) );
Expand All @@ -215,6 +286,7 @@ private void configureRealm( Config config )


authenticationEnabled = config.get( SecuritySettings.ldap_authentication_enabled ); authenticationEnabled = config.get( SecuritySettings.ldap_authentication_enabled );
authorizationEnabled = config.get( SecuritySettings.ldap_authorization_enabled ); authorizationEnabled = config.get( SecuritySettings.ldap_authorization_enabled );
useStartTls = config.get( SecuritySettings.ldap_use_starttls );


userSearchBase = config.get( SecuritySettings.ldap_authorization_user_search_base ); userSearchBase = config.get( SecuritySettings.ldap_authorization_user_search_base );
userSearchFilter = config.get( SecuritySettings.ldap_authorization_user_search_filter ); userSearchFilter = config.get( SecuritySettings.ldap_authorization_user_search_filter );
Expand All @@ -224,6 +296,11 @@ private void configureRealm( Config config )
parseGroupToRoleMapping( config.get( SecuritySettings.ldap_authorization_group_to_role_mapping ) ); parseGroupToRoleMapping( config.get( SecuritySettings.ldap_authorization_group_to_role_mapping ) );
} }


private String parseLdapServerUrl( String rawLdapServer )
{
return rawLdapServer.contains( "://" ) ? rawLdapServer : "ldap://" + rawLdapServer;
}

Map<String,Collection<String>> parseGroupToRoleMapping( String groupToRoleMappingString ) Map<String,Collection<String>> parseGroupToRoleMapping( String groupToRoleMappingString )
{ {
Map<String,Collection<String>> map = new HashMap<>(); Map<String,Collection<String>> map = new HashMap<>();
Expand Down
Expand Up @@ -65,8 +65,14 @@ public class SecuritySettings
setting( "dbms.security.realms.plugin.authorization_enabled", BOOLEAN, "false" ); setting( "dbms.security.realms.plugin.authorization_enabled", BOOLEAN, "false" );


@Description( "Hostname and port of LDAP server to use for authentication and authorization." ) @Description( "Hostname and port of LDAP server to use for authentication and authorization." )
public static final Setting<HostnamePort> ldap_server = public static final Setting<String> ldap_server =
setting( "dbms.security.realms.ldap.host", HOSTNAME_PORT, "0.0.0.0:389" ); setting( "dbms.security.realms.ldap.host", STRING, "0.0.0.0:389" );

@Description( "Use secure communication with the LDAP server using opportunistic TLS. " +
"First an initial insecure connection will be made with the LDAP server and a STARTTLS command will be " +
"issued to negotiate an upgrade of the connection to TLS before initiating authentication." )
public static final Setting<Boolean> ldap_use_starttls =
setting( "dbms.security.realms.ldap.use_starttls", BOOLEAN, "false" );


@Description( "LDAP authentication mechanism. This is one of `simple` or a SASL mechanism supported by JNDI, " + @Description( "LDAP authentication mechanism. This is one of `simple` or a SASL mechanism supported by JNDI, " +
"e.g. `DIGEST-MD5`. `simple` is basic username" + "e.g. `DIGEST-MD5`. `simple` is basic username" +
Expand Down

0 comments on commit 1b74de1

Please sign in to comment.