/* * JOSSO: Java Open Single Sign-On * * Copyright 2004-2009, Atricore, Inc. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.josso.gateway.signon; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.struts.action.*; import org.josso.Lookup; import org.josso.SecurityDomain; import org.josso.auth.Credential; import org.josso.auth.exceptions.AuthenticationFailureException; import org.josso.gateway.SSOContext; import org.josso.gateway.SSOGateway; import org.josso.gateway.SSOWebConfiguration; import org.josso.gateway.identity.SSOUser; import org.josso.gateway.identity.SSORole; import org.josso.gateway.assertion.AuthenticationAssertion; import org.josso.gateway.session.SSOSession; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; /** * Base login action extended by concrete actions associated to specific authentication schemes. * This actions controls the auth. process and invokes subclasses methods (template method). * * @author <a href="mailto:sgonzalez@josso.org">Sebastian Gonzalez Oyuela</a> * @version $Id: LoginAction.java 612 2008-08-22 12:17:20Z gbrigand $ * @revision 07/05/2008 ajadzinsky */ public abstract class LoginAction extends SignonBaseAction { public static final String JOSSO_CMD_LOGIN = "login"; private static final Log logger = LogFactory.getLog(LoginAction.class); /** * Executes the proper login method based on sso_command request parameter. */ public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isDebugEnabled()) logger.debug("JOSSO Command : [cmd="+getSSOCmd(request)+"]"); /** * SSO Context needs to resolve security domain */ prepareContext(request); // Get current SSO Command ... String cmd = getSSOCmd(request); // Validate BACK TO URL to avoid XSR exploit String backTo = getBackTo(request); if (backTo != null) { backTo = backTo.toLowerCase(); SecurityDomain domain = SSOContext.getCurrent().getSecurityDomain(); SSOWebConfiguration cfg = domain.getSSOWebConfiguration(); boolean trusted = false; if (cfg.getTrustedHosts().size() > 0) { String backToHost = null; if (backTo.startsWith("http://") || backTo.startsWith("https://")) { try { URL backToUrl = new URL(backTo); backToHost = backToUrl.getHost(); backToHost = backToHost.substring(backToHost.lastIndexOf("@")+1); } catch (MalformedURLException e) { if (logger.isDebugEnabled()) logger.debug("BackTo URL is malformed : [backTo=" + backTo + "]"); } } for (String trustedHost : cfg.getTrustedHosts()) { if (StringUtils.isNotBlank(trustedHost) && trustedHost.equals(backToHost)) { trusted = true; break; } } } if (!trusted && cfg.getTrustedHosts().size() > 0) { logger.warn("Attempt to use untrusted host in back_to URL " + backTo); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Pragma", "no-cache"); response.setHeader("Expires", "0"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); // Add non-cache headers return null; } } if (canRelay(request)) return relay(mapping, form, request, response); // If no command was specified, "ask-for-login" is the default value. if (cmd == null) { return askForLogin(mapping, form, request, response); } // All other commands mean "perform-login" return login(mapping, form, request, response); } /** * Ask the user for login information. */ protected ActionForward askForLogin(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { try { // Ask user for login information. SSOWebConfiguration cfg = Lookup.getInstance().lookupSSOWebConfiguration(); String loginUrl = cfg.getCustomLoginURL(); String backTo = getBackTo(request); if (loginUrl != null) { if (backTo != null) { loginUrl += (loginUrl.indexOf("?") >= 0 ? "&" : "?") + "josso_back_to=" + backTo; } // The authentication interface is not the default ... if (logger.isDebugEnabled()) logger.debug("Redirecting to custom login : " + loginUrl); response.sendRedirect(response.encodeRedirectURL(loginUrl)); return null; // No action forward needed, we } return mapping.findForward("login-page"); } catch (Exception e) { if (this.onFatalError(e, request, response)) return null; return mapping.findForward("error"); } } /** * Logins the user in the SSO infrastructure */ protected ActionForward login(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { try { SSOGateway gwy = getSSOGateway(); Credential[] c = getCredentials(request); try { // 1 - Handle Outbound relaying by generating an assertion for the authentication request SSOContext ctx = SSOContext.getCurrent(); AuthenticationAssertion authAssertion = gwy.assertIdentity(c, ctx.getScheme()); String sessionId = authAssertion.getSSOSessionId(); SSOSession session = gwy.findSession(sessionId); // Cookie ssoCookie = newJossoCookie(request.getContextPath(), session.getProcessId()); // response.addCookie(ssoCookie); storeSSOInformation(request, response, session); if (logger.isDebugEnabled()) logger.debug("[login()], authentication successfull."); // 2 - Restore BACK TO URL ... String back_to = this.getBackTo(request, session, authAssertion); if (back_to == null) { // Return to controller, if we do not have a back-to url, add more information to the context ;) SSOUser user = gwy.findUserInSession(sessionId); SSORole[] roles = gwy.findRolesByUsername(user.getName()); // so that pages can access this bean request.setAttribute(KEY_JOSSO_SESSION, session); request.setAttribute(KEY_JOSSO_USER, user); request.setAttribute(KEY_JOSSO_USER_ROLES, roles); return mapping.findForward("login-result"); } // If authentication succeds, remove al SSO session data. this.clearSSOParameters(request); // We're going back to the partner app. if (logger.isDebugEnabled()) logger.debug("[login()], Redirecting user to : " + back_to); response.sendRedirect(response.encodeRedirectURL(back_to)); return null; // No forward is needed, we perfomed a 'sendRedirect'. } catch (AuthenticationFailureException e) { if (logger.isDebugEnabled()) logger.debug("[AuthenticationFailureException] " + e.getMessage(), e); // logs the error ActionErrors errors = new ActionErrors(); errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("sso.login.failed")); saveErrors(request, errors); // Invalid login attempt, redirect to ON ERROR URL, if any. boolean ok = this.onLoginAuthenticationException(e, request, response, c); if (ok) { return null; // No forward is needed, we perfomed a 'sendRedirect'. } SSOWebConfiguration cfg = SSOContext.getCurrent().getSecurityDomain().getSSOWebConfiguration(); if (cfg.isBasicAuthenticationEnabled()) { return mapping.findForward("login-page"); } else { response.setHeader("Cache-Control", "no-cache"); response.setHeader("Pragma", "no-cache"); response.setHeader("Expires", "0"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); return null; } } } catch (Exception e) { if (this.onFatalError(e, request, response)) return null; return mapping.findForward("error"); } } /** * @param e is the <AuthenticationFailureException> Exception to br handled * @param request is the <HttpServletRequest> context * @param response is the <HttpServletResponse> context * @param credentials contains the <Crdential> used to perform de authentication * @return false will execute the default mapping.findForward otherwise no action will be taken */ protected boolean onLoginAuthenticationException(AuthenticationFailureException e, HttpServletRequest request, HttpServletResponse response, Credential[] credentials) throws IOException { String cmd = getSSOCmd(request); if (cmd != null && cmd.equals("login_optional")) { // Go back to agent ... String back_to = getBackTo(request); // We're going back to the partner app. if (logger.isDebugEnabled()) logger.debug("[login()], Login Optional failed, redirecting user to : " + back_to); response.sendRedirect(response.encodeRedirectURL(back_to)); return true; // We handled the redirect } String on_error = (String) request.getSession(true).getAttribute(KEY_JOSSO_ON_ERROR); if (on_error == null) { // Check for a configured custom login url try { SSOWebConfiguration cfg = Lookup.getInstance().lookupSSOWebConfiguration(); if (cfg.isBasicAuthenticationEnabled()) { on_error = cfg.getCustomLoginURL(); } } catch (Exception ex) { logger.error(e.getMessage(), e); } } if (on_error != null) { // TODO : Improve error information handling, this could be managed with an outbound mechanism, like assertions. // Add error type and received username to ERROR URL. SSOGateway g = getSSOGateway(); on_error += (on_error.indexOf("?") >= 0 ? "&" : "?") + "josso_error_type=" + e.getErrorType(); try { SSOContext ctx = SSOContext.getCurrent(); on_error += "&josso_username=" + g.getPrincipalName(ctx.getScheme(), credentials); } catch (Exception ex) { if (logger.isDebugEnabled()) logger.error(" [onLoginAuthenticationException()] cant find PrincipalName"); } response.sendRedirect(response.encodeRedirectURL(on_error)); if (logger.isDebugEnabled()) logger.debug("[login()], authentication failure. Redirecting user to : " + on_error); return true; } return false; } /** * Relay using a previously opened and valid SSO session. */ protected ActionForward relay(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { try { SSOGateway g = getSSOGateway(); // 1 - Recover session and create a new assertion. SSOSession session = SSOContext.getCurrent().getSession(); AuthenticationAssertion authAssertion = g.assertIdentity(session.getId()); if (logger.isDebugEnabled()) logger.debug("[relay()], authentication successfull."); // 2 - Restore BACK TO URL ... String back_to = this.getBackTo(request, session, authAssertion); if (back_to == null) { // Return to controller. return mapping.findForward("login-result"); } this.clearSSOParameters(request); // We're going back to the partner app. if (logger.isDebugEnabled()) logger.debug("[relay()], Redirecting user to : " + back_to); response.sendRedirect(response.encodeRedirectURL(back_to)); return null; // No forward is needed, we perfomed a 'sendRedirect'. } catch (Exception e) { if (this.onFatalError(e, request, response)) return null; return mapping.findForward("error"); } } protected boolean onFatalError(Exception e, HttpServletRequest request, HttpServletResponse response) { // Fatal error ... logger.error(e.getMessage(), e); ActionErrors errors = new ActionErrors(); errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("sso.error", e.getMessage() != null ? e.getMessage() : e.toString())); saveErrors(request, errors); return false; } /** * Check if the request can be relayed to the requesting party without having to reauthenticate. * * @param request * @return true if a relay can be achieved or false in case a new authentication assertion must be issued. */ protected boolean canRelay(HttpServletRequest request) { SSOSession s = SSOContext.getCurrent().getSession(); return s!= null && s.isValid(); /* boolean canRelay = false; try { String jossoSessionId = getJossoSessionId(request); if ( jossoSessionId != null ) { SSOSessionManager ssoSessionManager = Lookup.getInstance().lookupSecurityDomain().getSessionManager(); SSOSession s = ssoSessionManager.getSession( jossoSessionId); if (s != null) canRelay = true; } } catch (NoSuchSessionException e) { // Ingore this error .... we probably got an old SESSION id ... if (logger.isDebugEnabled()) logger.debug(e.getMessage()); } catch (Exception e) { logger.error(e.getMessage(), e); } return canRelay; */ } }