Java tutorial
/* $This file is distributed under the terms of the license in /doc/license.txt$ */ package edu.cornell.mannlib.vitro.webapp.controller.edit; import static edu.cornell.mannlib.vitro.webapp.beans.UserAccount.MAX_PASSWORD_LENGTH; import static edu.cornell.mannlib.vitro.webapp.beans.UserAccount.MIN_PASSWORD_LENGTH; import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.MLevel.ERROR; import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.State.FORCED_PASSWORD_CHANGE; import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.State.LOGGED_IN; import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.State.LOGGING_IN; import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.State.NOWHERE; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletContext; import javax.servlet.ServletException; 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.hp.hpl.jena.ontology.OntModel; import edu.cornell.mannlib.vedit.beans.LoginStatusBean; import edu.cornell.mannlib.vedit.beans.LoginStatusBean.AuthenticationSource; import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; import edu.cornell.mannlib.vitro.webapp.controller.Controllers; import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator; import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator.LoginNotPermitted; import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LoginInProcessFlag; import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LoginRedirector; import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean; import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.State; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; import edu.cornell.mannlib.vitro.webapp.dao.jena.LoginLogoutEvent; public class Authenticate extends VitroHttpServlet { private static final Log log = LogFactory.getLog(Authenticate.class.getName()); /** * If this is set at any point in the process, store it as the post-login * destination. * * NOTE: we expect URL-encoding on this parameter, and will decode it when * we read it. */ private static final String PARAMETER_AFTER_LOGIN = "afterLogin"; /** * If this is set at any point in the process, store the referrer as the * post-login destination. */ private static final String PARAMETER_RETURN = "return"; /** The username field on the login form. */ private static final String PARAMETER_USERNAME = "loginName"; /** The password field on the login form. */ private static final String PARAMETER_PASSWORD = "loginPassword"; /** The new password field on the password change form. */ private static final String PARAMETER_NEW_PASSWORD = "newPassword"; /** The confirm password field on the password change form. */ private static final String PARAMETER_CONFIRM_PASSWORD = "confirmPassword"; /** If this parameter is "true" (ignoring case), cancel the login. */ private static final String PARAMETER_CANCEL = "cancel"; /** If this parameter is set, we are not NOWHERE. */ private static final String PARAMETER_LOGIN_FORM = "loginForm"; /** Where do we find the User/Session map in the servlet context? */ public static final String USER_SESSION_MAP_ATTR = "userURISessionMap"; /** * Find out where they are in the login process, process any input, record * the new state, and show the next page. */ @Override public void doPost(HttpServletRequest request, HttpServletResponse response) { VitroRequest vreq = new VitroRequest(request); try { if (loginProcessIsRestarting(vreq)) { LoginProcessBean.removeBean(vreq); } if (loginProcessPagesAreEmpty(vreq)) { recordLoginProcessPages(vreq); } // Where do we stand in the process? State entryState = getCurrentLoginState(vreq); dumpStateToLog("entry", entryState, vreq); // Act on any input. switch (entryState) { case NOWHERE: processInputNowhere(vreq); break; case LOGGING_IN: processInputLoggingIn(vreq); break; case FORCED_PASSWORD_CHANGE: processInputChangePassword(vreq); break; default: // LOGGED_IN: processInputLoggedIn(vreq); break; } // Now where do we stand? State exitState = getCurrentLoginState(vreq); dumpStateToLog("exit", exitState, vreq); // Send them on their way. switch (exitState) { case NOWHERE: showLoginCanceled(response, vreq); break; case LOGGING_IN: showLoginScreen(vreq, response); break; case FORCED_PASSWORD_CHANGE: showLoginScreen(vreq, response); break; default: // LOGGED_IN: showLoginComplete(response, vreq); break; } } catch (Exception e) { showSystemError(e, response); } } /** * The after-login page or the return flag are supplied only on the first * step in the process. If we see either of them, we conclude that the user * has re-started the login. */ private boolean loginProcessIsRestarting(HttpServletRequest request) { if (isAfterLoginParameterSet(request)) { log.debug("after-login parameter is set: restarting the login."); return true; } if (isReturnParameterSet(request)) { log.debug("return parameter is set: restarting the login."); return true; } return false; } /** * Once these URLs have been set, don't change them. */ private boolean loginProcessPagesAreEmpty(HttpServletRequest request) { LoginProcessBean bean = LoginProcessBean.getBean(request); return ((bean.getAfterLoginUrl() == null) && (bean.getLoginPageUrl() == null)); } /** * If they supply an after-login page, record it and use the Login page for * the process. Note that we expect it to be URL-encoded. * * If they supply a return flag, record the current page as the after-login * page and use the Login page for the process. * * Otherwise, use the current page for the process. * * The "current page" is the referrer, unless there is no referrer for some * reason. In that case, pretend it's the login page. */ private void recordLoginProcessPages(HttpServletRequest request) { LoginProcessBean bean = LoginProcessBean.getBean(request); String afterLoginUrl = decodeAfterLoginParameter(request); boolean doReturn = isReturnParameterSet(request); String referrer = whereDidWeComeFrom(request); if (afterLoginUrl != null) { bean.setAfterLoginUrl(afterLoginUrl); bean.setLoginPageUrl(request.getContextPath() + Controllers.LOGIN); } else if (doReturn) { bean.setAfterLoginUrl(referrer); bean.setLoginPageUrl(request.getContextPath() + Controllers.LOGIN); } else { bean.setAfterLoginUrl(referrer); bean.setLoginPageUrl(referrer); } } private String decodeAfterLoginParameter(HttpServletRequest request) { String parm = request.getParameter(PARAMETER_AFTER_LOGIN); if (parm == null) { return null; } else { try { return URLDecoder.decode(parm, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error("No UTF-8 encoding? Really?", e); return parm; } } } private boolean isAfterLoginParameterSet(HttpServletRequest request) { return (null != request.getParameter(PARAMETER_AFTER_LOGIN)); } private boolean isReturnParameterSet(HttpServletRequest request) { return (null != request.getParameter(PARAMETER_RETURN)); } /** If no referrer, say we were on the login page. */ private String whereDidWeComeFrom(HttpServletRequest request) { String referrer = request.getHeader("referer"); if (referrer != null) { return referrer; } else { return request.getContextPath() + Controllers.LOGIN; } } /** * Where are we in the process? Logged in? Not? Somewhere in between? */ private State getCurrentLoginState(HttpServletRequest request) { State currentState; HttpSession session = request.getSession(false); if (session == null) { currentState = NOWHERE; log.debug("no session: current state is NOWHERE"); } else if (LoginStatusBean.getBean(request).isLoggedIn()) { currentState = LOGGED_IN; log.debug("found a LoginStatusBean: current state is LOGGED IN"); } else if (LoginProcessBean.isBean(request)) { currentState = LoginProcessBean.getBean(request).getState(); log.debug("state from LoginProcessBean is " + currentState); } else { currentState = NOWHERE; log.debug("no LoginSessionBean, no LoginProcessBean: " + "current state is NOWHERE"); } if (weCameFromAColdWidget(request, currentState)) { currentState = actLikeWeWereLoggingIn(request); } return currentState; } /** * If they submitted the login form, they shouldn't be NOWHERE. */ private boolean weCameFromAColdWidget(HttpServletRequest request, State currentState) { if (currentState == NOWHERE) { if (null != request.getParameter(PARAMETER_LOGIN_FORM)) { return true; } } return false; } /** * They got here by submitting the login form. They should be treated as * already logging in. */ private State actLikeWeWereLoggingIn(HttpServletRequest request) { LoginProcessBean bean = new LoginProcessBean(); bean.setState(LOGGING_IN); bean.setLoginPageUrl(whereDidWeComeFrom(request)); bean.setAfterLoginUrl(whereDidWeComeFrom(request)); LoginProcessBean.setBean(request, bean); return LOGGING_IN; } /** * They just got here. Start the process. */ private void processInputNowhere(HttpServletRequest request) { transitionToLoggingIn(request); } /** * They are logging in. If they get it wrong, let them know. If they get it * right, record it. */ private void processInputLoggingIn(HttpServletRequest request) { String username = request.getParameter(PARAMETER_USERNAME); String password = request.getParameter(PARAMETER_PASSWORD); LoginProcessBean bean = LoginProcessBean.getBean(request); bean.clearMessage(); log.trace("username=" + username + ", password=" + password + ", bean=" + bean); if ((username == null) || username.isEmpty()) { bean.setMessage(request, ERROR, "error_no_email_address"); return; } bean.setUsername(username); UserAccount user = getAuthenticator(request).getAccountForInternalAuth(username); log.trace("User is " + (user == null ? "null" : user.getUri())); if (user == null) { bean.setMessage(request, ERROR, "error_incorrect_credentials"); return; } if ((password == null) || password.isEmpty()) { bean.setMessage(request, ERROR, "error_no_password"); return; } if (!getAuthenticator(request).isUserPermittedToLogin(user)) { bean.setMessage(request, ERROR, "logins_disabled_for_maintenance"); return; } if (!getAuthenticator(request).isCurrentPassword(user, password)) { bean.setMessage(request, ERROR, "error_incorrect_credentials"); return; } // Username and password are correct. What next? if (user.isPasswordChangeRequired()) { transitionToForcedPasswordChange(request); } else { try { transitionToLoggedIn(request, user); } catch (LoginNotPermitted e) { // This should have been caught by isUserPermittedToLogin() bean.setMessage(request, ERROR, "logins_disabled_for_maintenance"); return; } } } /** * <pre> * They are changing passwords. * - If they cancel, let them out without checking for problems. * - Otherwise, * - If they get it wrong, let them know. * - If they get it right, record it. * </pre> */ private void processInputChangePassword(HttpServletRequest request) { String newPassword = request.getParameter(PARAMETER_NEW_PASSWORD); String confirm = request.getParameter(PARAMETER_CONFIRM_PASSWORD); String cancel = request.getParameter(PARAMETER_CANCEL); if (Boolean.valueOf(cancel)) { // It's over, man. Let them go. transitionToNowhere(request); return; } LoginProcessBean bean = LoginProcessBean.getBean(request); bean.clearMessage(); log.trace("newPassword=" + newPassword + ", confirm=" + confirm + ", bean=" + bean); if ((newPassword == null) || newPassword.isEmpty()) { bean.setMessage(request, ERROR, "error_no_new_password"); return; } if (!newPassword.equals(confirm)) { bean.setMessage(request, ERROR, "error_passwords_dont_match"); return; } if ((newPassword.length() < MIN_PASSWORD_LENGTH) || (newPassword.length() > MAX_PASSWORD_LENGTH)) { bean.setMessage(request, ERROR, "error_password_length", MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH); return; } String username = bean.getUsername(); UserAccount user = getAuthenticator(request).getAccountForInternalAuth(username); if (getAuthenticator(request).isCurrentPassword(user, newPassword)) { bean.setMessage(request, ERROR, "error_previous_password"); return; } // New password is acceptable. Store it and go on. try { transitionToLoggedIn(request, user, newPassword); } catch (LoginNotPermitted e) { // This should have been caught by isUserPermittedToLogin() bean.setMessage(request, ERROR, "logins_disabled_for_maintenance"); return; } } /** * They are already logged in. */ @SuppressWarnings("unused") private void processInputLoggedIn(HttpServletRequest request) { // Nothing to do. No transition. } /** * State change: they are starting the login process. */ private void transitionToLoggingIn(HttpServletRequest request) { log.debug("Starting the login process."); LoginProcessBean.getBean(request).setState(LOGGING_IN); } /** * State change: username and password were correct, but now we require a * new password. */ private void transitionToForcedPasswordChange(HttpServletRequest request) { log.debug("Forcing first-time password change"); LoginProcessBean.getBean(request).setState(FORCED_PASSWORD_CHANGE); } /** * State change: all requirements are satisfied. Log them in. */ private void transitionToLoggedIn(HttpServletRequest request, UserAccount user) throws LoginNotPermitted { log.debug("Completed login: " + user.getEmailAddress()); getAuthenticator(request).recordLoginAgainstUserAccount(user, AuthenticationSource.INTERNAL); } /** * State change: all requirements are satisfied. Change their password and * log them in. */ private void transitionToLoggedIn(HttpServletRequest request, UserAccount user, String newPassword) throws LoginNotPermitted { log.debug("Completed login: " + user.getEmailAddress() + ", password changed."); getAuthenticator(request).recordNewPassword(user, newPassword); getAuthenticator(request).recordLoginAgainstUserAccount(user, AuthenticationSource.INTERNAL); } /** * State change: they decided to cancel the login. */ private void transitionToNowhere(HttpServletRequest request) { LoginProcessBean.getBean(request).setState(NOWHERE); log.debug("Cancelling login."); } /** * Exit: there has been an unexpected exception, so show it. */ private void showSystemError(Exception e, HttpServletResponse response) { log.error("Unexpected error in login process", e); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } catch (IOException e1) { log.error(e1, e1); } } /** * Exit: user is still logging in, so go back to the page they were on. */ private void showLoginScreen(VitroRequest vreq, HttpServletResponse response) throws IOException { log.debug("logging in."); LoginInProcessFlag.set(vreq); String loginProcessPage = LoginProcessBean.getBean(vreq).getLoginPageUrl(); response.sendRedirect(loginProcessPage); return; } /** * Exit: user has completed the login. Redirect appropriately and clear the * bean. */ private void showLoginComplete(HttpServletResponse response, VitroRequest vreq) throws IOException { getLoginRedirector(vreq).redirectLoggedInUser(response); LoginProcessBean.removeBean(vreq); } /** * Exit: user has canceled. Redirect and clear the bean. */ private void showLoginCanceled(HttpServletResponse response, VitroRequest vreq) throws IOException { getLoginRedirector(vreq).redirectCancellingUser(response); LoginProcessBean.removeBean(vreq); } private LoginRedirector getLoginRedirector(VitroRequest vreq) { String afterLoginUrl = LoginProcessBean.getBean(vreq).getAfterLoginUrl(); return new LoginRedirector(vreq, afterLoginUrl); } /** Get a reference to the Authenticator. */ private Authenticator getAuthenticator(HttpServletRequest request) { return Authenticator.getInstance(request); } // ---------------------------------------------------------------------- // Public utility methods. // ---------------------------------------------------------------------- /** * The servlet context should contain a map from User URIs to * {@link HttpSession}s. Get a reference to it, creating it if necessary. */ @SuppressWarnings("unchecked") public static Map<String, HttpSession> getUserURISessionMapFromContext(ServletContext ctx) { Map<String, HttpSession> m = (Map<String, HttpSession>) ctx.getAttribute(USER_SESSION_MAP_ATTR); if (m == null) { m = new HashMap<String, HttpSession>(); ctx.setAttribute(USER_SESSION_MAP_ATTR, m); } return m; } /** * Let everyone know that somebody has logged in or logged out. */ public static void sendLoginNotifyEvent(LoginLogoutEvent event, ServletContext context, HttpSession session) { if (event == null) { log.warn("Unable to notify audit model of login " + "because a null event was passed"); return; } OntModel jenaOntModel = ModelAccess.on(context).getOntModel(); jenaOntModel.getBaseModel().notifyEvent(event); } private void dumpStateToLog(String label, State state, VitroRequest vreq) { log.debug("State on " + label + ": " + state); if (log.isTraceEnabled()) { log.trace("Status bean on " + label + ": " + LoginStatusBean.getBean(vreq)); LoginProcessBean processBean = null; if (LoginProcessBean.isBean(vreq)) { processBean = LoginProcessBean.getBean(vreq); } log.trace("Process bean on " + label + ": " + processBean); } } @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doPost(request, response); } }