/*
* (C) Copyright 2006-2009 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.
*
* Contributors:
* Nuxeo - initial API and implementation
* Academie de Rennes - proxy CAS support
*
* $Id: JOOoConvertPluginImpl.java 18651 2007-05-13 20:28:53Z sfermigier $
*/
package org.nuxeo.ecm.platform.ui.web.auth.cas2;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.URIUtils;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.LoginResponseHandler;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
import org.nuxeo.runtime.api.Framework;
import org.xml.sax.SAXException;
import edu.yale.its.tp.cas.client.ProxyTicketValidator;
import edu.yale.its.tp.cas.client.ServiceTicketValidator;
/**
* @author Thierry Delprat
* @author Olivier Adam
* @author M.-A. Darche
* @author Benjamin Jalon
* @author Thierry Martins
*/
public class Cas2Authenticator implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension,
LoginResponseHandler {
protected static final String CAS_SERVER_HEADER_KEY = "CasServer";
protected static final String CAS_SERVER_PATTERN_KEY = "$CASSERVER";
protected static final String NUXEO_SERVER_PATTERN_KEY = "$NUXEO";
protected static final String LOGIN_ACTION = "Login";
protected static final String LOGOUT_ACTION = "Logout";
protected static final String VALIDATE_ACTION = "Valid";
protected static final String PROXY_VALIDATE_ACTION = "ProxyValid";
protected static final Log log = LogFactory.getLog(Cas2Authenticator.class);
protected static final String EXCLUDE_PROMPT_KEY = "excludePromptURL";
protected static final String ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME = "org.nuxeo.auth.plugin.alternative";
protected String ticketKey = "ticket";
protected String proxyKey = "proxy";
protected String appURL = "http://127.0.0.1:8080/nuxeo/";
protected String serviceLoginURL = "http://127.0.0.1:8080/cas/login";
protected String serviceValidateURL = "http://127.0.0.1:8080/cas/serviceValidate";
/**
* We tell the CAS server whether we want a plain text (CAS 1.0) or XML (CAS 2.0) response by making the request
* either to the '.../validate' or '.../serviceValidate' URL. The older protocol supports only the CAS 1.0
* functionality, which is left around as the legacy '.../validate' URL.
*/
protected String proxyValidateURL = "http://127.0.0.1:8080/cas/proxyValidate";
protected String serviceKey = "service";
protected String logoutURL = "";
protected String defaultCasServer = "";
protected String ticketValidatorClassName = "edu.yale.its.tp.cas.client.ServiceTicketValidator";
protected String proxyValidatorClassName = "edu.yale.its.tp.cas.client.ProxyTicketValidator";
protected boolean promptLogin = true;
protected List<String> excludePromptURLs;
protected String errorPage;
public List<String> getUnAuthenticatedURLPrefix() {
// CAS login screen is not part of Nuxeo5 Web App
return null;
}
protected String getServiceURL(HttpServletRequest httpRequest, String action) {
String url = "";
if (action.equals(LOGIN_ACTION)) {
url = serviceLoginURL;
} else if (action.equals(LOGOUT_ACTION)) {
url = logoutURL;
} else if (action.equals(VALIDATE_ACTION)) {
url = serviceValidateURL;
} else if (action.equals(PROXY_VALIDATE_ACTION)) {
url = proxyValidateURL;
}
if (url.contains(CAS_SERVER_PATTERN_KEY)) {
String serverURL = httpRequest.getHeader(CAS_SERVER_HEADER_KEY);
if (serverURL != null) {
url = url.replace(CAS_SERVER_PATTERN_KEY, serverURL);
} else {
if (url.contains(CAS_SERVER_PATTERN_KEY)) {
url = url.replace(CAS_SERVER_PATTERN_KEY, defaultCasServer);
}
}
}
log.debug("serviceUrl: " + url);
return url;
}
public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
// Check for an alternative authentication plugin in request cookies
NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse);
if (alternativeAuthPlugin != null) {
log.debug(String.format("Found alternative authentication plugin %s, using it to handle login prompt.",
alternativeAuthPlugin));
return alternativeAuthPlugin.handleLoginPrompt(httpRequest, httpResponse, baseURL);
}
// Redirect to CAS Login screen
// passing our application URL as service name
String location = null;
try {
Map<String, String> urlParameters = new HashMap<String, String>();
urlParameters.put("service", getAppURL(httpRequest));
location = URIUtils.addParametersToURIQuery(getServiceURL(httpRequest, LOGIN_ACTION), urlParameters);
httpResponse.sendRedirect(location);
} catch (IOException e) {
log.error("Unable to redirect to CAS login screen to " + location, e);
return false;
}
return true;
}
protected String getAppURL(HttpServletRequest httpRequest) {
if (isValidStartupPage(httpRequest)) {
StringBuffer sb = new StringBuffer(VirtualHostHelper.getServerURL(httpRequest));
if (VirtualHostHelper.getServerURL(httpRequest).endsWith("/")) {
sb.deleteCharAt(sb.length() - 1);
}
sb.append(httpRequest.getRequestURI());
if (httpRequest.getQueryString() != null) {
sb.append("?");
sb.append(httpRequest.getQueryString());
// remove ticket parameter from URL to correctly validate the
// service
int indexTicketKey = sb.lastIndexOf(ticketKey + "=");
if (indexTicketKey != -1) {
sb.delete(indexTicketKey - 1, sb.length());
}
}
return sb.toString();
}
if (appURL == null || appURL.equals("")) {
appURL = NUXEO_SERVER_PATTERN_KEY;
}
if (appURL.contains(NUXEO_SERVER_PATTERN_KEY)) {
String nxurl = BaseURL.getBaseURL(httpRequest);
return appURL.replace(NUXEO_SERVER_PATTERN_KEY, nxurl);
} else {
return appURL;
}
}
private boolean isValidStartupPage(HttpServletRequest httpRequest) {
if (httpRequest.getRequestURI() == null) {
return false;
}
PluggableAuthenticationService service = (PluggableAuthenticationService) Framework.getRuntime().getComponent(
PluggableAuthenticationService.NAME);
if (service == null) {
return false;
}
String startPage = httpRequest.getRequestURI().replace(VirtualHostHelper.getContextPath(httpRequest) + "/", "");
for (String prefix : service.getStartURLPatterns()) {
if (startPage.startsWith(prefix)) {
return true;
}
}
return false;
}
public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
String casTicket = httpRequest.getParameter(ticketKey);
// Retrieve the proxy parameter for knowing if the caller is à proxy
// CAS
String proxy = httpRequest.getParameter(proxyKey);
if (casTicket == null) {
log.debug("No ticket found");
return null;
}
String userName;
if (proxy == null) {
// no ticket found
userName = checkCasTicket(casTicket, httpRequest);
} else {
userName = checkProxyCasTicket(casTicket, httpRequest);
}
if (userName == null) {
return null;
}
UserIdentificationInfo uui = new UserIdentificationInfo(userName, casTicket);
uui.setToken(casTicket);
return uui;
}
public void initPlugin(Map<String, String> parameters) {
if (parameters.containsKey(CAS2Parameters.TICKET_NAME_KEY)) {
ticketKey = parameters.get(CAS2Parameters.TICKET_NAME_KEY);
}
if (parameters.containsKey(CAS2Parameters.PROXY_NAME_KEY)) {
proxyKey = parameters.get(CAS2Parameters.PROXY_NAME_KEY);
}
if (parameters.containsKey(CAS2Parameters.NUXEO_APP_URL_KEY)) {
appURL = parameters.get(CAS2Parameters.NUXEO_APP_URL_KEY);
}
if (parameters.containsKey(CAS2Parameters.SERVICE_LOGIN_URL_KEY)) {
serviceLoginURL = parameters.get(CAS2Parameters.SERVICE_LOGIN_URL_KEY);
}
if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATE_URL_KEY)) {
serviceValidateURL = parameters.get(CAS2Parameters.SERVICE_VALIDATE_URL_KEY);
}
if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATE_URL_KEY)) {
proxyValidateURL = parameters.get(CAS2Parameters.PROXY_VALIDATE_URL_KEY);
}
if (parameters.containsKey(CAS2Parameters.SERVICE_NAME_KEY)) {
serviceKey = parameters.get(CAS2Parameters.SERVICE_NAME_KEY);
}
if (parameters.containsKey(CAS2Parameters.LOGOUT_URL_KEY)) {
logoutURL = parameters.get(CAS2Parameters.LOGOUT_URL_KEY);
}
if (parameters.containsKey(CAS2Parameters.DEFAULT_CAS_SERVER_KEY)) {
defaultCasServer = parameters.get(CAS2Parameters.DEFAULT_CAS_SERVER_KEY);
}
if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATOR_CLASS)) {
ticketValidatorClassName = parameters.get(CAS2Parameters.SERVICE_VALIDATOR_CLASS);
}
if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATOR_CLASS)) {
proxyValidatorClassName = parameters.get(CAS2Parameters.PROXY_VALIDATOR_CLASS);
}
if (parameters.containsKey(CAS2Parameters.PROMPT_LOGIN)) {
promptLogin = Boolean.parseBoolean(parameters.get(CAS2Parameters.PROMPT_LOGIN));
}
excludePromptURLs = new ArrayList<String>();
for (String key : parameters.keySet()) {
if (key.startsWith(EXCLUDE_PROMPT_KEY)) {
excludePromptURLs.add(parameters.get(key));
}
}
if (parameters.containsKey(CAS2Parameters.ERROR_PAGE)) {
errorPage = parameters.get(CAS2Parameters.ERROR_PAGE);
}
}
public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
String requestedURI = httpRequest.getRequestURI();
String context = httpRequest.getContextPath() + '/';
requestedURI = requestedURI.substring(context.length());
for (String prefixURL : excludePromptURLs) {
if (requestedURI.startsWith(prefixURL)) {
return false;
}
}
return promptLogin;
}
public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
// Check for an alternative authentication plugin in request cookies
NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse);
if (alternativeAuthPlugin != null) {
if (alternativeAuthPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) {
log.debug(String.format("Found alternative authentication plugin %s, using it to handle logout.",
alternativeAuthPlugin));
return ((NuxeoAuthenticationPluginLogoutExtension) alternativeAuthPlugin).handleLogout(httpRequest,
httpResponse);
} else {
log.debug(String.format(
"Found alternative authentication plugin %s which cannot handle logout, letting authentication filter handle it.",
alternativeAuthPlugin));
return false;
}
}
if (logoutURL == null || logoutURL.equals("")) {
log.debug("No CAS logout params, skipping CAS2Logout");
return false;
}
try {
httpResponse.sendRedirect(getServiceURL(httpRequest, LOGOUT_ACTION));
} catch (IOException e) {
log.error("Unable to redirect to CAS logout screen:", e);
return false;
}
return true;
}
protected String checkProxyCasTicket(String ticket, HttpServletRequest httpRequest) {
// Get the service passed by the portlet
String service = httpRequest.getParameter(serviceKey);
if (service == null) {
log.error("checkProxyCasTicket: no service name in the URL");
return null;
}
ProxyTicketValidator proxyValidator;
try {
proxyValidator = (ProxyTicketValidator) Framework.getRuntime().getContext().loadClass(
proxyValidatorClassName).newInstance();
} catch (InstantiationException e) {
log.error(
"checkProxyCasTicket during the ProxyTicketValidator initialization with InstantiationException:",
e);
return null;
} catch (IllegalAccessException e) {
log.error(
"checkProxyCasTicket during the ProxyTicketValidator initialization with IllegalAccessException:",
e);
return null;
} catch (ClassNotFoundException e) {
log.error(
"checkProxyCasTicket during the ProxyTicketValidator initialization with ClassNotFoundException:",
e);
return null;
}
proxyValidator.setCasValidateUrl(getServiceURL(httpRequest, PROXY_VALIDATE_ACTION));
proxyValidator.setService(service);
proxyValidator.setServiceTicket(ticket);
try {
proxyValidator.validate();
} catch (IOException e) {
log.error("checkProxyCasTicket failed with IOException:", e);
return null;
} catch (SAXException e) {
log.error("checkProxyCasTicket failed with SAXException:", e);
return null;
} catch (ParserConfigurationException e) {
log.error("checkProxyCasTicket failed with ParserConfigurationException:", e);
return null;
}
log.debug("checkProxyCasTicket: validation executed without error");
String username = proxyValidator.getUser();
log.debug("checkProxyCasTicket: validation returned username = " + username);
return username;
}
// Cas2 Ticket management
protected String checkCasTicket(String ticket, HttpServletRequest httpRequest) {
ServiceTicketValidator ticketValidator;
try {
ticketValidator = (ServiceTicketValidator) Framework.getRuntime().getContext().loadClass(
ticketValidatorClassName).newInstance();
} catch (InstantiationException e) {
log.error("checkCasTicket during the ServiceTicketValidator initialization with InstantiationException:", e);
return null;
} catch (IllegalAccessException e) {
log.error("checkCasTicket during the ServiceTicketValidator initialization with IllegalAccessException:", e);
return null;
} catch (ClassNotFoundException e) {
log.error("checkCasTicket during the ServiceTicketValidator initialization with ClassNotFoundException:", e);
return null;
}
ticketValidator.setCasValidateUrl(getServiceURL(httpRequest, VALIDATE_ACTION));
ticketValidator.setService(getAppURL(httpRequest));
ticketValidator.setServiceTicket(ticket);
try {
ticketValidator.validate();
} catch (IOException e) {
log.error("checkCasTicket failed with IOException:", e);
return null;
} catch (SAXException e) {
log.error("checkCasTicket failed with SAXException:", e);
return null;
} catch (ParserConfigurationException e) {
log.error("checkCasTicket failed with ParserConfigurationException:", e);
return null;
}
log.debug("checkCasTicket : validation executed without error");
String username = ticketValidator.getUser();
log.debug("checkCasTicket: validation returned username = " + username);
return username;
}
@Override
public boolean onError(HttpServletRequest request, HttpServletResponse response) {
try {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
if (errorPage != null) {
response.sendRedirect(errorPage);
}
} catch (IOException e) {
log.error(e);
return false;
}
return true;
}
@Override
public boolean onSuccess(HttpServletRequest arg0, HttpServletResponse arg1) {
// TODO Auto-generated method stub
return false;
}
protected NuxeoAuthenticationPlugin getAlternativeAuthPlugin(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
Cookie alternativeAuthPluginCookie = getCookie(httpRequest, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME);
if (alternativeAuthPluginCookie != null) {
String alternativeAuthPluginName = alternativeAuthPluginCookie.getValue();
PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime().getComponent(
PluggableAuthenticationService.NAME);
NuxeoAuthenticationPlugin alternativeAuthPlugin = authService.getPlugin(alternativeAuthPluginName);
if (alternativeAuthPlugin == null) {
log.error(String.format("No alternative authentication plugin named %s, will remove cookie %s.",
alternativeAuthPluginName, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME));
removeCookie(httpRequest, httpResponse, alternativeAuthPluginCookie);
} else {
return alternativeAuthPlugin;
}
}
return null;
}
protected Cookie getCookie(HttpServletRequest httpRequest, String cookieName) {
Cookie cookies[] = httpRequest.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
if (cookieName.equals(cookies[i].getName())) {
return cookies[i];
}
}
}
return null;
}
protected void removeCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse, Cookie cookie) {
log.debug(String.format("Removing cookie %s.", cookie.getName()));
cookie.setMaxAge(0);
cookie.setValue("");
cookie.setPath(httpRequest.getContextPath());
httpResponse.addCookie(cookie);
}
}