Created
January 16, 2013 09:47
-
-
Save stephen-masters/4545962 to your computer and use it in GitHub Desktop.
This is a read-only Seam identity store implementation, which I knocked up to enable Drools Guvnor to authenticate users against Microsoft Active Directory. To use it, produce a .jar containing this class and put it in your Guvnor web application lib directory. I would recommend creating a .jar which only contains this class, as it is best to mi…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package uk.co.scattercode.security.seam; | |
import java.util.ArrayList; | |
import java.util.Hashtable; | |
import java.util.List; | |
import java.util.Set; | |
import java.util.TreeSet; | |
import javax.naming.Context; | |
import javax.naming.NamingEnumeration; | |
import javax.naming.NamingException; | |
import javax.naming.directory.Attributes; | |
import javax.naming.directory.SearchControls; | |
import javax.naming.directory.SearchResult; | |
import javax.naming.ldap.InitialLdapContext; | |
import javax.naming.ldap.LdapContext; | |
import org.jboss.seam.security.Credentials; | |
import org.jboss.seam.security.Identity; | |
import org.jboss.seam.security.management.LdapIdentityStore; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* This is a read-only Seam identity store implementation to authenticate users | |
* against Microsoft Active Directory. | |
* <p> | |
* To make use of this authenticator, you first need to build this project. Put | |
* the resulting .jar into the WEB-INF/lib directory of your web application. | |
* You will then need to put something similar to the following into the Seam | |
* components.xml file (in WEB-INF) and restart the web server. | |
* </p> | |
* | |
* <pre> | |
* <component name="activeDirectoryAuthenticator" | |
* class="uk.co.scattercode.security.seam.ActiveDirectoryIdentityStore" | |
* startup="true" scope="APPLICATION"> | |
* <property name="domain">mydomain.local</property> | |
* <property name="serverAddress">myadhost.local</property> | |
* <property name="serverPort">389</property> | |
* <property name="sysUser">readonlyuser</property> | |
* <property name="sysPassword">r3@donlyus3rpwd</property> | |
* <property name="userContextDn">OU=My Company,DC=mydomain,DC=local</property> | |
* </component> | |
* | |
* <security:identity authenticate-method="#{activeDirectoryAuthenticator.authenticate}" /> | |
* </pre> | |
* <p> | |
* The "sysUser" must have read access to Active Directory. If it is created as | |
* an A/D user, then this should be the case. Write access is not necessary, as | |
* this implementation is deliberately restricted to authentication only. | |
* </p> | |
* <p> | |
* As a note of warning, this authenticator will lower-case the username before | |
* adding it to the security context. This is a workaround for those situations | |
* where Active Directory does not enforce case-sensitive user names, but the | |
* web application (Guvnor is one example) does. | |
* </p> | |
* | |
* @author Stephen Masters | |
*/ | |
public class ActiveDirectoryIdentityStore extends LdapIdentityStore { | |
/** Generated serialVersionUID. */ | |
private static final long serialVersionUID = -3999965613116614977L; | |
private static Logger log = LoggerFactory.getLogger(ActiveDirectoryIdentityStore.class); | |
private String domain; | |
private String serverAddress; | |
private int serverPort; | |
private String sysuser; | |
private String syspassword; | |
private String userContextDn; | |
/** | |
* This no-args authenticate() method is required for Seam JAAS authentication. | |
*/ | |
public boolean authenticate() { | |
Credentials credentials = Identity.instance().getCredentials(); | |
if (credentials.getUsername() == null || credentials.getPassword() == null) { | |
log.error("Login attempted with null credentials."); | |
return false; | |
} | |
// Active Directory is not case-sensitive when searching for users. | |
// However, Drools Guvnor users for example, are case-sensitive. | |
// This can lead to an issue where a user is able to login with a | |
// mixture of upper and lower case for their username, but when this | |
// username is added to the context, it fails to map to the application | |
// username. To work around this issue, this authenticator will | |
// lower-case the username when adding it to the context. This means | |
// that as long as you create users in Guvnor with lower case names, | |
// they should map successfully. | |
credentials.setUsername(credentials.getUsername().toLowerCase()); | |
return authenticate(credentials.getUsername(), credentials.getPassword()); | |
} | |
@Override | |
public boolean authenticate(String username, String password) { | |
try { | |
// If an LDAP context can be created with the user name and password, | |
// the user credentials are valid. | |
@SuppressWarnings("unused") | |
LdapContext ctxGC = getContext(username, password); | |
} catch (NamingException e) { | |
log.error("Login attempted with invalid credentials by user: " + username); | |
return false; | |
} | |
Attributes userAttributes = findUser(username); | |
if (userAttributes != null) { | |
return true; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Performs an active directory search in the context of the system user. | |
*/ | |
private Attributes findUser(String username) { | |
try { | |
LdapContext ctxGC = getContext(); | |
log.info("Connected to LDAP context."); | |
String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))"; | |
SearchControls searchCtls = new SearchControls(); | |
// Search through all sub-directories to find the user. | |
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); | |
// Define which attributes we wish to get hold of. | |
String[] returnedAtts = { "sn", "givenName", "mail", "distinguishedName", "memberOf" }; | |
searchCtls.setReturningAttributes(returnedAtts); | |
log.info("Searching for: " + searchFilter); | |
NamingEnumeration<SearchResult> results = ctxGC.search(this.userContextDn, searchFilter, searchCtls); | |
while (results.hasMoreElements()) { | |
SearchResult result = results.next(); | |
log.info("Results: " + result.toString()); | |
Attributes attrs = result.getAttributes(); | |
if (attrs != null) { | |
return attrs; | |
} | |
} | |
} catch (NamingException e) { | |
log.error("NamingException finding user.", e); | |
} | |
// Didn't find anything. | |
return null; | |
} | |
private LdapContext getContext(String username, String password) throws NamingException { | |
log.info("Connecting to LDAP context..."); | |
Hashtable<String, String> env = new Hashtable<String, String>(); | |
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); | |
env.put(Context.PROVIDER_URL, "ldap://" + this.serverAddress + ":" + this.serverPort); | |
env.put(Context.SECURITY_AUTHENTICATION, "simple"); | |
env.put(Context.SECURITY_PRINCIPAL, username + "@" + this.domain); | |
env.put(Context.SECURITY_CREDENTIALS, password); | |
LdapContext ctx = new InitialLdapContext(env, null); | |
return ctx; | |
} | |
private LdapContext getContext() throws NamingException { | |
log.info("Connecting to LDAP context..."); | |
Hashtable<String, String> env = new Hashtable<String, String>(); | |
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); | |
env.put(Context.PROVIDER_URL, "ldap://" + this.serverAddress + ":" + this.serverPort); | |
env.put(Context.SECURITY_AUTHENTICATION, "simple"); | |
env.put(Context.SECURITY_PRINCIPAL, this.sysuser + "@" + this.domain); | |
env.put(Context.SECURITY_CREDENTIALS, this.syspassword); | |
return new InitialLdapContext(env, null); | |
} | |
public boolean isUserEnabled(String username) { | |
Attributes userAttributes = findUser(username); | |
if (userAttributes != null) { | |
if ("TRUE".equals(userAttributes.get("msRTCSIP-UserEnabled"))) { | |
return true; | |
} | |
} | |
return false; | |
} | |
public List<String> getGrantedRoles(String username) { | |
Attributes userAttributes = findUser(username); | |
String memberships = userAttributes.get("memberOf").toString(); | |
Set<String> roles = new TreeSet<String>(); | |
if (memberships != null) { | |
for (String attr : memberships.split(",")) { | |
if (attr.contains("CN=")) { | |
// Role is the value to the right of the equals symbol. | |
roles.add(attr.split("=")[1]); | |
} | |
} | |
} | |
return new ArrayList<String>(roles); | |
} | |
/** | |
* This is a read-only LDAP identity store, so this method will return false | |
* for all of the optional 'mutating' features. | |
*/ | |
public boolean supportsFeature(Feature feature) { | |
return false; | |
} | |
/** | |
* The LDAP host server. | |
*/ | |
@Override | |
public String getServerAddress() { | |
return this.serverAddress; | |
} | |
public void setServerAddress(String host) { | |
this.serverAddress = host; | |
} | |
/** | |
* The LDAP port on the host server. | |
*/ | |
@Override | |
public int getServerPort() { | |
return this.serverPort; | |
} | |
public void setServerPort(int port) { | |
this.serverPort = port; | |
} | |
/** | |
* The system user name to be used by the application to connect to Active Directory. | |
*/ | |
public String getSysUser() { | |
return sysuser; | |
} | |
public void setSysUser(String sysuser) { | |
this.sysuser = sysuser; | |
} | |
/** | |
* The system user password to connect to Active Directory. | |
*/ | |
public String getSysPassword() { | |
return syspassword; | |
} | |
public void setSysPassword(String syspassword) { | |
this.syspassword = syspassword; | |
} | |
/** | |
* The Active Directory domain. | |
*/ | |
public String getDomain() { | |
return domain; | |
} | |
public void setDomain(String domain) { | |
this.domain = domain; | |
} | |
/** | |
* The LDAP Distinguished Name where users can be found. | |
*/ | |
public String getUserContextDn() { | |
return userContextDn; | |
} | |
public void setUserContextDn(String dn) { | |
this.userContextDn = dn; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thx! It helped me a lot to work with f... Active Directory in JBoss Seam!