/********************************************************************************** * $URL: $ * $Id: $ *********************************************************************************** * * Copyright (c) 2008 The Sakai Foundation. * * Licensed under the Educational Community License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ecl1.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.login.tool; import java.io.IOException; import java.io.PrintWriter; import java.net.URL; import javax.security.auth.login.LoginException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.event.cover.UsageSessionService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.login.api.Login; import org.sakaiproject.login.api.LoginCredentials; import org.sakaiproject.login.api.LoginRenderContext; import org.sakaiproject.login.api.LoginRenderEngine; import org.sakaiproject.login.api.LoginService; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.Tool; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.Web; import edu.umd.cs.findbugs.annotations.SuppressWarnings; public class SkinnableLogin extends HttpServlet implements Login { private static final long serialVersionUID = 1L; /** Our log (commons). */ private static Log log = LogFactory.getLog(SkinnableLogin.class); /** Session attribute used to store a message between steps. */ protected static final String ATTR_MSG = "notify"; /** Session attribute set and shared with ContainerLoginTool: URL for redirecting back here. */ public static final String ATTR_RETURN_URL = "sakai.login.return.url"; /** Session attribute set and shared with ContainerLoginTool: if set we have failed container and need to check internal. */ public static final String ATTR_CONTAINER_CHECKED = "sakai.login.container.checked"; /** Marker to indicate we are logging in the PDA Portal and should put out abbreviated HTML */ public static final String PDA_PORTAL_SUFFIX = "/pda/"; /** The Neo-portal in 2.9+ introduced a portal.neoprefix config variable */ private static final String PORTAL_SKIN_NEOPREFIX_PROPERTY = "portal.neoprefix"; private static final String PORTAL_SKIN_NEOPREFIX_DEFAULT = "neo-"; private static String portalSkinPrefix; // Services are transient because the class is serializable but the services aren't private transient ServerConfigurationService serverConfigurationService; private transient SiteService siteService; private transient LoginService loginService; private static ResourceLoader rb = new ResourceLoader("auth"); private String loginContext; public void init(ServletConfig config) throws ServletException { super.init(config); loginContext = config.getInitParameter("login.context"); if (loginContext == null || loginContext.length() == 0) { loginContext = DEFAULT_LOGIN_CONTEXT; } loginService = (LoginService)ComponentManager.get(LoginService.class); serverConfigurationService = (ServerConfigurationService) ComponentManager .get(ServerConfigurationService.class); siteService = (SiteService) ComponentManager.get(SiteService.class); portalSkinPrefix = serverConfigurationService.getString(PORTAL_SKIN_NEOPREFIX_PROPERTY, PORTAL_SKIN_NEOPREFIX_DEFAULT); log.info("init()"); } public void destroy() { log.info("destroy()"); super.destroy(); } /** * Access the Servlet's information display. * * @return servlet information. */ public String getServletInfo() { return "Sakai Login"; } @SuppressWarnings(value = "HRS_REQUEST_PARAMETER_TO_HTTP_HEADER", justification = "Looks like the data is already URL encoded") protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // get the session Session session = SessionManager.getCurrentSession(); // get my tool registration Tool tool = (Tool) req.getAttribute(Tool.TOOL); // recognize what to do from the path String option = req.getPathInfo(); // maybe we don't want to do the container this time boolean skipContainer = false; // flag for whether we should show the auth choice page boolean showAuthChoice = false; // if missing, set it to "/login" if ((option == null) || ("/".equals(option))) { option = "/login"; } // look for the extreme login (i.e. to skip container checks) else if ("/xlogin".equals(option)) { option = "/login"; skipContainer = true; } // get the parts (the first will be "", second will be "login" or "logout") String[] parts = option.split("/"); if (parts[1].equals("logout")) { // get the session info complete needs, since the logout will invalidate and clear the session String returnUrl = (String) session.getAttribute(Tool.HELPER_DONE_URL); // logout the user UsageSessionService.logout(); complete(returnUrl, null, tool, res); return; } // see if we need to check container boolean checkContainer = serverConfigurationService.getBoolean("container.login", false); if (checkContainer && !skipContainer) { // if we have not checked the container yet, check it now if (session.getAttribute(ATTR_CONTAINER_CHECKED) == null) { // save our return path session.setAttribute(ATTR_RETURN_URL, Web.returnUrl(req, null)); String containerCheckPath = this.getServletConfig().getInitParameter("container"); String containerCheckUrl = Web.serverUrl(req) + containerCheckPath; // support query parms in url for container auth String queryString = req.getQueryString(); if (queryString != null) containerCheckUrl = containerCheckUrl + "?" + queryString; /* * FindBugs: HRS_REQUEST_PARAMETER_TO_HTTP_HEADER Looks like the * data is already URL encoded. Had to @SuppressWarnings * the entire method. */ //SAK-21498 choice page for selecting auth sources showAuthChoice = serverConfigurationService.getBoolean("login.auth.choice", false); URL helperUrl = new URL((String) session.getAttribute(Tool.HELPER_DONE_URL)); String helperPath = helperUrl == null ? null : helperUrl.getPath(); if (showAuthChoice && !(StringUtils.isEmpty(helperPath) || helperPath.equals("/portal") || helperPath.equals("/portal/") || helperPath.equals("/portal/pda") || helperPath.equals("/portal/pda/"))) { String xloginUrl = serverConfigurationService.getPortalUrl() + "/xlogin"; // Present the choice template LoginRenderContext rcontext = startChoiceContext("", req, res); rcontext.put("containerLoginUrl", containerCheckUrl); rcontext.put("xloginUrl", xloginUrl); sendResponse(rcontext, res, "choice", null); } else { //go straight to container check res.sendRedirect(res.encodeRedirectURL(containerCheckUrl)); } return; } } // PDA or not? String portalUrl = (String) session.getAttribute(Tool.HELPER_DONE_URL); boolean isPDA = false; if ( portalUrl != null ) { isPDA = (portalUrl.indexOf (PDA_PORTAL_SUFFIX) > -1); } log.debug("isPDA: " + isPDA); // Present the xlogin template LoginRenderContext rcontext = startPageContext("", req, res); rcontext.put("isPDA", isPDA); // Decide whether or not to put up the Cancel String actualPortal = serverConfigurationService.getPortalUrl(); if ( portalUrl != null && portalUrl.indexOf("/site/") < 1 && portalUrl.startsWith(actualPortal) ) { rcontext.put("doCancel", Boolean.TRUE); } sendResponse(rcontext, res, "xlogin", null); } /** * Respond to data posting requests. * * @param req * The servlet request. * @param res * The servlet response. */ protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Present the xlogin template LoginRenderContext rcontext = startPageContext(null, req, res); // Get the Sakai session Session session = SessionManager.getCurrentSession(); // Get my tool registration Tool tool = (Tool) req.getAttribute(Tool.TOOL); // Determine if the user canceled this request String cancel = req.getParameter("cancel"); // cancel if (cancel != null) { rcontext.put(ATTR_MSG, rb.getString("log.canceled")); // get the session info complete needs, since the logout will invalidate and clear the session String returnUrl = (String) session.getAttribute(Tool.HELPER_DONE_URL); // Trim off the force.login parameter from return URL if present if ( returnUrl != null ) { int where = returnUrl.indexOf("?force.login"); if ( where > 0 ) returnUrl = returnUrl.substring(0,where); } // TODO: send to the cancel URL, cleanup session complete(returnUrl, session, tool, res); } // submit else { LoginCredentials credentials = new LoginCredentials(req); credentials.setSessionId(session.getId()); try { loginService.authenticate(credentials); String returnUrl = (String) session.getAttribute(Tool.HELPER_DONE_URL); complete(returnUrl, session, tool, res); } catch (LoginException le) { String message = le.getMessage(); log.debug("LoginException: " + message); boolean showAdvice = false; if (message.equals(EXCEPTION_INVALID_CREDENTIALS)) { rcontext.put(ATTR_MSG, rb.getString("log.invalid.credentials")); showAdvice = true; logFailedAttempt(credentials); } else if (message.equals(EXCEPTION_DISABLED)) { rcontext.put(ATTR_MSG, rb.getString("log.disabled.user")); logFailedAttempt(credentials); String disabledUrl = serverConfigurationService.getString("disabledSiteUrl"); if(disabledUrl != null && !"".equals(disabledUrl)){ res.sendRedirect(disabledUrl); } } else if (message.equals(EXCEPTION_INVALID_WITH_PENALTY)) { rcontext.put(ATTR_MSG, rb.getString("log.invalid.with.penalty")); showAdvice = true; logFailedAttempt(credentials); } else if (message.equals(EXCEPTION_MISSING_CREDENTIALS)) { rcontext.put(ATTR_MSG, rb.getString("log.tryagain")); //Do we need to log this one? You can't really brute force with empty credentials... } else { rcontext.put(ATTR_MSG, rb.getString("log.invalid")); logFailedAttempt(credentials); } if (showAdvice) { String loginAdvice = loginService.getLoginAdvice(credentials); if (loginAdvice != null && !loginAdvice.equals("")) { log.debug("Returning login advice"); rcontext.put("loginAdvice", loginAdvice); } } // Decide whether or not to put up the Cancel String portalUrl = (String) session.getAttribute(Tool.HELPER_DONE_URL); String actualPortal = serverConfigurationService.getPortalUrl(); if ( portalUrl != null && portalUrl.indexOf("/site/") < 1 && portalUrl.startsWith(actualPortal) ) { rcontext.put("doCancel", Boolean.TRUE); } sendResponse(rcontext, res, "xlogin", null); } } } public void sendResponse(LoginRenderContext rcontext, HttpServletResponse res, String template, String contentType) throws IOException { // headers if (contentType == null) { res.setContentType("text/html; charset=UTF-8"); } else { res.setContentType(contentType); } res.addDateHeader("Expires", System.currentTimeMillis() - (1000L * 60L * 60L * 24L * 365L)); res.addDateHeader("Last-Modified", System.currentTimeMillis()); res.addHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0"); res.addHeader("Pragma", "no-cache"); // get the writer PrintWriter out = res.getWriter(); try { LoginRenderEngine rengine = rcontext.getRenderEngine(); rengine.render(template, rcontext, out); } catch (Exception e) { throw new RuntimeException("Failed to render template ", e); } } public LoginRenderContext startPageContext(String skin, HttpServletRequest request, HttpServletResponse response) { LoginRenderEngine rengine = loginService.getRenderEngine(loginContext, request); LoginRenderContext rcontext = rengine.newRenderContext(request); if (StringUtils.isEmpty(skin)) { skin = serverConfigurationService.getString("skin.default", "default"); } String templates = serverConfigurationService.getString("portal.templates", "neoskin"); if ("neoskin".equals(templates) && portalSkinPrefix != null && !StringUtils.startsWith(skin, portalSkinPrefix)) { // Don't add the prefix twice skin = portalSkinPrefix + skin; } String skinRepo = serverConfigurationService.getString("skin.repo"); String uiService = serverConfigurationService.getString("ui.service", "Sakai"); String passwordResetUrl = getPasswordResetUrl(); String eidWording = rb.getString("userid"); String pwWording = rb.getString("log.pass"); String loginRequired = rb.getString("log.logreq"); String loginWording = rb.getString("log.login"); String cancelWording = rb.getString("log.cancel"); String passwordResetWording = rb.getString("log.password.reset"); rcontext.put("action", response.encodeURL(Web.returnUrl(request, null))); rcontext.put("pageSkinRepo", skinRepo); rcontext.put("pageSkin", skin); rcontext.put("uiService", uiService); rcontext.put("pageScriptPath", getScriptPath()); rcontext.put("loginEidWording", eidWording); rcontext.put("loginPwWording", pwWording); rcontext.put("loginRequired", loginRequired); rcontext.put("loginWording", loginWording); rcontext.put("cancelWording", cancelWording); rcontext.put("passwordResetUrl", passwordResetUrl); rcontext.put("passwordResetWording", passwordResetWording); String eid = StringEscapeUtils.escapeHtml(request.getParameter("eid")); String pw = StringEscapeUtils.escapeHtml(request.getParameter("pw")); if (eid == null) eid = ""; if (pw == null) pw = ""; rcontext.put("eid", eid); rcontext.put("password", pw); return rcontext; } public LoginRenderContext startChoiceContext(String skin, HttpServletRequest request, HttpServletResponse response) { LoginRenderEngine rengine = loginService.getRenderEngine(loginContext, request); LoginRenderContext rcontext = rengine.newRenderContext(request); if (skin == null || skin.trim().length() == 0) { skin = serverConfigurationService.getString("skin.default"); } String skinRepo = serverConfigurationService.getString("skin.repo"); String uiService = serverConfigurationService.getString("ui.service","Sakai"); rcontext.put("pageSkinRepo", skinRepo); rcontext.put("pageSkin", skin); rcontext.put("uiService", uiService); rcontext.put("pageScriptPath", getScriptPath()); rcontext.put("choiceRequired", rb.getString("log.choicereq")); rcontext.put("containerLoginChoiceIcon", serverConfigurationService.getString("container.login.choice.icon")); rcontext.put("xloginChoiceIcon", serverConfigurationService.getString("xlogin.choice.icon")); //the URLs for these are set above, as containerLoginUrl and xloginUrl rcontext.put("containerLoginChoiceText", serverConfigurationService.getString("container.login.choice.text")); rcontext.put("xloginChoiceText", serverConfigurationService.getString("xlogin.choice.text")); return rcontext; } public String getLoginContext() { return loginContext; } // Helper methods /** * Cleanup and redirect when we have a successful login / logout * * @param session * @param tool * @param res * @throws IOException */ protected void complete(String returnUrl, Session session, Tool tool, HttpServletResponse res) throws IOException { // cleanup session if (session != null) { session.removeAttribute(Tool.HELPER_MESSAGE); session.removeAttribute(Tool.HELPER_DONE_URL); session.removeAttribute(ATTR_MSG); session.removeAttribute(ATTR_RETURN_URL); session.removeAttribute(ATTR_CONTAINER_CHECKED); } // if we end up with nowhere to go, go to the portal if (returnUrl == null) { returnUrl = serverConfigurationService.getPortalUrl(); log.info("complete: nowhere set to go, going to portal"); } // redirect to the done URL res.sendRedirect(res.encodeRedirectURL(returnUrl)); } protected String getScriptPath() { return "/library/js/"; } /** * Gets the password reset URL. If looks for a configured URL, otherwise it looks * for the password reset tool in the gateway site and builds a link to that. * @return The password reset URL or <code>null</code> if there isn't one or we * can't find the password reset tool. */ protected String getPasswordResetUrl() { // Has a password reset url been specified in sakai.properties? If so, it rules. String passwordResetUrl = serverConfigurationService.getString("login.password.reset.url", null); if(passwordResetUrl == null) { // No explicit password reset url. Try and locate the tool on the gateway page. // If it has been installed we'll use it. String gatewaySiteId = serverConfigurationService.getGatewaySiteId(); try { Site gatewaySite = siteService.getSite(gatewaySiteId); ToolConfiguration resetTC = gatewaySite.getToolForCommonId("sakai.resetpass"); if(resetTC != null) { passwordResetUrl = resetTC.getContainingPage().getUrl(); } } catch(IdUnusedException iue) { log.warn("No " + gatewaySiteId + " site found whilst building password reset url, set password.reset.url" + " or create " + gatewaySiteId + " and add password reset tool."); } } return passwordResetUrl; } /** * Helper to log failed login attempts (SAK-22430) * @param credentials the credentials supplied * * Note that this could easily be extedned to track login attempts per session and report on it here */ private void logFailedAttempt(LoginCredentials credentials) { if(serverConfigurationService.getBoolean("login.log-failed", true)) { // SAK-23672 Safe login string before log log.warn("Login attempt failed. ID=" + StringUtils.abbreviate(credentials.getIdentifier().replaceAll("(\\r|\\n)", ""),255) + ", IP Address=" + credentials.getRemoteAddr()); } } }