/** * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2011-2012 ForgeRock Inc. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * */ package com.sun.identity.authentication.modules.membership; /** * * @author steve */ import com.sun.identity.shared.debug.Debug; import com.sun.identity.shared.datastruct.CollectionHelper; import com.iplanet.sso.SSOException; import com.sun.identity.authentication.spi.AMLoginModule; import com.sun.identity.authentication.spi.AuthLoginException; import com.sun.identity.authentication.spi.InvalidPasswordException; import com.sun.identity.authentication.util.ISAuthConstants; import com.sun.identity.idm.AMIdentityRepository; import com.sun.identity.idm.IdRepoException; import com.sun.identity.idm.IdSearchControl; import com.sun.identity.idm.IdSearchResults; import com.sun.identity.idm.IdType; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.ChoiceCallback; import javax.security.auth.callback.ConfirmationCallback; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; public class Membership extends AMLoginModule { private ResourceBundle bundle; private Map sharedState; // user's valid ID and principal private String validatedUserID; private MembershipPrincipal userPrincipal; // configurations private Map options; private String serviceStatus; private boolean isDisclaimerExist = true; private Set defaultRoles; private int requiredPasswordLength; private String createMyOwn; private String userID; private String userName; private Map userAttrs; private static final String amAuthMembership = "amAuthMembership"; private final static Debug debug = Debug.getInstance(amAuthMembership); private String regEx; private static final String INVALID_CHARS = "iplanet-am-auth-membership-invalid-chars"; private boolean getCredentialsFromSharedState; private Callback[] callbacks; /** * Initializes this LoginModule. * * @param subject the Subject to be authenticated. * @param sharedState shared LoginModule state. * @param options options specified in the login. * Configuration for this particular * LoginModule. */ public void init(Subject subject, Map sharedState, Map options) { java.util.Locale locale = getLoginLocale(); bundle = amCache.getResBundle(amAuthMembership, locale); if (debug.messageEnabled()) { debug.message("Membership getting resource bundle for locale: " + locale); } this.options = options; this.sharedState = sharedState; } /** * Takes an array of submitted Callback, * process them and decide the order of next state to go. * Return STATE_SUCCEED if the login is successful, return STATE_FAILED * if the LoginModule should be ignored. * * @param callbacks an array of Callback for this Login state * @param state order of state. State order starts with 1. * @return int order of next state. Return STATE_SUCCEED if authentication * is successful, return STATE_FAILED if the * LoginModule should be ignored. * @throws AuthLoginException */ public int process(Callback[] callbacks, int state) throws AuthLoginException { if (debug.messageEnabled()) { debug.message("in process(), login state is " + state); } this.callbacks = callbacks; ModuleState moduleState = ModuleState.get(state); ModuleState nextState = null; switch (moduleState) { case LOGIN_START: int action = 0; // callback[2] is user selected button index // action == 0 is a Submit Button if (callbacks !=null && callbacks.length != 0) { action = ((ConfirmationCallback)callbacks[2]).getSelectedIndex(); if (debug.messageEnabled()) { debug.message("LOGIN page button index: " + action); } } if (action == 0) { // loginUser will attempt to validate the user and return // the next state to display, either an error state or // SUCCESS nextState = loginUser(callbacks); } else { // new user registration initAuthConfig(); clearInfoText(ModuleState.REGISTRATION.intValue()); nextState = ModuleState.REGISTRATION; } break; case CHOOSE_USERNAMES: // user name entered already exists, generate // a set of user names for user to choose nextState = chooseUserID(callbacks); break; case DISCLAIMER: // when disclaimer page exists the user is created // after the user agrees to disclaimer // callbacks[0] is user selected button index int agree = ((ConfirmationCallback)callbacks[0]).getSelectedIndex(); if (debug.messageEnabled()) { debug.message("DISCLAIMER page button index: " + agree); } if (agree == 0) { RegistrationResult result = registerNewUser(); if (result.equals(RegistrationResult.NO_ERROR)) { return ISAuthConstants.LOGIN_SUCCEED; } else { switch (result) { case USER_EXISTS_ERROR: setErrorMessage(result,0); nextState = ModuleState.REGISTRATION; break; case PROFILE_ERROR: nextState = ModuleState.PROFILE_ERROR; break; case NO_ERROR: nextState = ModuleState.COMPLETE; break; } } } else if (agree == 1) { nextState = ModuleState.DISCLAIMER_DECLINED; } else { throw new AuthLoginException(amAuthMembership, "loginException", null); } break; case REGISTRATION: // this is REGISTRATION state, registration will attempt to // create a new user profile // callbacks[len-1] is a user selected button index // next == 0 is a Submit button // next == 1 is a Cancel button int next = ((ConfirmationCallback) callbacks[callbacks.length-1]).getSelectedIndex(); if (debug.messageEnabled()) { debug.message("REGISTRATION page button index: " + next); } if (next == 0) { //clear infotexts in case they had error messages in the //previous run clearInfoText(ModuleState.REGISTRATION.intValue()); ModuleState result = getAndCheckRegistrationFields(callbacks); switch (result) { case DISCLAIMER: nextState = processRegistrationResult(); break; case REGISTRATION: case CHOOSE_USERNAMES: case PROFILE_ERROR: if (debug.messageEnabled()) { debug.message("Recoverable error: " + result.toString()); } nextState = result; break; } } else if (next == 1) { clearCallbacks(callbacks); nextState = ModuleState.LOGIN_START; } else { return ISAuthConstants.LOGIN_IGNORE; } } return nextState.intValue(); } private ModuleState processRegistrationResult() throws AuthLoginException { ModuleState nextState = null; if (isDisclaimerExist) { if (debug.messageEnabled()) { debug.message("Move to disclaimer page"); } nextState = ModuleState.DISCLAIMER; } else { if (debug.messageEnabled()) { debug.message("No disclaimer, register user"); } RegistrationResult regResult = registerNewUser(); switch (regResult) { case USER_EXISTS_ERROR: setErrorMessage(regResult,0); nextState = ModuleState.REGISTRATION; break; case PROFILE_ERROR: nextState = ModuleState.PROFILE_ERROR; break; case NO_ERROR: nextState = ModuleState.COMPLETE; } } return nextState; } /** * User input value will be store in the callbacks[]. * When user click cancel button, these input field should be reset * to blank. */ private void clearCallbacks(Callback[] callbacks) { for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof NameCallback) { NameCallback nc = (NameCallback) callbacks[i]; nc.setName(""); } } } /** * Returns Principal. * * @return Principal */ public Principal getPrincipal() { if (userPrincipal != null) { return userPrincipal; } else if (validatedUserID != null) { userPrincipal = new MembershipPrincipal(validatedUserID); return userPrincipal; } else { return null; } } /** * Destroy the module state */ @Override public void destroyModuleState() { validatedUserID = null; } /** * Set all the used variables to null */ @Override public void nullifyUsedVars() { bundle = null; sharedState = null; options = null; serviceStatus = null; defaultRoles = null; userID = null; userName = null ; userAttrs = null; regEx = null; callbacks = null; } /** * Initializes registration configurations. */ private void initAuthConfig() throws AuthLoginException { if (options == null || options.isEmpty()) { debug.error("options is null or empty"); throw new AuthLoginException(amAuthMembership, "unable-to-initialize-options", null); } try { String authLevel = CollectionHelper.getMapAttr(options, "iplanet-am-auth-membership-auth-level"); if (authLevel != null) { try { int tmp = Integer.parseInt(authLevel); setAuthLevel(tmp); } catch (NumberFormatException e) { // invalid auth level debug.error("invalid auth level " + authLevel, e); } } regEx = CollectionHelper.getMapAttr(options, INVALID_CHARS); serviceStatus = CollectionHelper.getMapAttr(options, "iplanet-am-auth-membership-default-user-status", "Active"); if (getNumberOfStates() >= ModuleState.DISCLAIMER.intValue()) { isDisclaimerExist = true; } else { isDisclaimerExist = false; } defaultRoles = (Set) options.get("iplanet-am-auth-membership-default-roles"); if (debug.messageEnabled()) { debug.message("defaultRoles is : " + defaultRoles); } String tmp = CollectionHelper.getMapAttr(options, "iplanet-am-auth-membership-min-password-length"); if (tmp != null) { requiredPasswordLength = Integer.parseInt(tmp); } } catch(Exception ex){ debug.error("unable to initialize in initAuthConfig(): ", ex); throw new AuthLoginException(amAuthMembership, "Membershipex", null, ex); } } private ModuleState loginUser(Callback[] callbacks) throws AuthLoginException { String password = null; Callback[] idCallbacks = new Callback[2]; try { if (callbacks !=null && callbacks.length == 0) { userName = (String) sharedState.get(getUserKey()); password = (String) sharedState.get(getPwdKey()); if (userName == null || password == null) { return ModuleState.LOGIN_START; } getCredentialsFromSharedState = true; NameCallback nameCallback = new NameCallback("dummy"); nameCallback.setName(userName); idCallbacks[0] = nameCallback; PasswordCallback passwordCallback = new PasswordCallback("dummy",false); passwordCallback.setPassword(password.toCharArray()); idCallbacks[1] = passwordCallback; } else { idCallbacks = callbacks; //callbacks is not null userName = ( (NameCallback) callbacks[0]).getName(); password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword()); } if (password == null || password.length() == 0) { if (debug.messageEnabled()) { debug.message("Membership.loginUser: Password is null/empty"); } throw new InvalidPasswordException("amAuth", "invalidPasswd", null); } //store username password both in success and failure case storeUsernamePasswd(userName, password); initAuthConfig(); AMIdentityRepository idrepo = getAMIdentityRepository( getRequestOrg()); boolean success = idrepo.authenticate(idCallbacks); if (success) { validatedUserID = userName; return ModuleState.COMPLETE; } else { throw new AuthLoginException(amAuthMembership, "authFailed", null); } } catch (IdRepoException ex) { if (getCredentialsFromSharedState && !isUseFirstPassEnabled()) { getCredentialsFromSharedState = false; return ModuleState.LOGIN_START; } if (debug.warningEnabled()) { debug.warning("idRepo Exception"); } setFailureID(userName); throw new AuthLoginException(amAuthMembership, "authFailed", null, ex); } } /** * Creates user profile and sets the membership profile attributes. * This registration should be done after getting and checking all * registration fields. */ private RegistrationResult registerNewUser() throws AuthLoginException { if (debug.messageEnabled()) { debug.message("trying to register(create) a new user: " + userID); } try { if (userExists(userID)) { if (debug.messageEnabled()) { debug.message("unable to register, user " + userID + " already exists"); } return RegistrationResult.USER_EXISTS_ERROR; } Set vals = new HashSet(); // set user status vals.add(serviceStatus); userAttrs.put("inetuserstatus", vals); createIdentity(userID,userAttrs,defaultRoles); } catch (SSOException ssoe) { debug.error("profile exception occured: ", ssoe); return RegistrationResult.PROFILE_ERROR; } catch (IdRepoException ire) { // log constraint violation message getLoginState("Membership").logFailed(ire.getMessage(), "CREATE_USER_PROFILE_FAILED", false, null); debug.error("profile exception occured: ", ire); return RegistrationResult.PROFILE_ERROR; } validatedUserID = userID; if (debug.messageEnabled()) { debug.message("registration is completed, created user: " + validatedUserID); } return RegistrationResult.NO_ERROR; } /** * Returns and checks registration fields. Returns error state for none * recoverable errors, REGISTRATION for recoverable errors and DISCLAIMER if * completed. */ private ModuleState getAndCheckRegistrationFields(Callback[] callbacks) throws AuthLoginException { // callback[0] is for user name // callback[1] is for new password // callback[2] is for confirm password Map> attrs = new HashMap>(); // get the value of the user name from the input form userID = getCallbackFieldValue(callbacks[0]); // check user name if ((userID == null) || userID.length() == 0) { // no user name was entered, this is required to // create the user's profile updateRegistrationCallbackFields(callbacks); setErrorMessage(RegistrationResult.NO_USER_NAME_ERROR, 0); return ModuleState.REGISTRATION; } //validate username using plugin if any validateUserName(userID, regEx); // get the passwords from the input form String password = getPassword((PasswordCallback)callbacks[1]); String confirmPassword = getPassword((PasswordCallback)callbacks[2]); // check passwords RegistrationResult checkPasswdResult = checkPassword(password, confirmPassword); if (debug.messageEnabled()) { debug.message("state returned from checkPassword(): " + checkPasswdResult); } if (!checkPasswdResult.equals(RegistrationResult.NO_ERROR)) { // the next state to display is returned from checkPassword updateRegistrationCallbackFields(callbacks); setErrorMessage(checkPasswdResult, 1); return ModuleState.REGISTRATION; } // validate password using validation plugin if any validatePassword(confirmPassword); if (password.equals(userID)) { // the user name and password are the same. these fields // must be different updateRegistrationCallbackFields(callbacks); setErrorMessage(RegistrationResult.USER_PASSWORD_SAME_ERROR, 1); return ModuleState.REGISTRATION; } // get the registration fields, also check required fields for (int i = 0; i < callbacks.length; i++) { String attrName = getAttribute(ModuleState.REGISTRATION.intValue(), i); Set values = getCallbackFieldValues(callbacks[i]); if (isRequired(ModuleState.REGISTRATION.intValue(), i)) { if (values.isEmpty()) { if (debug.messageEnabled()) { debug.message("Empty value for required field :" + attrName); } updateRegistrationCallbackFields(callbacks); setErrorMessage(RegistrationResult.MISSING_REQ_FIELD_ERROR, i); return ModuleState.REGISTRATION; } } if (attrName != null && attrName.length() != 0) { attrs.put(attrName, values); } } userAttrs = attrs; // check user ID uniqueness try { if (userExists(userID)) { if (debug.messageEnabled()) { debug.message("user ID " + userID + " already exists"); } // get a list of user IDs from the generator Set generatedUserIDs = getNewUserIDs(attrs, 0); if (generatedUserIDs == null) { // user name generator is disable updateRegistrationCallbackFields(callbacks); setErrorMessage(RegistrationResult.USER_EXISTS_ERROR, 0); return ModuleState.REGISTRATION; } // get a list of user IDs that are not yet being used List nonExistingUserIDs = getNonExistingUserIDs(generatedUserIDs); resetCallback(ModuleState.CHOOSE_USERNAMES.intValue(), 0); Callback[] origCallbacks = getCallback(ModuleState.CHOOSE_USERNAMES.intValue()); ChoiceCallback origCallback = (ChoiceCallback) origCallbacks[0]; String prompt = origCallback.getPrompt(); createMyOwn = origCallback.getChoices()[0]; nonExistingUserIDs.add(createMyOwn); String[] choices = ((String[]) nonExistingUserIDs.toArray(new String[0])); ChoiceCallback callback = new ChoiceCallback(prompt, choices, 0, false); callback.setSelectedIndex(0); replaceCallback(ModuleState.CHOOSE_USERNAMES.intValue(), 0, callback); return ModuleState.CHOOSE_USERNAMES; } } catch (SSOException pe) { debug.error("profile exception occured: ", pe); return ModuleState.PROFILE_ERROR; } catch (IdRepoException pe) { debug.error("profile exception occured: ", pe); return ModuleState.PROFILE_ERROR; } return ModuleState.DISCLAIMER; } /** * Checks the passwords and returned error state or SUCCEEDED if * the passwords are valid. */ private RegistrationResult checkPassword(String password, String confirmPassword) { if ((password == null) || password.length() == 0) { if (debug.messageEnabled()) { debug.message("password was missing from the form"); } return RegistrationResult.NO_PASSWORD_ERROR; } else { // compare the length of the user entered password with // the length required if (password.length() < requiredPasswordLength) { if (debug.messageEnabled()) { debug.message("password was not long enough"); } return RegistrationResult.PASSWORD_TOO_SHORT; } // length OK, now make sure the user entered a confirmation // password if ((confirmPassword == null) || confirmPassword.length() == 0) { if (debug.messageEnabled()) { debug.message("no confirmation password"); } return RegistrationResult.NO_CONFIRMATION_ERROR; } else { // does the confirmation password match the actual password if (!password.equals(confirmPassword)) { // the password and the confirmation password don't match return RegistrationResult.PASSWORD_MISMATCH_ERROR; } } } return RegistrationResult.NO_ERROR; } /** * Returns the user choice user ID and proceed to the next state. */ private ModuleState chooseUserID(Callback[] callbacks) throws AuthLoginException { ModuleState result = null; // callbacks[0] is the choice of the user ID String userChoiceID = getCallbackFieldValue(callbacks[0]); if (userChoiceID.equals(createMyOwn)) { return ModuleState.REGISTRATION; } else { String attrName = getAttribute(ModuleState.REGISTRATION.intValue(), 0); userID = userChoiceID; Set values = new HashSet(); values.add(userID); userAttrs.put(attrName, values); result = processRegistrationResult(); } return result; } /** * Returns the password from the PasswordCallback. */ private String getPassword(PasswordCallback callback) { char[] tmpPassword = callback.getPassword(); if (tmpPassword == null) { // treat a NULL password as an empty password tmpPassword = new char[0]; } char[] pwd = new char[tmpPassword.length]; System.arraycopy(tmpPassword, 0, pwd, 0, tmpPassword.length); return (new String(pwd)); } /** * Returns the input values as a Set for different types of Callback. * An empty Set will be returned if there is no value for the * Callback, or the Callback is not supported. */ private Set getCallbackFieldValues(Callback callback) { Set values = new HashSet(); if (callback instanceof NameCallback) { String value = ((NameCallback)callback).getName(); if (value != null && value.length() != 0) { values.add(value); } } else if (callback instanceof PasswordCallback) { String value = getPassword((PasswordCallback)callback); if (value != null && value.length() != 0) { values.add(value); } } else if (callback instanceof ChoiceCallback) { String[] vals = ((ChoiceCallback)callback).getChoices(); int[] selectedIndexes = ((ChoiceCallback)callback).getSelectedIndexes(); for (int i = 0; i < selectedIndexes.length; i++) { values.add(vals[selectedIndexes[i]]); } } return values; } /** * Returns the first input value for the given Callback. * Returns null if there is no value for the Callback. */ private String getCallbackFieldValue(Callback callback) { Set values = getCallbackFieldValues(callback); Iterator it = values.iterator(); if (it.hasNext()) { return it.next(); } return null; } /** * Returns a list of user IDs from the specified set of user IDs that * are not exist under the specified people container. */ private List getNonExistingUserIDs(Set userIDs) throws IdRepoException, SSOException { List validUserIDs = new ArrayList(); for (String uid : userIDs) { // check if user already exists with the same user ID if (!userExists(uid)) { validUserIDs.add(uid); } } return validUserIDs; } /** check if user exists */ private boolean userExists(String userID) throws IdRepoException, SSOException { AMIdentityRepository amIdRepo = getAMIdentityRepository( getRequestOrg()); IdSearchControl idsc = new IdSearchControl(); idsc.setRecursive(true); idsc.setTimeOut(0); idsc.setAllReturnAttributes(true); // search for the identity Set results = Collections.EMPTY_SET; try { idsc.setMaxResults(0); IdSearchResults searchResults = amIdRepo.searchIdentities(IdType.USER, userID, idsc); if (searchResults != null) { results = searchResults.getSearchResults(); } } catch (IdRepoException e) { if (debug.messageEnabled()) { debug.message("IdRepoException : Error searching " + " Identities with username : " + e.getMessage()); } } return !results.isEmpty(); } private void setErrorMessage(RegistrationResult error, int callback) throws AuthLoginException { if (error.equals(RegistrationResult.PASSWORD_TOO_SHORT)) { String errorText = bundle.getString(error.toString()); String msg = com.sun.identity.shared.locale.Locale. formatMessage(errorText, requiredPasswordLength); substituteInfoText(ModuleState.REGISTRATION.intValue(), callback, msg); } else { substituteInfoText(ModuleState.REGISTRATION.intValue(), callback, bundle.getString(error.toString())); } } private void updateRegistrationCallbackFields(Callback[] submittedCallbacks) throws AuthLoginException { Callback[] origCallbacks = getCallback(ModuleState.REGISTRATION.intValue()); for (int c = 0; c < origCallbacks.length; c++) { if (origCallbacks[c] instanceof NameCallback) { NameCallback nc = (NameCallback) origCallbacks[c]; nc.setName(((NameCallback) submittedCallbacks[c]).getName()); replaceCallback(ModuleState.REGISTRATION.intValue(), c, nc); } else if (origCallbacks[c] instanceof PasswordCallback) { PasswordCallback pc = (PasswordCallback) origCallbacks[c]; pc.setPassword(((PasswordCallback) submittedCallbacks[c]).getPassword()); replaceCallback(ModuleState.REGISTRATION.intValue(), c, pc); } else { continue; } } } }