您的位置:首页 > Web前端

Liferay和SSO HowTo: Liferay 4 + CAS + AD/LDAP

2008-06-20 11:09 387 查看
Liferay done lots of integration with JOSSO, CAS and other clients. You just have to write a Liferay auth connector as documented on their site.
This took about a week's worth of researching and trial and error... I figure posting this might save some people a lot of time and headache.

The Problem
Our organization already uses Active Directory for single sign on. We want users to be able to log in to the Liferay Portal using their AD username and password.

So, we need to do at minimum 2 things:
1. Authenticate the username/password entered at the Liferay Portal logon page against Active Directory (i.e. LDAP)
2. Automatically create Liferay Portal accounts for anyone who already has an account in Active Directory

Just using Liferay's LDAPAuth or ADSAuth modules alone via auth.pipeline.pre (see http://content.liferay.com/4.0.0/docs/users/ch04s02.html) won't work. These modules check authentication information against LDAP/AD, but you would still have to manually create accounts for users in the Liferay Portal database.

One way around this would be to write your own extension of the LDAPAuth module and insert some code to automatically create the user account during logon if the account does not already exist. This has been suggested elsewhere in these forums, and this method worked for us.

However, rather than using Liferay's authentication system, we decided to go with something more powerful, that we could use for all of our web-based applications -- whether they are J2EE, PHP, etc. Additionally, at some point we may want to do authentication via NTLS, so that the user never has to enter their credentials via the web browser, but rather has this information passed from their operating system logon (via Kerberos, etc.)

The Solution
Given our requirements, we decided to do all of our authentication via CAS (or more specifically, the JA-SIG implementation of CAS). The username and password authentication is done via the CAS server. When a user tries to log on to Liferay (or any other application we've enabled to use CAS), Liferay connects to the CAS server, checks if the user has been authenticated (i.e. if their "authentication ticket" is valid), and either redirects them to the login page on the CAS server, or lets them in to Liferay, automatically logging them in behind the scenes. This circumvents the Liferay login page altogether -- the user will never see the Liferay login box. Instead they will log in via the CAS login page.

I won't go in to how we set up our CAS server. This is fairly well documented on the JA-SIG site and elsewhere. I should mention though that we had a hard time getting CAS talking to our AD server and had to write our own authentication module. I can post the code for that if anyone's interested.

Once you have CAS working (i.e. you can enter your AD username and password on the CAS login page and the server tells you that you've been successfully authenticated), you can move on to configuring Liferay.

First, obtain the extended cas client JAR from discursive: http://www.discursive.com/projects/cas-extend/download.html and put it in your server's lib directory. This client provides a dummy trust filter for SSL certificates. I'll explain why you'll need this later.

Second, get the LDAP client library from Novell: http://developer.novell.com/wiki/index.php...lasses_for_Java

Now you will need to write your own CASAutoLogin module for Liferay.

First though, we also wrote a general LDAP wrapper library for dealing with LDAP. This is ugly and needs to be refactored, but I'll leave that up to the reader. For now this works:

CODE

package com.company.ldap;

import java.io.UnsupportedEncodingException;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPJSSESecureSocketFactory;
import com.novell.ldap.LDAPSearchResults;
import com.novell.ldap.util.DN;

public class ActiveDirectoryAuthenticator {

private static Log log = LogFactory.getLog(ActiveDirectoryAuthenticator.class);

private static String authenticatorDN = "CN=Authenticator,CN=Users,DC=COMPANY,DC=COM"; // full DN of the authenticator user (used for the initial bind to the LDAP server)
private static String authenticatorPassword = "changeme"; // password for the authenticator user (used for the initial bind to the LDAP server)
private static String ldapServer = "ad01.company.com"; // LDAP server address
private static String usersDN = "CN=Users,DC=COMPANY,DC=COM"; // DN for container where users can be found
private static String usernameAttribute = "sAMAccountName"; // name of attribute in LDAP that contains the username

public static String getAuthenticatorDN() {
return authenticatorDN;
}

public static String getAuthenticatorPassword() {
return authenticatorPassword;
}

public static String getUsernameAttribute() {
return usernameAttribute;
}

public static boolean validateUsernamePassword(String username, String password)
throws BadAuthenticatorCredentialsException, Exception {
ActiveDirectoryAuthenticator auth = new ActiveDirectoryAuthenticator();
try {
auth.authenticate(username, password);
} catch (BadUserCredentialsException e) {
return false;
}

return true;
}

public LDAPEntry authenticate(String username, String password)
throws BadUserCredentialsException, BadAuthenticatorCredentialsException, LDAPException, UnsupportedEncodingException {
LDAPConnection ldap = connectToLDAP();

if (!ldap.isConnected())
throw new IllegalStateException("Cannot authenticate because the given LDAPConnection is not connected.");

// first we bind using the special authenticator user so that we can get access to the active directory
bindAuthenticator(ldap);

LDAPEntry userEntry = null;

try {
String userDN = getUserDN(ldap, username);

if (userDN == null) {
throw new BadUserCredentialsException("User '"+username+"' not found in the LDAP directory.");
}

log.info("Found DN for username '"+username+"': "+userDN);

bindUser(ldap, userDN, password);

try {
// if we got this far, then we're authenticated... now fetch the users's
// attributes so that we can feed them to liferay in case the user was
// not found in liferay (yes, it is probably unnecessary to do it again here since
// presumably we got the user entry in the previous search, but it's better
// practice to do it here again with the actual users's credentials)
userEntry = getUserEntry(ldap, userDN);

} catch (LDAPException e) {
switch (e.getResultCode()) {
case LDAPException.NO_SUCH_OBJECT:
log.error("The user with DN '"+userDN+"' does not exist in the LDAP directory.");
throw new BadUserCredentialsException("User with DN '"+userDN+"' not found in the LDAP directory.");
default:
log.error(e);
throw new BadUserCredentialsException(e);
}
}

} catch(LDAPException e) {
switch (e.getResultCode()) {
case LDAPException.INVALID_CREDENTIALS:
log.info("Invalid password supplied for user '"+username+"'.");
throw new BadUserCredentialsException("Invalid password supplied for user '"+username+"'.");
default:
log.error(e);
throw e;
}
}

try {
ldap.disconnect();
} catch (LDAPException e) {
log.error(e);
}

return userEntry;
}

public LDAPConnection connectToLDAP() {
LDAPConnection ldap = new LDAPConnection();

log.info("Connecting to LDAP server '"+ldapServer+"'");

try {
ldap.connect(ldapServer, LDAPConnection.DEFAULT_PORT);
} catch (LDAPException e) {
// TODO: re-throw the exception
log.error(e);
}

// this will always print the second option, since we are not binding here
log.info(ldap.isBound() ? "Got authenticated to the LDAP server" : "Got anonymous bind to the LDAP server");

return ldap;
}

public void bindUser(LDAPConnection ldap, String dn, String password)
throws BadUserCredentialsException, LDAPException, UnsupportedEncodingException {
if (!ldap.isConnected())
throw new IllegalStateException("Cannot bindUser to the given LDAPConnection because it is not connected.");

checkDNSyntax(dn);

try {
ldap.bind(LDAPConnection.LDAP_V3, dn, password.getBytes("UTF8"));
} catch( LDAPException e ) {
switch (e.getResultCode()) {
case LDAPException.NO_SUCH_OBJECT:
log.error("The user with DN '"+dn+"' does not exist in the LDAP directory.");
throw new BadUserCredentialsException("The user '"+dn+"' was not found in the LDAP directory.");
case LDAPException.INVALID_CREDENTIALS:
log.info("The password for user with DN '"+dn+"' is invalid.");
throw new BadUserCredentialsException("The password for user '"+dn+"' is incorrect.");
default:
log.error(e);
throw e;
}
} catch( UnsupportedEncodingException e ) {
log.error(e);
throw e;
}

if (ldap.isBound())
log.info("Successfully bound to LDAP server with credentials for DN: "+dn);
else
log.error("LDAP connection is not bound for user with DN '"+dn+"' even though bind() did not raise an LDAPException!");
}

public void bindAuthenticator(LDAPConnection ldap)
throws BadAuthenticatorCredentialsException, LDAPException, UnsupportedEncodingException {
if (!ldap.isConnected())
throw new IllegalStateException("Cannot bindAuthenticator to the given LDAPConnection because it is not connected.");

try {
ldap.bind(LDAPConnection.LDAP_V3, authenticatorDN, authenticatorPassword.getBytes("UTF8"));
} catch( LDAPException e ) {
switch (e.getResultCode()) {
case LDAPException.NO_SUCH_OBJECT:
log.error("The authenticator user (DN: "+authenticatorDN+") does not exist in the LDAP directory. LDAP authentication cannot proceed!");
throw new BadAuthenticatorCredentialsException("The authenticator user was not found in the LDAP directory.");
case LDAPException.INVALID_CREDENTIALS:
log.error("The authenticator user's password is invalid. LDAP authentication cannot proceed!");
throw new BadAuthenticatorCredentialsException("The password for the authenticator user is incorrect.");
default:
log.error(e);
throw e;
}
} catch( UnsupportedEncodingException e ) {
log.error("The authenticator user's password (DN: "+authenticatorDN+") is encoded using an unsupported text encoding format (i.e. the system can't decode it to UTF8).", e);
throw e;
}

if (ldap.isBound())
log.info("Successfully bound to LDAP server with authenticator credentials");
else
log.error("LDAP connection is not bound for authenticator even though bind() did not raise an LDAPException!");
}

public String getUserDN(LDAPConnection ldap, String username)
throws LDAPException {
if (!ldap.isBound())
throw new IllegalStateException("Cannot getUserDN because the given LDAPConnection is not bound.");

LDAPSearchResults searchResults = null;

String attrs[] = {LDAPConnection.NO_ATTRS};
searchResults =
ldap.search(usersDN, // container to search
LDAPConnection.SCOPE_ONE, // search scope
"("+usernameAttribute+"="+username+")", // search filter
attrs, // "1.1" returns entry name only
true); // no attributes are returned

if (searchResults.hasMore()) {
String dn = searchResults.next().getDN();
log.info("Found DN for username '"+username+"': "+dn);
return dn;
} else {
// error because search() should have thrown an LDAPException
log.info("Couldn't find a DN for username '"+username+"'.");
return null;
}
}

public LDAPEntry getUserEntry(LDAPConnection ldap, String dn)
throws LDAPException {
if (!ldap.isBound())
throw new IllegalStateException("Cannot getUser because the given LDAPConnection is not bound.");

checkDNSyntax(dn);

LDAPSearchResults searchResults = null;

try {
searchResults =
ldap.search(dn, // container to search
LDAPConnection.SCOPE_SUB, // search scope
"", // search filter
null,
false); // return all attributes

if (searchResults.hasMore()) {
LDAPEntry entry = searchResults.next();
log.info("Found entry for DN '"+dn+"'");
return entry;
} else {
// error because search() should have thrown an LDAPException
log.info("Couldn't find a match for DN '"+dn+"'.");
return null;
}
} catch (LDAPException e) {
switch (e.getResultCode()) {
case LDAPException.NO_SUCH_OBJECT:
log.error("The user with DN '"+dn+"' does not exist in the LDAP directory.");
return null;
default:
log.error(e);
throw e;
}
}
}

protected void checkDNSyntax(String dn) {
// apparently a DN string like "foobar" is valid, but we don't want to allow that here so we do this manual fudge...
if (!Pattern.matches(".*?//w+=//w+.*?", dn))
throw new IllegalArgumentException(dn+" is not a valid DN");

new DN(dn); // this constructor throws an IllegalArgumentException if dn is invalid
// @see http://developer.novell.com/ndk/doc/jldap/jldapenu/api/com/novell/ldap/util/DN.html#DN(java.lang.String) }
}

You'll also need to create BadUserCredentialsException.java and BadAuthenticatorCredentialsException.java in the same package. Both just extend the standard Exception class with no additional code of their own.

Active Directory requires us to first bind to the LDAP server before we can search it, so we have an "Authenticator" user defined in our AD. This user's credentials are hard-coded near the top of the above code.

Now, our CASAutoLogin module uses the above code as follows:

CODE

package com.company.portal.security.auth;

import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Locale;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.liferay.portal.NoSuchUserException;
import com.liferay.portal.model.User;
import com.liferay.portal.security.auth.AutoLoginException;
import com.liferay.portal.service.spring.CompanyLocalServiceUtil;
import com.liferay.portal.service.spring.UserLocalServiceUtil;
import com.liferay.util.StringPool;

import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;

import com.company.ldap.ActiveDirectoryAuthenticator;
import com.company.ldap.BadAuthenticatorCredentialsException;

public class CASAutoLogin extends com.liferay.portal.security.auth.CASAutoLogin {

/**
* This should be based on the CAS filter class we are using.
* See the CAS Filter stuff in web.xml to find out what CAS filter class we're using.
*/
public static final String CAS_FILTER_USER = "com.discursive.cas.extend.client.filter.user";

/**
* All user accounts automatically created by this module will be assigned to this company id.
*/
public static final String DEFAULT_COMPANY_ID = "company.com";

/**
* All user accounts automatically created by this module will be marked as having been created by this user.
*/
public static final String DEFAULT_CREATOR_USER_ID = "company.com.1";

private static Log log = LogFactory.getLog(CASAutoLogin.class);

public String[] login(HttpServletRequest req, HttpServletResponse res)
throws AutoLoginException {

try {
String[] credentials = null;

HttpSession ses = req.getSession();

log.info("Checking session attribute '"+CAS_FILTER_USER+"' to get userId of user authenticated by CAS.");

String userId = (String)ses.getAttribute(CAS_FILTER_USER);

if (userId != null) {
log.info("Okay, authenticated user is: "+userId);

User user = null;

try {
log.info("Looking for user '"+userId+"' in the portal database...");
user = UserLocalServiceUtil.getUserById(userId);
} catch (NoSuchUserException e) {
log.info("User '"+userId+"' does not exist in the portal database. Attempting to create this account...");

user = createLiferayAccountForUser(userId);
}

log.info("Alright, we've got data for '"+userId+"'! Autologin seems to have worked.");
credentials = new String[3];

credentials[0] = userId;
credentials[1] = user.getPassword();
credentials[2] = Boolean.TRUE.toString();
} else {
log.info("The session does not appear to have been authenticated by CAS :(");
}

return credentials;
} catch (Exception e) {
throw new AutoLoginException(e);
}
}

private User createLiferayAccountForUser(String userId)
throws AutoLoginException {
// we pull the user data from LDAP using the special authenticator user
ActiveDirectoryAuthenticator auth = new ActiveDirectoryAuthenticator();
LDAPConnection ldap = auth.connectToLDAP();

LDAPEntry userEntry = null;

try {
auth.bindAuthenticator(ldap);
userEntry = auth.getUserEntry(ldap, auth.getUserDN(ldap, userId));
} catch (BadAuthenticatorCredentialsException e) {
throw new AutoLoginException(e);
} catch (LDAPException e) {
throw new AutoLoginException(e);
} catch (UnsupportedEncodingException e) {
throw new AutoLoginException(e);
}

boolean autoUserId = false;
boolean autoPassword = true;
String companyId = DEFAULT_COMPANY_ID;

// there's no good way to retrieve the user's password from LDAP, so we generate a random one here instead
// (the user never has to enter this, so it doesn't really matter what we use)
//String password = generateRandomPassword();

String password1 = null;
String password2 = null;

boolean passwordReset = false;
String emailAddress = userEntry.getAttribute("mail").getStringValue();
Locale locale = Locale.CANADA;
String nickName = userEntry.getAttribute("mailNickname").getStringValue();
String prefixId = StringPool.BLANK;
String suffixId = StringPool.BLANK;
String middleName = StringPool.BLANK;
String lastName = userEntry.getAttribute("sn").getStringValue();
String firstName = userEntry.getAttribute("givenName").getStringValue();
boolean male = true;
int birthdayMonth = Calendar.JANUARY;
int birthdayDay = 1;
int birthdayYear = 1970;
String jobTitle = StringPool.BLANK;
String organizationId = null;
String locationId = null;

User user = null;

try {
user = UserLocalServiceUtil.addUser(
DEFAULT_CREATOR_USER_ID, companyId, autoUserId, userId, autoPassword, password1,
password2, passwordReset, emailAddress, locale, firstName,
middleName, lastName, nickName, prefixId, suffixId, male,
birthdayMonth, birthdayDay, birthdayYear, jobTitle,
organizationId, locationId);
} catch (com.liferay.portal.DuplicateUserEmailAddressException e) {
log.error(e.getMessage(), e);
throw new AutoLoginException("A user with the email address '"+userEntry.getAttribute("mail").getStringValue()+
"' already exists in the portal database but has a username different from "+userId+". " +
"Delete this user from the portal database and try again.");
} catch (com.liferay.portal.SystemException e) {
log.error(e.getMessage());
throw new AutoLoginException(e);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new AutoLoginException(e);
}

return user;
}

}

The above code will try to automatically authenticate via CAS. If the user is successfully authenticated, it searches the Liferay portal database for a user record matching the username being authenticated. If it doesn't find the user, it tries to create him/her based on data pulled from the LDAP server. Note that we don't have to worry about making the AD and Liferay passwords match, because the Liferay password is never actually checked. (AFAIK, it's probably impossible to extract the user's password from the AD... maybe only its hash?... regardless, we don't need this).

Compile all your code, and package it nicely into a JAR (i.e. cd into your build directory and jar cvf company.jar com). Put the JAR file into the server's lib directory.

Now we can configure Liferay.

First, if you haven't done so already, you should make sure liferay is configured to use your company id. See this post for good instructions on how to do this: http://forums.liferay.com/index.php?s=&sho...indpost&p=11958

Make sure you've created the default user (or some other general administration account -- make sure you hard-code that user ID into the CASAutoLogin DEFAULT_CREATOR_ID in the code above).

Now, add the following to your portal-ext.properties:

CODE

auto.login.hooks=com.company.portal.security.auth.CASAutoLogin
company.security.auth.type=userId
# disable auth pipeline (we use CAS for all this via auto.login.hooks)
auth.pipeline.pre=
auth.pipeline.enable.liferay.check=false
# we leave this blank so that all users with the Administrator role are omniadmins
omniadmin.users=

Some of the above may be unnecessary, but it works for us so I'm not going to try playing around with it.

Now open up your web.xml (ext-web/docroot/WEB-INF/web.xml) and add the following:

CODE

<context-param>
<param-name>ccompany.com</param-value>
</context-param>

<!-- we use the discursive.com extended client because it offers a dummy filter (see below) -->
<filter>
<filter-name>CAS Filter</filter-name>
<filter-class>com.discursive.cas.extend.client.filter.CASFilter</filter-class>
<init-param>
<param-name>com.discursive.cas.extend.client.filter.loginUrl</param-name>
<param-value>https://localhost:8443/cas/login</param-value>
</init-param>
<init-param>
<param-name>com.discursive.cas.extend.client.filter.validateUrl</param-name>
<param-value>https://localhost:8443/cas/proxyValidate</param-value>
</init-param>
<init-param>
<param-name>com.discursive.cas.extend.client.filter.logout</param-name>
<param-value>https://localhost:8443/cas/logout</param-value>
</init-param>
<init-param>
<param-name>com.discursive.cas.extend.client.filter.serverName</param-name>
<param-value>localhost:8080</param-value>
</init-param>

<!--
this lets us use self-signed certificates...
!!!!!! TURN THIS OFF FOR PRODUCTION ENVIRONMENT!!!!!
(i.e. we must use a real, authority-signed certificate for production)
-->
<init-param>
<param-name>com.discursive.cas.extend.client.dummy.trust</param-name>
<param-value>true</param-value>
</init-param>

</filter>

This is where the discursive extended cas client comes in really useful. CAS uses SSL for all of its communication, so you will have to have set up an SSL certificate on your CAS webapp server (i.e. in Tomcat or whatever). Chances are though, in your development environment, your certificate will be self-signed. The standard CAS client won't accept self-signed certificates. Fortunately the discursive extended library provides a dummy trust client that will let you use your self-signed certificate. This is specified in the <init-param> above.

Alright. Now, unless I'm forgetting something (which I very well could be), this should be it. Deploy your portal ext, restart your server, and you should have a fully working CAS auto-login system.

Hope this helps.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: