From bb614c833bf53bff1fd11eb11b19e541a717c3b8 Mon Sep 17 00:00:00 2001 From: jeb228 Date: Wed, 10 Nov 2010 21:26:29 +0000 Subject: [PATCH] NIHVIVO-1207 Add the LoginShibboleth servlet. --- webapp/config/web.xml | 9 + .../authenticate/LoginShibboleth.java | 278 ++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/LoginShibboleth.java diff --git a/webapp/config/web.xml b/webapp/config/web.xml index b70191043..2e0873cc1 100644 --- a/webapp/config/web.xml +++ b/webapp/config/web.xml @@ -1068,6 +1068,11 @@ edu.cornell.mannlib.vitro.webapp.controller.edit.Login + + loginShibboleth + edu.cornell.mannlib.vitro.webapp.controller.authenticate.LoginShibboleth + + logout edu.cornell.mannlib.vitro.webapp.controller.edit.Logout @@ -1229,6 +1234,10 @@ login /login + + loginShibboleth + /loginShibboleth + login diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/LoginShibboleth.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/LoginShibboleth.java new file mode 100644 index 000000000..0f55c1aed --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/LoginShibboleth.java @@ -0,0 +1,278 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.authenticate; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Enumeration; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean; +import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.Message; + +/** + * This servlet acts as the interface to the Shibboleth authentication server. + * + * If the request has the "setup" property, it is coming from the Vivo login + * screen and going to the Shibboleth server. + * + * Otherwise, the request is coming from the Shibboleth server and going back to + * Vivo. + */ +public class LoginShibboleth extends HttpServlet { + private static final Log log = LogFactory.getLog(LoginShibboleth.class); + + private static final String CLASSNAME = LoginShibboleth.class.getName(); + + /** This session attribute tells where we came from. */ + private static final String ATTRIBUTE_REFERRER = CLASSNAME + ".referrer"; + + /** This request parameter indicates that we are setting up the login. */ + private static final String PARAMETER_SETUP = "setup"; + + /** This http header holds the referring page. */ + private static final String HEADING_REFERRER = "referer"; + + /** On return froma Shibboleth login, this header holds the provider name. */ + private static final String HEADING_SHIBBOLETH_PROVIDER = "shib-identity-provider"; + + /** On return froma Shibboleth login, this header holds the user name. */ + private static final String HEADING_SHIBBOLETH_USERNAME = "glid"; + + /** The configuration property that points to the Shibboleth server. */ + private static final String PROPERTY_SHIBBOLETH_SERVER_URL = "shibboleth.server.url"; + + /** The configuration property that tells what provider name we expect. */ + private static final String PROPERTY_SHIBBOLETH_PROVIDER = "shibboleth.provider"; + + private static final Message MESSAGE_NO_SHIBBOLETH_SERVER = new LoginProcessBean.Message( + "deploy.properties doesn't contain a value for '" + + PROPERTY_SHIBBOLETH_SERVER_URL + "'", + LoginProcessBean.MLevel.ERROR); + + private static final Message MESSAGE_NO_SHIBBOLETH_PROVIDER = new LoginProcessBean.Message( + "deploy.properties doesn't contain a value for '" + + PROPERTY_SHIBBOLETH_PROVIDER + "'", + LoginProcessBean.MLevel.ERROR); + + private static final Message MESSAGE_LOGIN_FAILED = new LoginProcessBean.Message( + "Shibboleth login failed.", LoginProcessBean.MLevel.ERROR); + + private static final Message MESSAGE_NO_SUCH_USER = new LoginProcessBean.Message( + "Shibboleth login succeeded, but user {0} is unknown to VIVO.", + LoginProcessBean.MLevel.ERROR); + + private static final String ERROR_NO_PARAMETERS = "Likely error in the template: " + + "'setup' parameter was not found, " + + "but there was no info from the Shibboleth server either."; + + private final LoginRedirector loginRedirector = new LoginRedirector(); + + private String shibbolethServerUrl; + private String shibbolethProvider; + private static boolean isFirstCallToServlet = true; + + /** Get the configuration properties. */ + @Override + public void init() throws ServletException { + shibbolethServerUrl = ConfigurationProperties + .getProperty(PROPERTY_SHIBBOLETH_SERVER_URL); + shibbolethProvider = ConfigurationProperties + .getProperty(PROPERTY_SHIBBOLETH_PROVIDER); + } + + /** + *
+	 * The first request to this servlet must include the setup parameter. 
+	 *      If it doesn't, it is totally bogus and will cause a complaint.
+	 *      This means that the login form isn't coded correctly, 
+	 *      	We try to notify the sysadmin in a meaningful way.
+	 *      
+	 * On setup, write down the referring page and redirect to the shib server URL
+	 * 		a URL that comes from the deploy.properties via the widget code.
+	 * 			if no such property, set error message and return to referring page.
+	 * 		it returns to this page
+	 * 
+	 * Not on setup
+	 * 		check for the site name and the username
+	 *      if either is missing,
+	 * 			return to the referring page with an error message: login failed.
+	 * 		if both there and the provider is wrong
+	 * 			return to the referring page with an error message: login failed.
+	 *      if both there and the user doesn't exist
+	 * 			return to the referring page with an error message: no such user.
+	 * 		otherwise (successful)
+	 * 			record the login and redirect like we would on a normal login.
+	 * 
+ */ + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + setupLoginProcessBean(req); + + boolean setupParmIsSet = checkSetupParameterIsSet(req); + + if (requestIsTotallyBogus(setupParmIsSet)) { + resp.sendError(500, ERROR_NO_PARAMETERS); + } else if (setupParmIsSet) { + settingUpShibbolethLogin(req, resp); + } else { + returningFromShibbolethLogin(req, resp); + } + } + + /** Record that the login is in progress. */ + private void setupLoginProcessBean(HttpServletRequest req) { + LoginProcessBean bean = LoginProcessBean.getBean(req); + bean.setState(LoginProcessBean.State.LOGGING_IN); + } + + /** Does the request contain a "setup" parameter? */ + private boolean checkSetupParameterIsSet(HttpServletRequest req) { + String setupParm = req.getParameter(PARAMETER_SETUP); + log.debug("setup=" + setupParm); + if ((setupParm == null) || setupParm.isEmpty()) { + return false; + } else { + return true; + } + } + + /** If the first call doesn't include "setup", the template is broken. */ + private boolean requestIsTotallyBogus(boolean setupParmIsSet) { + boolean bogosity = isFirstCallToServlet && !setupParmIsSet; + isFirstCallToServlet = false; + return bogosity; + } + + /** On setup, hand over to the Shibboleth server. */ + private void settingUpShibbolethLogin(HttpServletRequest req, + HttpServletResponse resp) throws IOException { + storeTheReferringPage(req); + if (shibbolethServerUrl == null) { + log.debug("No shibboleth server in deploy.properties"); + complainAndReturnToReferrer(req, resp, MESSAGE_NO_SHIBBOLETH_SERVER); + } else if (shibbolethProvider == null) { + log.debug("No shibboleth provider in deploy.properties"); + complainAndReturnToReferrer(req, resp, + MESSAGE_NO_SHIBBOLETH_PROVIDER); + } else { + log.debug("Sending to shibboleth server."); + resp.sendRedirect(buildShibbolethRedirectUrl(req)); + } + } + + /** Remember where we came from - we'll need to go back there. */ + private void storeTheReferringPage(HttpServletRequest req) { + String referrer = req.getHeader(HEADING_REFERRER); + if (referrer == null) { + dumpRequestHeaders(req); + referrer = figureHomePageUrl(req); + } + log.debug("Referring page is '" + referrer + "'"); + req.getSession().setAttribute(ATTRIBUTE_REFERRER, referrer); + } + + /** How do we get to the Shibboleth server and back? */ + private String buildShibbolethRedirectUrl(HttpServletRequest req) { + try { + String returnUrl = req.getRequestURL().toString(); + String encodedReturnUrl = URLEncoder.encode(returnUrl, "UTF-8"); + String shibbolethUrl = shibbolethServerUrl + "?target=" + + encodedReturnUrl; + log.debug("shibbolethURL is '" + shibbolethUrl + "'"); + return shibbolethUrl; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); // No UTF-8? Really? + } + } + + /** On return from the Shibboleth server, try to apply the results. */ + private void returningFromShibbolethLogin(HttpServletRequest req, + HttpServletResponse resp) throws IOException { + String provider = req.getHeader(HEADING_SHIBBOLETH_PROVIDER); + String user = req.getHeader(HEADING_SHIBBOLETH_USERNAME); + log.debug("Info from Shibboleth: user=" + user + ", provider=" + + provider); + + if ((provider == null) || (user == null)) { + complainAndReturnToReferrer(req, resp, MESSAGE_LOGIN_FAILED); + } else if (!this.shibbolethProvider.equals(provider)) { + log.error("Wrong shibboleth provider: " + provider); + complainAndReturnToReferrer(req, resp, MESSAGE_LOGIN_FAILED); + } else if (!getAuthenticator(req).isExistingUser(user)) { + log.debug("No such user: " + user); + complainAndReturnToReferrer(req, resp, MESSAGE_NO_SUCH_USER, user); + } else { + log.debug("Logging in as " + user); + recordLoginAndRedirect(req, resp, user); + } + } + + /** Success. Record the login and send them to the appropriate page. */ + private void recordLoginAndRedirect(HttpServletRequest req, + HttpServletResponse resp, String username) + throws UnsupportedEncodingException, IOException { + getAuthenticator(req).recordUserIsLoggedIn(username); + req.getSession().removeAttribute(ATTRIBUTE_REFERRER); + loginRedirector.redirectLoggedInUser(req, resp); + } + + /** Store an error message in the login bean and go back where we came from. */ + private void complainAndReturnToReferrer(HttpServletRequest req, + HttpServletResponse resp, Message message, Object... args) + throws IOException { + log.debug(message.getMessageLevel() +": "+ message.formatMessage(args)); + LoginProcessBean.getBean(req).setMessage(message, args); + + String referrer = (String) req.getSession().getAttribute( + ATTRIBUTE_REFERRER); + log.debug("returning to referrer: " + referrer); + if (referrer == null) { + referrer = figureHomePageUrl(req); + log.debug("returning to home page: " + referrer); + } + + req.getSession().removeAttribute(ATTRIBUTE_REFERRER); + resp.sendRedirect(referrer); + } + + private String figureHomePageUrl(HttpServletRequest req) { + StringBuffer url = req.getRequestURL(); + String uri = req.getRequestURI(); + int authLength = url.length() - uri.length(); + String auth = url.substring(0, authLength); + return auth + req.getContextPath(); + } + + private Authenticator getAuthenticator(HttpServletRequest req) { + return Authenticator.getInstance(req); + } + + private void dumpRequestHeaders(HttpServletRequest req) { + if (log.isDebugEnabled()) { + @SuppressWarnings("unchecked") + Enumeration names = req.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + log.debug("header: " + name + "=" + req.getHeader(name)); + } + } + } + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + doPost(request, response); + } + +}