I am trying to add a secondary ldap contextSource to spring security in a split domain environment and I seem to be coming up short. I realize similar questions have been asked before but this is for two separate domains logging into the same application.
My first step was to add the secondary context source to my security-config xml file like so:
<beans:bean id="secondaryContextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<beans:constructor-arg value="ldap://<ldap address>:389/DC=example,DC=com"/>
<beans:property name="userDn" value="CN=BindAccount,CN=Users,DC=example,DC=com" />
<beans:property name="password" value="examplepw" />
</beans:bean>
In addition I added the constructor-arg to the ldapAuthenticationProvider and replaced the BindAuthenticator with my custom class like so:
<beans:bean id="ldapAuthenticationProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
<beans:constructor-arg ref="contextSource" />
<beans:constructor-arg ref="secondaryContextSource" />
<beans:property name="userSearch">
<beans:bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<beans:constructor-arg index="0" value="CN=Users"/>
<beans:constructor-arg index="1" value="(userPrincipalName={0})"/>
<beans:constructor-arg index="2" ref="contextSource" />
</beans:bean>
</beans:property>
</beans:bean>
</beans:constructor-arg>
<beans:property name="userDetailsContextMapper">
<beans:bean id="employeeServiceFacade" class="com.example.service.security.EmployeeServiceFacade" />
</beans:property>
<beans:constructor-arg>
<beans:bean class="com.example.web.security.CustomLdapAuthoritiesPopulator" />
</beans:constructor-arg>
</beans:bean>
Then I attempted to extend BindAuthenticator to accept and set a secondary context source in the constructor. Initially I couldn't get this to work so I completely rewrote the BindAuthenticator class and extended AbstractLdapAuthenticator to get around BindAuthenticator. Then I overrode the authenticate method to check if the username contained the secondary DN and if it did, I would call bindWithDn again to attempt to rebind to the secondary domain. This is where I think I am going about this all wrong because when it attempts to get the new Dn it fails. Basically it states that it could not bind to the domain. (I have triple checked the domain settings and have connected to it with a ldap administration console and took those settings for my app) Here is my extended BindAuthenticator
public class ExtendedBindAuthenticator extends AbstractLdapAuthenticator {
private BaseLdapPathContextSource secondaryContextSource;
public void setSecondContextSource(BaseLdapPathContextSource secondContextSource) {
this.secondaryContextSource = secondaryContextSource;
}
public ExtendedBindAuthenticator(BaseLdapPathContextSource contextSource, BaseLdapPathContextSource secondContextSource) {
super(contextSource);
this.secondaryContextSource = secondaryContextSource;
}
public DirContextOperations authenticate(Authentication authentication) {
DirContextOperations user = null;
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
"Can only process UsernamePasswordAuthenticationToken objects");
String username = authentication.getName();
String password = (String)authentication.getCredentials();
if(username.contains("secondDomain")) {
DirContextOperations secondaryUser = getUserSearch().searchForUser(username);
this.bindWithDn(secondaryUser.getDn().toString(), username, password);
}
if (!StringUtils.hasLength(password)) {
throw new BadCredentialsException(messages.getMessage("BindAuthenticator.emptyPassword",
"Empty Password"));
}
// If DN patterns are configured, try authenticating with them directly
for (String dn : getUserDns(username)) {
user = this.bindWithDn(dn, username, password);
if (user != null) {
break;
}
}
// Otherwise use the configured search object to find the user and authenticate with the returned DN.
if (user == null && getUserSearch() != null) {
DirContextOperations userFromSearch = getUserSearch().searchForUser(username);
user = bindWithDn(userFromSearch.getDn().toString(), username, password);
}
if (user == null) {
throw new BadCredentialsException(
messages.getMessage("BindAuthenticator.badCredentials", "Bad credentials"));
}
return user;
}
private DirContextOperations bindWithDn(String userDnStr, String username, String password) {
BaseLdapPathContextSource ctxSource = (BaseLdapPathContextSource) getContextSource();
if(username.contains("secondDomain")) {
ctxSource = secondaryContextSource;
}
DistinguishedName userDn = new DistinguishedName(userDnStr);
DistinguishedName fullDn = new DistinguishedName(userDn);
fullDn.prepend(ctxSource.getBaseLdapPath());
DirContext ctx = null;
try {
ctx = getContextSource().getContext(fullDn.toString(), password);
// Check for password policy control
PasswordPolicyControl ppolicy = PasswordPolicyControlExtractor.extractControl(ctx);
Attributes attrs = ctx.getAttributes(userDn, getUserAttributes());
DirContextAdapter result = new DirContextAdapter(attrs, userDn, ctxSource.getBaseLdapPath());
if (ppolicy != null) {
result.setAttributeValue(ppolicy.getID(), ppolicy);
}
return result;
} catch (NamingException e) {
// This will be thrown if an invalid user name is used and the method may
// be called multiple times to try different names, so we trap the exception
// unless a subclass wishes to implement more specialized behaviour.
if ((e instanceof org.springframework.ldap.AuthenticationException)
|| (e instanceof org.springframework.ldap.OperationNotSupportedException)) {
handleBindException(userDnStr, username, e);
} else {
throw e;
}
} catch (javax.naming.NamingException e) {
throw LdapUtils.convertLdapException(e);
} finally {
LdapUtils.closeContext(ctx);
}
return null;
}
/**
* Allows subclasses to inspect the exception thrown by an attempt to bind with a particular DN.
* The default implementation just reports the failure to the debug logger.
*/
protected void handleBindException(String userDn, String username, Throwable cause) {
System.out.println("Failed to bind as " + userDn + ": " + cause);
}
}
If anyone has any experience with this kind of thing I would greatly appreciate this as I couldn't find much on this subject. I was hoping that someone could tell me if I am on the right track or if I should be going about this in a different way. Just to be clear I am using spring-security-ldap and not spring-ldap. Also just want to mention that I do have all my dependencies in my pom file. Thanks!
It's not completely clear from your question what's actually going wrong - for example the configuration you have wouldn't actually load since it is using Spring Security's BindAuthenticator
and attempting to pass two ContextSource
arguments to it.
If I were you I would avoid trying to hack the internal implementation classes and instead leave them alone and write a separate delegation class, based on your selection criteria.
First I would define two separate LdapAuthenticationProvider
beans, one for each domain and first make sure that you can authenticate users with each of them by calling them directly in a unit test. Make sure you can configure each of them correctly for their respective domains before you try and use both together.
After that I would wire them into a separate delegating AuthenticationProvider
. Something like:
public class DelegatingLdapAuthenticationProvider implements AuthenticationProvider {
// Inject these via the app context
private LdapAuthenticationProvider primary;
private LdapAuthenticationProvider secondary;
public Authentication authenticate(Authentication a) {
if (a.getName().contains("secondDomain")) {
return secondary.authenticate(a);
} else {
return primary.authenticate(a);
}
}
}
I would then configure this bean as the authentication provider which Spring Security actually calls.