/**
* This file Copyright (c) 2003-2012 Magnolia International
* Ltd. (http://www.magnolia-cms.com). All rights reserved.
*
*
* This file is dual-licensed under both the Magnolia
* Network Agreement and the GNU General Public License.
* You may elect to use one or the other of these licenses.
*
* This file is distributed in the hope that it will be
* useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
* Redistribution, except as permitted by whichever of the GPL
* or MNA you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or
* modify this file under the terms of the GNU General
* Public License, Version 3, as published by the Free Software
* Foundation. You should have received a copy of the GNU
* General Public License, Version 3 along with this program;
* if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 2. For the Magnolia Network Agreement (MNA), this file
* and the accompanying materials are made available under the
* terms of the MNA which accompanies this distribution, and
* is available at http://www.magnolia-cms.com/mna.html
*
* Any modifications to this file must keep this entire header
* intact.
*
*/
package info.magnolia.jaas.sp;
import info.magnolia.cms.security.Realm;
import info.magnolia.cms.security.auth.callback.RealmCallback;
import info.magnolia.cms.util.BooleanUtil;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract implementation of the <code>LoginModule</code> providing common methods and constants implementation.
* @author Sameer Charles
* $Id$
*/
public abstract class AbstractLoginModule implements LoginModule {
// magnolia specific option to define if "this" module needs to be
// skipped based on previous (in JAAS module chain) module status
public static final String OPTION_SKIP_ON_PREVIOUS_SUCCESS = "skip_on_previous_success";
public static final String OPTION_REALM = "realm";
public static final String OPTION_USE_REALM_CALLBACK= "use_realm_callback";
public static final String STATUS = "statusValue";
public static final int STATUS_SUCCEEDED = 1;
/**
* @deprecated use STATUS_SUCCEEDED
*/
@Deprecated
public static final int STATUS_SUCCEDED = STATUS_SUCCEEDED;
public static final int STATUS_FAILED = 2;
public static final int STATUS_SKIPPED = 3;
public static final int STATUS_UNAVAILABLE = 4;
// TODO: implement the following commonly supported flags to allow single signon with third party modules
//If true, the first LoginModule in the stack saves the password entered,
// and subsequent LoginModules also try to use it. If authentication fails,
// the LoginModules prompt for a new password and retry the authentication.
public static final String TRY_FIRST_PASS = "try_first_pass";
//If true, the first LoginModule in the stack saves the password entered,
// and subsequent LoginModules also try to use it.
// LoginModules do not prompt for a new password if authentication fails (authentication simply fails).
public static final String USE_FIRST_PASS = "use_first_pass";
//If true, the first LoginModule in the stack saves the password entered,
// and subsequent LoginModules attempt to map it into their service-specific password.
// If authentication fails, the LoginModules prompt for a new password and retry the authentication.
public static final String TRY_MAPPED_PASS = "try_mapped_pass";
//If true, the first LoginModule in the stack saves the password entered,
// and subsequent LoginModules attempt to map it into their service-specific password.
// LoginModules do not prompt for a new password if authentication fails (authentication simply fails).
public static final String USE_MAPPED_PASS = "use_mapped_pass";
public Subject subject;
public CallbackHandler callbackHandler;
public Map<String, Object> sharedState;
public Map<String, Object> options;
public String name;
public char[] pswd;
/**
* The realm we login into. Initialized by the option realm.
*/
protected Realm realm = Realm.REALM_ALL;
/**
* Allow the client to define the realm he logs into. Default value is false
*/
protected boolean useRealmCallback;
// this status is sent back to the LoginModule chain
public boolean success;
protected Logger log = LoggerFactory.getLogger(getClass());
private boolean skipOnPreviousSuccess;
/**
*
*/
public AbstractLoginModule() {
}
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, final Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
this.options = options;
// don't overwrite group and roles set in the shared state
if (this.sharedState.get("groupNames") == null) {
this.sharedState.put("groupNames", new LinkedHashSet<String>());
}
if (this.sharedState.get("roleNames") == null) {
this.sharedState.put("roleNames", new LinkedHashSet<String>());
}
String realmName = (String) options.get(OPTION_REALM);
this.realm = StringUtils.isBlank(realmName) ? Realm.DEFAULT_REALM : Realm.Factory.newRealm(realmName);
this.useRealmCallback = BooleanUtil.toBoolean((String) options.get(OPTION_USE_REALM_CALLBACK), true);
this.skipOnPreviousSuccess = BooleanUtil.toBoolean((String) options.get(OPTION_SKIP_ON_PREVIOUS_SUCCESS), false);
}
@Override
public boolean login() throws LoginException {
if (this.getSkip()) {
return true;
}
if (this.callbackHandler == null) {
throw new LoginException("Error: no CallbackHandler available");
}
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("name");
callbacks[1] = new PasswordCallback("pswd", false);
// if the realm is not defined in the jaas configuration
// we ask use a callback to get the value
if(this.useRealmCallback){
callbacks = (Callback[]) ArrayUtils.add(callbacks, new RealmCallback());
}
this.success = false;
try {
this.callbackHandler.handle(callbacks);
this.name = ((NameCallback) callbacks[0]).getName();
this.pswd = ((PasswordCallback) callbacks[1]).getPassword();
if(this.useRealmCallback){
String aRealm = ((RealmCallback) callbacks[2]).getRealm();
this.realm = StringUtils.isBlank(aRealm) ? this.realm : Realm.Factory.newRealm(aRealm);
}
this.validateUser();
} catch (IOException ioe) {
log.debug("Exception caught", ioe);
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException ce) {
log.debug(ce.getMessage(), ce);
throw new LoginException(ce.getCallback().toString() + " not available");
}
// TODO: should not we set success BEFORE calling validateUser to give it chance to decide whether to throw an exception or reset the value to false?
this.success = true;
this.setSharedStatus(STATUS_SUCCEEDED);
return this.success;
}
/**
* Updates subject with ACL and other properties.
*/
@Override
public boolean commit() throws LoginException {
/**
* If login module failed to authenticate then this method should simply return false
* instead of throwing an exception - refer to specs for more details
* */
if (!this.success) {
return false;
}
this.setEntity();
this.setACL();
return true;
}
@Override
public boolean abort() throws LoginException {
return this.release();
}
@Override
public boolean logout() throws LoginException {
return this.release();
}
/**
* @return shared status value as set by this LoginModule
* */
public int getSharedStatus() {
Integer status = (Integer) this.sharedState.get(STATUS);
if (null != status) {
return status.intValue();
}
return STATUS_UNAVAILABLE;
}
/**
* Sets shared status value to be used by subsequent LoginModule(s).
* */
public void setSharedStatus(int status) {
this.sharedState.put(STATUS, new Integer(status));
}
/**
* Tests if the option skip_on_previous_success is set to true and preceding LoginModule was successful.
* */
protected boolean getSkip() {
return skipOnPreviousSuccess && this.getSharedStatus() == STATUS_SUCCEEDED;
}
public void setGroupNames(Set<String> names) {
this.getGroupNames().addAll(names);
}
public void addGroupName(String groupName) {
getGroupNames().add(groupName);
}
public Set<String> getGroupNames() {
return (Set<String>) this.sharedState.get("groupNames");
}
public void setRoleNames(Set<String> names) {
this.getRoleNames().addAll(names);
}
public void addRoleName(String roleName) {
getRoleNames().add(roleName);
}
public Set<String> getRoleNames() {
return (Set<String>) this.sharedState.get("roleNames");
}
/**
* Releases all associated memory.
*/
public boolean release() {
return true;
}
/**
* Checks if the credentials exist in the repository.
* @throws LoginException or specific subclasses to report failures.
*/
public abstract void validateUser() throws LoginException;
/**
* Sets user details.
*/
public abstract void setEntity();
/**
* Sets access control list from the user, roles and groups.
*/
public abstract void setACL();
}