NIHVIVO-151 Get better control of redirection after logging in.

1) The login link goes to the controller, not directly to the form.
2) The process bean holds the URL where the form is located, and the URL we will go to on success.
3) Only the controller alters the state of the bean, not the widget.
4) The process bean is kept until the redirector can get information from it.
5) Finally, with more control in the redirector, change the behavior.
This commit is contained in:
jeb228 2010-12-08 22:14:39 +00:00
parent ba3163da20
commit 8676ec5544
8 changed files with 571 additions and 314 deletions

View file

@ -15,7 +15,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean.AuthenticationSource;
import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean;
/**
* Handle the return from the external authorization login server. If we are
@ -74,7 +73,6 @@ public class LoginExternalAuthReturn extends BaseLoginServlet {
}
private void removeLoginProcessArtifacts(HttpServletRequest req) {
LoginProcessBean.removeBean(req);
req.getSession().removeAttribute(ATTRIBUTE_REFERRER);
}

View file

@ -17,6 +17,7 @@ import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vitro.webapp.beans.DisplayMessage;
import edu.cornell.mannlib.vitro.webapp.controller.Controllers;
import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean;
/**
* A user has just completed the login process. What page do we direct them to?
@ -24,14 +25,13 @@ import edu.cornell.mannlib.vitro.webapp.controller.Controllers;
public class LoginRedirector {
private static final Log log = LogFactory.getLog(LoginRedirector.class);
private static final String ATTRIBUTE_RETURN_FROM_FORCED_LOGIN = "return_from_forced_login";
private final HttpServletRequest request;
private final HttpServletResponse response;
private final HttpSession session;
private final String urlOfRestrictedPage;
private final String uriOfAssociatedIndividual;
private final String loginProcessPage;
private final String afterLoginPage;
public LoginRedirector(HttpServletRequest request,
HttpServletResponse response) {
@ -39,18 +39,12 @@ public class LoginRedirector {
this.session = request.getSession();
this.response = response;
urlOfRestrictedPage = getUrlOfRestrictedPage();
uriOfAssociatedIndividual = getAssociatedIndividualUri();
}
/** Were we forced to log in when trying to access a restricted page? */
private String getUrlOfRestrictedPage() {
String url = (String) session
.getAttribute(ATTRIBUTE_RETURN_FROM_FORCED_LOGIN);
session.removeAttribute(ATTRIBUTE_RETURN_FROM_FORCED_LOGIN);
log.debug("URL of restricted page is " + url);
return url;
LoginProcessBean processBean = LoginProcessBean.getBean(request);
log.debug("process bean is: " + processBean);
loginProcessPage = processBean.getLoginPageUrl();
afterLoginPage = processBean.getAfterLoginUrl();
}
/** Is there an Individual associated with this user? */
@ -76,30 +70,79 @@ public class LoginRedirector {
}
public void redirectLoggedInUser() throws IOException {
if (isForcedFromRestrictedPage()) {
log.debug("Returning to restricted page.");
response.sendRedirect(urlOfRestrictedPage);
} else if (isUserEditorOrBetter()) {
log.debug("Going to site admin page.");
response.sendRedirect(getSiteAdminPageUrl());
} else if (isSelfEditorWithIndividual()) {
log.debug("Going to Individual home page.");
response.sendRedirect(getAssociatedIndividualHomePage());
} else {
log.debug("User not recognized. Going to application home.");
DisplayMessage.setMessage(request, "You have logged in, "
+ "but the system contains no profile for you.");
try {
if (isSelfEditorWithIndividual()) {
log.debug("Going to Individual home page.");
response.sendRedirect(getAssociatedIndividualHomePage());
} else if (isMerelySelfEditor()) {
log.debug("User not recognized. Going to application home.");
DisplayMessage.setMessage(request, "You have logged in, "
+ "but the system contains no profile for you.");
response.sendRedirect(getApplicationHomePageUrl());
} else {
if (hasSomeplaceToGoAfterLogin()) {
log.debug("Returning to requested page: " + afterLoginPage);
response.sendRedirect(afterLoginPage);
} else if (loginProcessPage == null) {
log.debug("Don't know what to do. Go home.");
response.sendRedirect(getApplicationHomePageUrl());
} else if (isLoginPage(loginProcessPage)) {
log.debug("Coming from /login. Going to site admin page.");
response.sendRedirect(getSiteAdminPageUrl());
} else {
log.debug("Coming from a login widget. Going back there.");
response.sendRedirect(loginProcessPage);
}
}
LoginProcessBean.removeBean(request);
} catch (IOException e) {
log.debug("Problem with re-direction", e);
response.sendRedirect(getApplicationHomePageUrl());
}
}
private boolean isForcedFromRestrictedPage() {
return urlOfRestrictedPage != null;
public void redirectCancellingUser() throws IOException {
try {
if (hasSomeplaceToGoAfterLogin()) {
log.debug("Returning to requested page: " + afterLoginPage);
response.sendRedirect(afterLoginPage);
} else if (loginProcessPage == null) {
log.debug("Don't know what to do. Go home.");
response.sendRedirect(getApplicationHomePageUrl());
} else if (isLoginPage(loginProcessPage)) {
log.debug("Coming from /login. Going to home.");
response.sendRedirect(getApplicationHomePageUrl());
} else {
log.debug("Coming from a login widget. Going back there.");
response.sendRedirect(loginProcessPage);
}
LoginProcessBean.removeBean(request);
} catch (IOException e) {
log.debug("Problem with re-direction", e);
response.sendRedirect(getApplicationHomePageUrl());
}
}
private boolean isUserEditorOrBetter() {
return LoginStatusBean.getBean(session).isLoggedInAtLeast(
LoginStatusBean.EDITOR);
public void redirectUnrecognizedExternalUser(String username)
throws IOException {
log.debug("Redirecting unrecognized external user: " + username);
DisplayMessage.setMessage(request,
"VIVO cannot find a profile for your account.");
response.sendRedirect(getApplicationHomePageUrl());
}
private boolean hasSomeplaceToGoAfterLogin() {
return afterLoginPage != null;
}
private boolean isMerelySelfEditor() {
return LoginStatusBean.getBean(session).isLoggedInExactly(
LoginStatusBean.NON_EDITOR);
}
private boolean isLoginPage(String page) {
return ((page != null) && page.endsWith(request.getContextPath()
+ Controllers.LOGIN));
}
private String getSiteAdminPageUrl() {
@ -120,14 +163,6 @@ public class LoginRedirector {
}
}
public void redirectUnrecognizedExternalUser(String username)
throws IOException {
log.debug("Redirecting unrecognized external user: " + username);
DisplayMessage.setMessage(request,
"VIVO cannot find a profile for your account.");
response.sendRedirect(getApplicationHomePageUrl());
}
/**
* The application home page can be overridden by an attribute in the
* ServletContext. Further, it can either be an absolute URL, or it can be
@ -145,14 +180,4 @@ public class LoginRedirector {
}
return request.getContextPath();
}
// ----------------------------------------------------------------------
// static helper methods
// ----------------------------------------------------------------------
public static void setReturnUrlFromForcedLogin(HttpServletRequest request,
String url) {
request.getSession().setAttribute(ATTRIBUTE_RETURN_FROM_FORCED_LOGIN,
url);
}
}

View file

@ -8,6 +8,8 @@ import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean
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.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
@ -28,6 +30,7 @@ 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.User;
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;
@ -41,6 +44,24 @@ 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";
/** If this is set, a status of NOWHERE should be treated as LOGGING_IN. */
private static final String PARAMETER_LOGGING_IN = "loginForm";
/** The username field on the login form. */
private static final String PARAMETER_USERNAME = "loginName";
@ -68,6 +89,8 @@ public class Authenticate extends VitroHttpServlet {
VitroRequest vreq = new VitroRequest(request);
try {
recordLoginProcessPages(vreq);
// Where do we stand in the process?
State entryState = getCurrentLoginState(vreq);
dumpStateToLog("entry", entryState, vreq);
@ -95,7 +118,7 @@ public class Authenticate extends VitroHttpServlet {
// Send them on their way.
switch (exitState) {
case NOWHERE:
redirectCancellingUser(vreq, response);
new LoginRedirector(vreq, response).redirectCancellingUser();
break;
case LOGGING_IN:
showLoginScreen(vreq, response);
@ -113,30 +136,77 @@ public class Authenticate extends VitroHttpServlet {
}
/**
* If they supply an after-login page, record it and use the Login page for
* the process.
*
* If they supply a return flag, record the referrer as the after-login page
* and use the Login page for the process.
*
* Otherwise, use the current page for the process.
*/
private void recordLoginProcessPages(HttpServletRequest request) {
LoginProcessBean bean = LoginProcessBean.getBean(request);
String afterLoginUrl = request.getParameter(PARAMETER_AFTER_LOGIN);
if (afterLoginUrl != null) {
try {
String decoded = URLDecoder.decode(afterLoginUrl, "UTF-8");
bean.setAfterLoginUrl(decoded);
} catch (UnsupportedEncodingException e) {
log.error("Really? No UTF-8 encoding?");
}
}
String returnParameter = request.getParameter(PARAMETER_RETURN);
if (returnParameter != null) {
String referrer = request.getHeader("referer");
bean.setAfterLoginUrl(referrer);
}
if (bean.getAfterLoginUrl() != null) {
bean.setLoginPageUrl(request.getContextPath() + Controllers.LOGIN);
} else {
bean.setLoginPageUrl(request.getHeader("referer"));
}
}
/**
* 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");
return NOWHERE;
}
if (LoginStatusBean.getBean(request).isLoggedIn()) {
} else if (LoginStatusBean.getBean(request).isLoggedIn()) {
currentState = LOGGED_IN;
log.debug("found a LoginStatusBean: current state is LOGGED IN");
return LOGGED_IN;
}
if (LoginProcessBean.isBean(request)) {
State state = LoginProcessBean.getBean(request).getState();
log.debug("state from LoginProcessBean is " + state);
return state;
} 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");
return NOWHERE;
}
if ((currentState == NOWHERE) && isLoggingInByParameter(request)) {
currentState = LOGGING_IN;
log.debug("forced from NOWHERE to LOGGING_IN by '"
+ PARAMETER_LOGGING_IN + "' parameter");
}
return currentState;
}
/**
* If this parameter is present, we aren't NOWHERE.
*/
private boolean isLoggingInByParameter(HttpServletRequest request) {
return (request.getParameter(PARAMETER_LOGGING_IN) != null);
}
/**
@ -288,7 +358,6 @@ public class Authenticate extends VitroHttpServlet {
log.debug("Completed login: " + username);
getAuthenticator(request).recordLoginAgainstUserAccount(username,
AuthenticationSource.INTERNAL);
LoginProcessBean.removeBean(request);
}
/**
@ -301,15 +370,14 @@ public class Authenticate extends VitroHttpServlet {
getAuthenticator(request).recordNewPassword(username, newPassword);
getAuthenticator(request).recordLoginAgainstUserAccount(username,
AuthenticationSource.INTERNAL);
LoginProcessBean.removeBean(request);
}
/**
* State change: they decided to cancel the login.
*/
private void transitionToNowhere(HttpServletRequest request) {
LoginProcessBean.getBean(request).setState(NOWHERE);
log.debug("Cancelling login.");
LoginProcessBean.removeBean(request);
}
/**
@ -331,35 +399,17 @@ public class Authenticate extends VitroHttpServlet {
throws IOException {
log.debug("logging in.");
String referringPage = vreq.getHeader("referer");
if (referringPage == null) {
log.warn("No referring page on the request");
referringPage = getHomeUrl(vreq);
}
response.sendRedirect(referringPage);
String loginProcessPage = LoginProcessBean.getBean(vreq)
.getLoginPageUrl();
response.sendRedirect(loginProcessPage);
return;
}
/**
* Exit: user cancelled the login, so show them the home page.
*/
private void redirectCancellingUser(HttpServletRequest request,
HttpServletResponse response) throws IOException {
log.debug("User cancelled the login. Redirect to site admin page.");
LoginProcessBean.removeBean(request);
response.sendRedirect(getHomeUrl(request));
}
/** Get a reference to the Authenticator. */
private Authenticator getAuthenticator(HttpServletRequest request) {
return Authenticator.getInstance(request);
}
/** What's the URL for the home page? */
private String getHomeUrl(HttpServletRequest request) {
return request.getContextPath();
}
// ----------------------------------------------------------------------
// Public utility methods.
// ----------------------------------------------------------------------
@ -422,7 +472,7 @@ public class Authenticate extends VitroHttpServlet {
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));

View file

@ -319,7 +319,7 @@ public class FreemarkerHttpServlet extends VitroHttpServlet {
}
urls.put("search", urlBuilder.getPortalUrl(Route.SEARCH));
urls.put("termsOfUse", urlBuilder.getPortalUrl(Route.TERMS_OF_USE));
urls.put("login", urlBuilder.getPortalUrl(Route.LOGIN));
urls.put("login", urlBuilder.getLoginUrl());
urls.put("logout", urlBuilder.getLogoutUrl());
urls.put("siteAdmin", urlBuilder.getPortalUrl(Route.SITE_ADMIN));
urls.put("siteIcons", urlBuilder.getPortalUrl(themeDir + "/site_icons")); // deprecated

View file

@ -26,6 +26,7 @@ public class UrlBuilder {
public enum Route {
ABOUT("/about"),
AUTHENTICATE("/authenticate"),
BROWSE("/browse"),
CONTACT("/contact"),
INDIVIDUAL("/individual"),
@ -120,6 +121,10 @@ public class UrlBuilder {
return contextPath;
}
public String getLoginUrl() {
return getPortalUrl(Route.AUTHENTICATE, "return", "true");
}
public String getLogoutUrl() {
return getPortalUrl(Route.LOGOUT);
}

View file

@ -164,6 +164,12 @@ public class LoginProcessBean {
/** What arguments are needed to format the message? */
private Object[] messageArguments = NO_ARGUMENTS;
/** Where is the interaction taking place? */
private String loginPageUrl;
/** Where do we go when finished? */
private String afterLoginUrl;
/**
* What username was submitted to the form? This isn't just for display --
* if they are changing passwords, we need to remember who it is.
@ -214,12 +220,29 @@ public class LoginProcessBean {
this.username = username;
}
public String getLoginPageUrl() {
return loginPageUrl;
}
public void setLoginPageUrl(String loginPageUrl) {
this.loginPageUrl = loginPageUrl;
}
public String getAfterLoginUrl() {
return afterLoginUrl;
}
public void setAfterLoginUrl(String afterLoginUrl) {
this.afterLoginUrl = afterLoginUrl;
}
@Override
public String toString() {
return "LoginProcessBean[state=" + currentState + ", message="
+ message + ", messageArguments="
+ Arrays.deepToString(messageArguments) + ", username="
+ username + "]";
+ username + ", loginPageUrl=" + loginPageUrl
+ ", afterLoginUrl=" + afterLoginUrl + "]";
}
}

View file

@ -106,14 +106,11 @@ public class LoginWidget extends Widget {
}
/**
* User is just starting the login process. Be sure that we have a
* {@link LoginProcessBean} with the correct status. Show them the login
* screen.
* User is starting the login process. Show them the login screen.
*/
private WidgetTemplateValues showLoginScreen(HttpServletRequest request)
throws IOException {
LoginProcessBean bean = LoginProcessBean.getBean(request);
bean.setState(State.LOGGING_IN);
log.trace("Going to login screen: " + bean);
WidgetTemplateValues values = new WidgetTemplateValues(Macro.LOGIN.toString());
@ -150,7 +147,6 @@ public class LoginWidget extends Widget {
*/
private WidgetTemplateValues showPasswordChangeScreen(HttpServletRequest request) {
LoginProcessBean bean = LoginProcessBean.getBean(request);
bean.setState(State.FORCED_PASSWORD_CHANGE);
log.trace("Going to password change screen: " + bean);
WidgetTemplateValues values = new WidgetTemplateValues(

View file

@ -9,6 +9,7 @@ import static org.junit.Assert.fail;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@ -17,6 +18,9 @@ import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import stubs.javax.servlet.ServletConfigStub;
import stubs.javax.servlet.ServletContextStub;
@ -27,38 +31,204 @@ import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean.AuthenticationSource;
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
import edu.cornell.mannlib.vitro.webapp.beans.User;
import edu.cornell.mannlib.vitro.webapp.controller.Controllers;
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.AuthenticatorStub;
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;
/**
* Test the Authentate class.
* <pre>
* Test the Authenticate class.
*
* This uses parameterized unit tests. Several sets of test data are set up, and
* then each test is run with each set of data.
*
* Each set of test data includes
* information about the user who is logging in,
* information about how the user began the login process
* information about where the user should end up
*
* We run the tests with these users:
* A DBA who has never logged in before
* A DBA who has logged in before
* A self-editor who has logged in before
* A self-editor wannabe, who has never logged in and has no profile.
*
* We run the tests with the assumption that the user started from:
* The login page
* A page that holds the login widget
* A forced login
* A login link on some page
* </pre>
*/
@RunWith(value = Parameterized.class)
public class AuthenticateTest extends AbstractTestClass {
private static final String USER_DBA_NAME = "dbaName";
private static final String USER_DBA_URI = "dbaURI";
private static final String USER_DBA_PASSWORD = "dbaPassword";
private static final String USER_OLDHAND_NAME = "oldHandName";
private static final String USER_OLDHAND_URI = "oldHandURI";
private static final String USER_OLDHAND_PASSWORD = "oldHandPassword";
private static final int USER_OLDHAND_LOGIN_COUNT = 100;
// ----------------------------------------------------------------------
// Helper classes
// ----------------------------------------------------------------------
private static final String URL_LOGIN_PAGE = "http://my.local.site/vivo/"
+ Controllers.LOGIN;
private static final String URL_SITE_ADMIN_PAGE = Controllers.SITE_ADMIN;
private static class UserInfo {
final String username;
final String uri;
final String password;
final int securityLevel;
final int loginCount;
private static final String URL_HOME_PAGE = "";
private static final String URL_SESSION_REDIRECT = "/sessionRedirect";
private static final String URL_CONTEXT_REDIRECT_LOCAL = "/servletContextRedirect";
private static final String URL_CONTEXT_REDIRECT_REMOTE = "http://servletContextRedirect";
private static final String URL_SELF_EDITOR_PAGE = "/individual?uri=selfEditorURI";
public UserInfo(String username, String uri, String password,
int securityLevel, int loginCount) {
this.username = username;
this.uri = uri;
this.password = password;
this.securityLevel = securityLevel;
this.loginCount = loginCount;
}
private static final LoginStatusBean LOGIN_STATUS_DBA = new LoginStatusBean(
USER_DBA_URI, USER_DBA_NAME, LoginStatusBean.DBA,
AuthenticationSource.INTERNAL);
@Override
public String toString() {
return "UserInfo[username=" + username + ", uri=" + uri
+ ", password=" + password + ", securityLevel="
+ securityLevel + ", loginCount=" + loginCount + "]";
}
}
private static class HowDidWeGetHere {
final String afterLoginUrl;
final boolean returnParameterSet;
final String referrer;
public HowDidWeGetHere(String afterLoginUrl,
boolean returnParameterSet, String referrer) {
this.afterLoginUrl = afterLoginUrl;
this.returnParameterSet = returnParameterSet;
this.referrer = referrer;
}
@Override
public String toString() {
return "HowDidWeGetHere[afterLoginUrl=" + afterLoginUrl
+ ", returnParameterSet=" + returnParameterSet
+ ", referrer=" + referrer + "]";
}
}
private static class WhereTo {
final String expectedContinueUrl;
final String expectedCompletionUrl;
final String expectedCancelUrl;
public WhereTo(String expectedContinueUrl,
String expectedCompletionUrl, String expectedCancelUrl) {
this.expectedContinueUrl = expectedContinueUrl;
this.expectedCompletionUrl = expectedCompletionUrl;
this.expectedCancelUrl = expectedCancelUrl;
}
@Override
public String toString() {
return "WhereTo[expectedContinueUrl=" + expectedContinueUrl
+ ", expectedCompletionUrl=" + expectedCompletionUrl
+ ", expectedCancelUrl=" + expectedCancelUrl + "]";
}
}
// ----------------------------------------------------------------------
// The parameters
// ----------------------------------------------------------------------
// --------- Pages ----------
/** the login page */
private static final String URL_LOGIN = "/vivo/login";
/** some page with a login widget on it. */
private static final String URL_WIDGET = "/vivo/widgetPage";
/** a restricted page that forces a login. */
private static final String URL_RESTRICTED = "/vivo/otherPage";
/** a page with a login link. */
private static final String URL_LINK = "/vivo/linkPage";
// pages that we might end up on.
private static final String URL_HOME = "/vivo";
private static final String URL_SITE_ADMIN = "/vivo/siteAdmin";
private static final String URL_SELF_PROFILE = "/vivo/individual?uri=old_self_associated_uri";
// --------- Users ----------
/** A DBA who has never logged in (forces password change). */
private static final UserInfo NEW_DBA = new UserInfo("new_dba_name",
"new_dba_uri", "new_dba_pw", 50, 0);
/** A DBA who has logged in before. */
private static final UserInfo OLD_DBA = new UserInfo("old_dba_name",
"old_dba_uri", "old_dba_pw", 50, 5);
/** A self-editor who has logged in before and has a profile. */
private static final UserInfo OLD_SELF = new UserInfo("old_self_name",
"old_self_uri", "old_self_pw", 1, 100);
/** A self-editor who has never logged in and has no profile. */
private static final UserInfo NEW_STRANGER = new UserInfo(
"new_stranger_name", "new_stranger_uri", "stranger_pw", 1, 0);
// --------- Starting circumstances ----------
private static final HowDidWeGetHere FROM_FORCED = new HowDidWeGetHere(
URL_RESTRICTED, false, URL_RESTRICTED);
private static final HowDidWeGetHere FROM_LINK = new HowDidWeGetHere(null,
true, URL_LINK);
private static final HowDidWeGetHere FROM_WIDGET = new HowDidWeGetHere(
null, false, URL_WIDGET);
private static final HowDidWeGetHere FROM_LOGIN = new HowDidWeGetHere(null,
false, URL_LOGIN);
// --------- All sets of test data ----------
@Parameters
public static Collection<Object[]> data() {
Object[][] data = new Object[][] {
{ NEW_DBA, FROM_FORCED,
new WhereTo(URL_LOGIN, URL_RESTRICTED, URL_RESTRICTED) }, // 0
{ NEW_DBA, FROM_LINK,
new WhereTo(URL_LOGIN, URL_LINK, URL_LINK) }, // 1
{ NEW_DBA, FROM_WIDGET,
new WhereTo(URL_WIDGET, URL_WIDGET, URL_WIDGET) }, // 2
{ NEW_DBA, FROM_LOGIN,
new WhereTo(URL_LOGIN, URL_SITE_ADMIN, URL_HOME) }, // 3
{ OLD_DBA, FROM_FORCED,
new WhereTo(URL_LOGIN, URL_RESTRICTED, null) }, // 4
{ OLD_DBA, FROM_LINK, new WhereTo(URL_LOGIN, URL_LINK, null) }, // 5
{ OLD_DBA, FROM_WIDGET,
new WhereTo(URL_WIDGET, URL_WIDGET, null) }, // 6
{ OLD_DBA, FROM_LOGIN,
new WhereTo(URL_LOGIN, URL_SITE_ADMIN, null) }, // 7
{ OLD_SELF, FROM_FORCED,
new WhereTo(URL_LOGIN, URL_SELF_PROFILE, null) }, // 8
{ OLD_SELF, FROM_LINK,
new WhereTo(URL_LOGIN, URL_SELF_PROFILE, null) }, // 9
{ OLD_SELF, FROM_WIDGET,
new WhereTo(URL_WIDGET, URL_SELF_PROFILE, null) }, // 10
{ OLD_SELF, FROM_LOGIN,
new WhereTo(URL_LOGIN, URL_SELF_PROFILE, null) }, // 11
{ NEW_STRANGER, FROM_FORCED,
new WhereTo(URL_LOGIN, URL_HOME, URL_RESTRICTED) }, // 12
{ NEW_STRANGER, FROM_LINK,
new WhereTo(URL_LOGIN, URL_HOME, URL_LINK) }, // 13
{ NEW_STRANGER, FROM_WIDGET,
new WhereTo(URL_WIDGET, URL_HOME, URL_WIDGET) }, // 14
{ NEW_STRANGER, FROM_LOGIN,
new WhereTo(URL_LOGIN, URL_HOME, URL_HOME) } // 15
};
return Arrays.asList(data);
}
// ----------------------------------------------------------------------
// Instance variables and setup
// ----------------------------------------------------------------------
private AuthenticatorStub authenticator;
private ServletContextStub servletContext;
@ -68,12 +238,23 @@ public class AuthenticateTest extends AbstractTestClass {
private HttpServletResponseStub response;
private Authenticate auth;
private final UserInfo userInfo;
private final HowDidWeGetHere urlBundle;
private final WhereTo whereTo;
public AuthenticateTest(UserInfo userInfo, HowDidWeGetHere urlBundle,
WhereTo whereTo) {
this.userInfo = userInfo;
this.urlBundle = urlBundle;
this.whereTo = whereTo;
}
@Before
public void setup() throws Exception {
authenticator = AuthenticatorStub.setup();
authenticator.addUser(createNewDbaUser());
authenticator.addUser(createOldHandUser());
authenticator.addUser(createUserFromUserInfo());
authenticator.setAssociatedUri(OLD_SELF.username,
"old_self_associated_uri");
servletContext = new ServletContextStub();
@ -85,270 +266,246 @@ public class AuthenticateTest extends AbstractTestClass {
request = new HttpServletRequestStub();
request.setSession(session);
request.setRequestUrl(new URL("http://this.that/vivo/siteAdmin"));
request.setRequestUrl(new URL("http://this.that/vivo/authenticate"));
request.setMethod("POST");
request.setHeader("referer", URL_LOGIN_PAGE);
request.setHeader("referer", urlBundle.referrer);
if (urlBundle.afterLoginUrl != null) {
request.addParameter("afterLogin", urlBundle.afterLoginUrl);
}
if (urlBundle.returnParameterSet) {
request.addParameter("return", "");
}
response = new HttpServletResponseStub();
auth = new Authenticate();
auth.init(servletConfig);
}
private User createNewDbaUser() {
private User createUserFromUserInfo() {
User user = new User();
user.setUsername(USER_DBA_NAME);
user.setURI(USER_DBA_URI);
user.setRoleURI("50");
user.setMd5password(Authenticate.applyMd5Encoding(USER_DBA_PASSWORD));
user.setFirstTime(null);
user.setLoginCount(0);
return user;
}
private User createOldHandUser() {
User user = new User();
user.setUsername(USER_OLDHAND_NAME);
user.setURI(USER_OLDHAND_URI);
user.setRoleURI("1");
user.setMd5password(Authenticate
.applyMd5Encoding(USER_OLDHAND_PASSWORD));
user.setLoginCount(USER_OLDHAND_LOGIN_COUNT);
user.setFirstTime(new Date(0));
user.setUsername(userInfo.username);
user.setURI(userInfo.uri);
user.setRoleURI(String.valueOf(userInfo.securityLevel));
user.setMd5password(Authenticate.applyMd5Encoding(userInfo.password));
user.setLoginCount(userInfo.loginCount);
if (userInfo.loginCount > 0) {
user.setFirstTime(new Date(0));
}
return user;
}
// ----------------------------------------------------------------------
// the tests
// The tests
// ----------------------------------------------------------------------
@Test
public void alreadyLoggedIn() {
LoginStatusBean.setBean(session, LOGIN_STATUS_DBA);
auth.doPost(request, response);
assertExpectedRedirect(URL_SITE_ADMIN_PAGE);
assertNoProcessBean();
assertExpectedLoginSessions();
}
@Test
public void justGotHere() {
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(LOGGING_IN, "", "", "");
assertProcessBean(LOGGING_IN, "", "", "");
assertNewLoginSessions();
assertRedirectToContinueUrl();
}
@Test
public void loggingInNoUsername() {
setProcessBean(LOGGING_IN);
setProcessBean(LOGGING_IN, null);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(LOGGING_IN, "", "",
assertProcessBean(LOGGING_IN, "", "",
"Please enter your email address.");
assertNewLoginSessions();
assertRedirectToContinueUrl();
}
@Test
public void loggingInUsernameNotRecognized() {
setProcessBean(LOGGING_IN);
setProcessBean(LOGGING_IN, null);
setLoginNameAndPassword("unknownBozo", null);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(LOGGING_IN, "unknownBozo", "",
assertProcessBean(LOGGING_IN, "unknownBozo", "",
"The email or password you entered is incorrect.");
assertNewLoginSessions();
assertRedirectToContinueUrl();
}
@Test
public void loggingInNoPassword() {
setProcessBean(LOGGING_IN);
setLoginNameAndPassword(USER_DBA_NAME, null);
setProcessBean(LOGGING_IN, null);
setLoginNameAndPassword(userInfo.username, null);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(LOGGING_IN, USER_DBA_NAME, "",
assertProcessBean(LOGGING_IN, userInfo.username, "",
"Please enter your password.");
assertNewLoginSessions();
assertRedirectToContinueUrl();
}
@Test
public void loggingInPasswordIsIncorrect() {
setProcessBean(LOGGING_IN);
setLoginNameAndPassword(USER_DBA_NAME, "bogus_password");
setProcessBean(LOGGING_IN, null);
setLoginNameAndPassword(userInfo.username, "bogus_password");
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(LOGGING_IN, USER_DBA_NAME, "",
assertProcessBean(LOGGING_IN, userInfo.username, "",
"The email or password you entered is incorrect.");
assertNewLoginSessions();
assertRedirectToContinueUrl();
}
@Test
public void loggingInSuccessfulNotFirstTime() {
setProcessBean(LOGGING_IN);
setLoginNameAndPassword(USER_OLDHAND_NAME, USER_OLDHAND_PASSWORD);
public void loggingInSuccessful() {
if (userInfo.loginCount == 0) {
testLoginFirstTime();
} else {
testLoginNotFirstTime();
}
}
private void testLoginFirstTime() {
setProcessBean(LOGGING_IN, null);
setLoginNameAndPassword(userInfo.username, userInfo.password);
auth.doPost(request, response);
assertProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username, "", "");
assertNewLoginSessions();
assertRedirectToContinueUrl();
}
private void testLoginNotFirstTime() {
setProcessBean(LOGGING_IN, null);
setLoginNameAndPassword(userInfo.username, userInfo.password);
auth.doPost(request, response);
assertNoProcessBean();
assertExpectedRedirect(URL_HOME_PAGE);
assertExpectedLoginSessions(USER_OLDHAND_NAME);
}
// ----------------------------------------------------------------------
// first-time password change
// ----------------------------------------------------------------------
@Test
public void loggingInSuccessfulFirstTime() {
setProcessBean(LOGGING_IN);
setLoginNameAndPassword(USER_DBA_NAME, USER_DBA_PASSWORD);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "", "");
assertNewLoginSessions(userInfo.username);
assertRedirectToCompletionUrl();
}
@Test
public void changingPasswordCancel() {
setProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME);
request.addParameter("cancel", "true");
// Only valid for first-time login.
if (userInfo.loginCount == 0) {
setProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username);
request.addParameter("cancel", "true");
auth.doPost(request, response);
auth.doPost(request, response);
assertExpectedRedirect(URL_HOME_PAGE);
assertExpectedLoginSessions();
assertNoProcessBean();
assertNoProcessBean();
assertNewLoginSessions();
assertRedirectToCancelUrl();
}
}
@Test
public void changingPasswordWrongLength() {
setProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME);
setNewPasswordAttempt("HI", "HI");
// Only valid for first-time login.
if (userInfo.loginCount == 0) {
setProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username);
setNewPasswordAttempt("HI", "HI");
auth.doPost(request, response);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "",
"Please enter a password between 6 and 12 characters in length.");
assertRedirectToContinueUrl();
assertNewLoginSessions();
assertProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username, "",
"Please enter a password between 6 and 12 characters in length.");
}
}
@Test
public void changingPasswordDontMatch() {
setProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME);
setNewPasswordAttempt("LongEnough", "DoesNotMatch");
// Only valid for first-time login.
if (userInfo.loginCount == 0) {
setProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username);
setNewPasswordAttempt("LongEnough", "DoesNotMatch");
auth.doPost(request, response);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "",
"The passwords entered do not match.");
assertRedirectToContinueUrl();
assertNewLoginSessions();
assertProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username, "",
"The passwords entered do not match.");
}
}
@Test
public void changingPasswordSameAsBefore() {
setProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME);
setNewPasswordAttempt(USER_DBA_PASSWORD, USER_DBA_PASSWORD);
// Only valid for first-time login.
if (userInfo.loginCount == 0) {
setProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username);
setNewPasswordAttempt(userInfo.password, userInfo.password);
auth.doPost(request, response);
auth.doPost(request, response);
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedLoginSessions();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "",
"Please choose a different password from the "
+ "temporary one provided initially.");
assertRedirectToContinueUrl();
assertNewLoginSessions();
assertProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username, "",
"Please choose a different password from the "
+ "temporary one provided initially.");
}
}
@Test
public void changingPasswordSuccess() {
setProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME);
setNewPasswordAttempt("NewPassword", "NewPassword");
// Only valid for first-time login.
if (userInfo.loginCount == 0) {
setProcessBean(FORCED_PASSWORD_CHANGE, userInfo.username);
setNewPasswordAttempt("NewPassword", "NewPassword");
auth.doPost(request, response);
assertRedirectToCompletionUrl();
assertNewLoginSessions(userInfo.username);
assertNoProcessBean();
assertPasswordChanges(userInfo.username, "NewPassword");
}
}
@Test
public void alreadyLoggedIn() {
LoginStatusBean statusBean = new LoginStatusBean(userInfo.uri,
userInfo.username, userInfo.securityLevel,
AuthenticationSource.INTERNAL);
LoginStatusBean.setBean(session, statusBean);
auth.doPost(request, response);
assertRedirectToCompletionUrl();
assertNoProcessBean();
assertExpectedRedirect(URL_SITE_ADMIN_PAGE);
assertExpectedLoginSessions(USER_DBA_NAME);
assertExpectedPasswordChanges(USER_DBA_NAME, "NewPassword");
assertNewLoginSessions();
}
// ----------------------------------------------------------------------
// Assorted redirects: these assume a successful non-first-time login.
// Helper methods
// ----------------------------------------------------------------------
@Test
public void redirectReturnToRestrictedPage() {
LoginRedirector.setReturnUrlFromForcedLogin(request,
URL_SESSION_REDIRECT);
loginNotFirstTime();
assertExpectedLiteralRedirect(URL_SESSION_REDIRECT);
}
@Test
public void redirectDbaToSiteAdmin() {
authenticator.getUserByUsername(USER_OLDHAND_NAME).setRoleURI("50");
loginNotFirstTime();
assertExpectedRedirect(URL_SITE_ADMIN_PAGE);
}
@Test
public void redirectSelfEditor() {
authenticator.setAssociatedUri(USER_OLDHAND_NAME, "selfEditorURI");
loginNotFirstTime();
assertExpectedRedirect(URL_SELF_EDITOR_PAGE);
}
@Test
public void redirectUnrecognizedUserToHome() {
loginNotFirstTime();
assertExpectedRedirect(URL_HOME_PAGE);
}
@Test
public void redirectUnrecognizedUserToApplicationHome() {
servletContext.setAttribute("postLoginRequest",
URL_CONTEXT_REDIRECT_LOCAL);
loginNotFirstTime();
assertExpectedRedirect(URL_CONTEXT_REDIRECT_LOCAL);
}
@Test
public void redirectUnrecognizedUserToApplicationExternalHome() {
servletContext.setAttribute("postLoginRequest",
URL_CONTEXT_REDIRECT_REMOTE);
loginNotFirstTime();
assertExpectedLiteralRedirect(URL_CONTEXT_REDIRECT_REMOTE);
}
// ----------------------------------------------------------------------
// helper methods
// ----------------------------------------------------------------------
private void setProcessBean(State state) {
LoginProcessBean processBean = new LoginProcessBean();
processBean.setState(state);
LoginProcessBean.setBean(request, processBean);
}
private void setProcessBean(State state, String username) {
LoginProcessBean processBean = new LoginProcessBean();
processBean.setState(state);
processBean.setUsername(username);
if (username != null) {
processBean.setUsername(username);
}
// the urls come directly from the url bundle every time.
if (urlBundle.afterLoginUrl != null) {
processBean.setAfterLoginUrl(urlBundle.afterLoginUrl);
processBean.setLoginPageUrl(URL_LOGIN);
} else if (urlBundle.returnParameterSet) {
processBean.setAfterLoginUrl(urlBundle.referrer);
processBean.setLoginPageUrl(URL_LOGIN);
} else {
processBean.setAfterLoginUrl(null);
processBean.setLoginPageUrl(urlBundle.referrer);
}
LoginProcessBean.setBean(request, processBean);
}
@ -363,21 +520,28 @@ public class AuthenticateTest extends AbstractTestClass {
request.addParameter("confirmPassword", confirmPassword);
}
private void assertExpectedRedirect(String path) {
private void assertRedirectToContinueUrl() {
assertRedirect(whereTo.expectedContinueUrl);
}
private void assertRedirectToCompletionUrl() {
assertRedirect(whereTo.expectedCompletionUrl);
}
private void assertRedirectToCancelUrl() {
assertRedirect(whereTo.expectedCancelUrl);
}
private void assertRedirect(String path) {
if (path.startsWith("http://")) {
assertEquals("absolute redirect", path,
response.getRedirectLocation());
} else {
assertEquals("relative redirect", request.getContextPath() + path,
assertEquals("relative redirect", path,
response.getRedirectLocation());
}
}
/** This is for explicit redirect URLs that already include context. */
private void assertExpectedLiteralRedirect(String path) {
assertEquals("redirect", path, response.getRedirectLocation());
}
private void assertNoProcessBean() {
if (LoginProcessBean.isBean(request)) {
fail("Process bean: expected <null>, but was <"
@ -385,7 +549,7 @@ public class AuthenticateTest extends AbstractTestClass {
}
}
private void assertExpectedProcessBean(State state, String username,
private void assertProcessBean(State state, String username,
String infoMessage, String errorMessage) {
if (!LoginProcessBean.isBean(request)) {
fail("login process bean is null");
@ -396,9 +560,25 @@ public class AuthenticateTest extends AbstractTestClass {
assertEquals("error message", errorMessage,
bean.getErrorMessageAndClear());
assertEquals("username", username, bean.getUsername());
// This should represent the URL bundle, every time.
String expectedAfterLoginUrl = (urlBundle.returnParameterSet) ? urlBundle.referrer
: urlBundle.afterLoginUrl;
assertEquals("after login URL", expectedAfterLoginUrl,
bean.getAfterLoginUrl());
}
private void assertExpectedPasswordChanges(String... strings) {
/** What logins were completed in this test? */
private void assertNewLoginSessions(String... usernames) {
Set<String> expected = new HashSet<String>(Arrays.asList(usernames));
Set<String> actualRecorded = new HashSet<String>(
authenticator.getRecordedLoginUsernames());
assertEquals("recorded logins", expected, actualRecorded);
}
/** What passwords were changed in this test? */
private void assertPasswordChanges(String... strings) {
if ((strings.length % 2) != 0) {
throw new RuntimeException(
"supply even number of args: username and password");
@ -413,26 +593,6 @@ public class AuthenticateTest extends AbstractTestClass {
authenticator.getNewPasswordMap());
}
/** How many folks logged in? */
private void assertExpectedLoginSessions(String... usernames) {
Set<String> expected = new HashSet<String>(Arrays.asList(usernames));
Set<String> actualRecorded = new HashSet<String>(
authenticator.getRecordedLoginUsernames());
assertEquals("recorded logins", expected, actualRecorded);
}
/** Boilerplate login process for the redirect tests. */
private void loginNotFirstTime() {
setProcessBean(LOGGING_IN);
setLoginNameAndPassword(USER_OLDHAND_NAME, USER_OLDHAND_PASSWORD);
auth.doPost(request, response);
assertExpectedLoginSessions(USER_OLDHAND_NAME);
assertNoProcessBean();
}
@SuppressWarnings("unused")
private void showBeans() {
LoginProcessBean processBean = (LoginProcessBean.isBean(request)) ? LoginProcessBean