001/* 002 * (C) Copyright 2006-2009 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * Academie de Rennes - proxy CAS support 017 * 018 * $Id: JOOoConvertPluginImpl.java 18651 2007-05-13 20:28:53Z sfermigier $ 019 */ 020 021package org.nuxeo.ecm.platform.ui.web.auth.cas2; 022 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import javax.servlet.http.Cookie; 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletResponse; 032import javax.xml.parsers.ParserConfigurationException; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.common.utils.URIUtils; 037import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo; 038import org.nuxeo.ecm.platform.ui.web.auth.interfaces.LoginResponseHandler; 039import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin; 040import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension; 041import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService; 042import org.nuxeo.ecm.platform.ui.web.util.BaseURL; 043import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 044import org.nuxeo.runtime.api.Framework; 045import org.xml.sax.SAXException; 046 047import edu.yale.its.tp.cas.client.ProxyTicketValidator; 048import edu.yale.its.tp.cas.client.ServiceTicketValidator; 049 050/** 051 * @author Thierry Delprat 052 * @author Olivier Adam 053 * @author M.-A. Darche 054 * @author Benjamin Jalon 055 * @author Thierry Martins 056 */ 057public class Cas2Authenticator implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension, 058 LoginResponseHandler { 059 060 protected static final String CAS_SERVER_HEADER_KEY = "CasServer"; 061 062 protected static final String CAS_SERVER_PATTERN_KEY = "$CASSERVER"; 063 064 protected static final String NUXEO_SERVER_PATTERN_KEY = "$NUXEO"; 065 066 protected static final String LOGIN_ACTION = "Login"; 067 068 protected static final String LOGOUT_ACTION = "Logout"; 069 070 protected static final String VALIDATE_ACTION = "Valid"; 071 072 protected static final String PROXY_VALIDATE_ACTION = "ProxyValid"; 073 074 protected static final Log log = LogFactory.getLog(Cas2Authenticator.class); 075 076 protected static final String EXCLUDE_PROMPT_KEY = "excludePromptURL"; 077 078 protected static final String ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME = "org.nuxeo.auth.plugin.alternative"; 079 080 protected String ticketKey = "ticket"; 081 082 protected String proxyKey = "proxy"; 083 084 protected String appURL = "http://127.0.0.1:8080/nuxeo/"; 085 086 protected String serviceLoginURL = "http://127.0.0.1:8080/cas/login"; 087 088 protected String serviceValidateURL = "http://127.0.0.1:8080/cas/serviceValidate"; 089 090 /** 091 * We tell the CAS server whether we want a plain text (CAS 1.0) or XML (CAS 2.0) response by making the request 092 * either to the '.../validate' or '.../serviceValidate' URL. The older protocol supports only the CAS 1.0 093 * functionality, which is left around as the legacy '.../validate' URL. 094 */ 095 protected String proxyValidateURL = "http://127.0.0.1:8080/cas/proxyValidate"; 096 097 protected String serviceKey = "service"; 098 099 protected String logoutURL = ""; 100 101 protected String defaultCasServer = ""; 102 103 protected String ticketValidatorClassName = "edu.yale.its.tp.cas.client.ServiceTicketValidator"; 104 105 protected String proxyValidatorClassName = "edu.yale.its.tp.cas.client.ProxyTicketValidator"; 106 107 protected boolean promptLogin = true; 108 109 protected List<String> excludePromptURLs; 110 111 protected String errorPage; 112 113 public List<String> getUnAuthenticatedURLPrefix() { 114 // CAS login screen is not part of Nuxeo5 Web App 115 return null; 116 } 117 118 protected String getServiceURL(HttpServletRequest httpRequest, String action) { 119 String url = ""; 120 if (action.equals(LOGIN_ACTION)) { 121 url = serviceLoginURL; 122 } else if (action.equals(LOGOUT_ACTION)) { 123 url = logoutURL; 124 } else if (action.equals(VALIDATE_ACTION)) { 125 url = serviceValidateURL; 126 } else if (action.equals(PROXY_VALIDATE_ACTION)) { 127 url = proxyValidateURL; 128 } 129 130 if (url.contains(CAS_SERVER_PATTERN_KEY)) { 131 String serverURL = httpRequest.getHeader(CAS_SERVER_HEADER_KEY); 132 if (serverURL != null) { 133 url = url.replace(CAS_SERVER_PATTERN_KEY, serverURL); 134 } else { 135 if (url.contains(CAS_SERVER_PATTERN_KEY)) { 136 url = url.replace(CAS_SERVER_PATTERN_KEY, defaultCasServer); 137 } 138 } 139 } 140 log.debug("serviceUrl: " + url); 141 return url; 142 } 143 144 public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) { 145 146 // Check for an alternative authentication plugin in request cookies 147 NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse); 148 if (alternativeAuthPlugin != null) { 149 log.debug(String.format("Found alternative authentication plugin %s, using it to handle login prompt.", 150 alternativeAuthPlugin)); 151 return alternativeAuthPlugin.handleLoginPrompt(httpRequest, httpResponse, baseURL); 152 } 153 154 // Redirect to CAS Login screen 155 // passing our application URL as service name 156 String location = null; 157 try { 158 Map<String, String> urlParameters = new HashMap<String, String>(); 159 urlParameters.put("service", getAppURL(httpRequest)); 160 location = URIUtils.addParametersToURIQuery(getServiceURL(httpRequest, LOGIN_ACTION), urlParameters); 161 httpResponse.sendRedirect(location); 162 } catch (IOException e) { 163 log.error("Unable to redirect to CAS login screen to " + location, e); 164 return false; 165 } 166 return true; 167 } 168 169 protected String getAppURL(HttpServletRequest httpRequest) { 170 if (isValidStartupPage(httpRequest)) { 171 StringBuffer sb = new StringBuffer(VirtualHostHelper.getServerURL(httpRequest)); 172 if (VirtualHostHelper.getServerURL(httpRequest).endsWith("/")) { 173 sb.deleteCharAt(sb.length() - 1); 174 } 175 sb.append(httpRequest.getRequestURI()); 176 if (httpRequest.getQueryString() != null) { 177 sb.append("?"); 178 sb.append(httpRequest.getQueryString()); 179 180 // remove ticket parameter from URL to correctly validate the 181 // service 182 int indexTicketKey = sb.lastIndexOf(ticketKey + "="); 183 if (indexTicketKey != -1) { 184 sb.delete(indexTicketKey - 1, sb.length()); 185 } 186 } 187 188 return sb.toString(); 189 } 190 if (appURL == null || appURL.equals("")) { 191 appURL = NUXEO_SERVER_PATTERN_KEY; 192 } 193 if (appURL.contains(NUXEO_SERVER_PATTERN_KEY)) { 194 String nxurl = BaseURL.getBaseURL(httpRequest); 195 return appURL.replace(NUXEO_SERVER_PATTERN_KEY, nxurl); 196 } else { 197 return appURL; 198 } 199 } 200 201 private boolean isValidStartupPage(HttpServletRequest httpRequest) { 202 if (httpRequest.getRequestURI() == null) { 203 return false; 204 } 205 PluggableAuthenticationService service = (PluggableAuthenticationService) Framework.getRuntime().getComponent( 206 PluggableAuthenticationService.NAME); 207 if (service == null) { 208 return false; 209 } 210 String startPage = httpRequest.getRequestURI().replace(VirtualHostHelper.getContextPath(httpRequest) + "/", ""); 211 for (String prefix : service.getStartURLPatterns()) { 212 if (startPage.startsWith(prefix)) { 213 return true; 214 } 215 } 216 return false; 217 } 218 219 public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest, 220 HttpServletResponse httpResponse) { 221 String casTicket = httpRequest.getParameter(ticketKey); 222 223 // Retrieve the proxy parameter for knowing if the caller is à proxy 224 // CAS 225 String proxy = httpRequest.getParameter(proxyKey); 226 227 if (casTicket == null) { 228 log.debug("No ticket found"); 229 return null; 230 } 231 232 String userName; 233 234 if (proxy == null) { 235 // no ticket found 236 userName = checkCasTicket(casTicket, httpRequest); 237 } else { 238 userName = checkProxyCasTicket(casTicket, httpRequest); 239 } 240 241 if (userName == null) { 242 return null; 243 } 244 245 UserIdentificationInfo uui = new UserIdentificationInfo(userName, casTicket); 246 uui.setToken(casTicket); 247 248 return uui; 249 } 250 251 public void initPlugin(Map<String, String> parameters) { 252 if (parameters.containsKey(CAS2Parameters.TICKET_NAME_KEY)) { 253 ticketKey = parameters.get(CAS2Parameters.TICKET_NAME_KEY); 254 } 255 if (parameters.containsKey(CAS2Parameters.PROXY_NAME_KEY)) { 256 proxyKey = parameters.get(CAS2Parameters.PROXY_NAME_KEY); 257 } 258 if (parameters.containsKey(CAS2Parameters.NUXEO_APP_URL_KEY)) { 259 appURL = parameters.get(CAS2Parameters.NUXEO_APP_URL_KEY); 260 } 261 if (parameters.containsKey(CAS2Parameters.SERVICE_LOGIN_URL_KEY)) { 262 serviceLoginURL = parameters.get(CAS2Parameters.SERVICE_LOGIN_URL_KEY); 263 } 264 if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATE_URL_KEY)) { 265 serviceValidateURL = parameters.get(CAS2Parameters.SERVICE_VALIDATE_URL_KEY); 266 } 267 if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATE_URL_KEY)) { 268 proxyValidateURL = parameters.get(CAS2Parameters.PROXY_VALIDATE_URL_KEY); 269 } 270 if (parameters.containsKey(CAS2Parameters.SERVICE_NAME_KEY)) { 271 serviceKey = parameters.get(CAS2Parameters.SERVICE_NAME_KEY); 272 } 273 if (parameters.containsKey(CAS2Parameters.LOGOUT_URL_KEY)) { 274 logoutURL = parameters.get(CAS2Parameters.LOGOUT_URL_KEY); 275 } 276 if (parameters.containsKey(CAS2Parameters.DEFAULT_CAS_SERVER_KEY)) { 277 defaultCasServer = parameters.get(CAS2Parameters.DEFAULT_CAS_SERVER_KEY); 278 } 279 if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATOR_CLASS)) { 280 ticketValidatorClassName = parameters.get(CAS2Parameters.SERVICE_VALIDATOR_CLASS); 281 } 282 if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATOR_CLASS)) { 283 proxyValidatorClassName = parameters.get(CAS2Parameters.PROXY_VALIDATOR_CLASS); 284 } 285 if (parameters.containsKey(CAS2Parameters.PROMPT_LOGIN)) { 286 promptLogin = Boolean.parseBoolean(parameters.get(CAS2Parameters.PROMPT_LOGIN)); 287 } 288 excludePromptURLs = new ArrayList<String>(); 289 for (String key : parameters.keySet()) { 290 if (key.startsWith(EXCLUDE_PROMPT_KEY)) { 291 excludePromptURLs.add(parameters.get(key)); 292 } 293 } 294 if (parameters.containsKey(CAS2Parameters.ERROR_PAGE)) { 295 errorPage = parameters.get(CAS2Parameters.ERROR_PAGE); 296 } 297 } 298 299 public Boolean needLoginPrompt(HttpServletRequest httpRequest) { 300 String requestedURI = httpRequest.getRequestURI(); 301 String context = httpRequest.getContextPath() + '/'; 302 requestedURI = requestedURI.substring(context.length()); 303 for (String prefixURL : excludePromptURLs) { 304 if (requestedURI.startsWith(prefixURL)) { 305 return false; 306 } 307 } 308 return promptLogin; 309 } 310 311 public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 312 313 // Check for an alternative authentication plugin in request cookies 314 NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse); 315 if (alternativeAuthPlugin != null) { 316 if (alternativeAuthPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) { 317 log.debug(String.format("Found alternative authentication plugin %s, using it to handle logout.", 318 alternativeAuthPlugin)); 319 return ((NuxeoAuthenticationPluginLogoutExtension) alternativeAuthPlugin).handleLogout(httpRequest, 320 httpResponse); 321 } else { 322 log.debug(String.format( 323 "Found alternative authentication plugin %s which cannot handle logout, letting authentication filter handle it.", 324 alternativeAuthPlugin)); 325 return false; 326 } 327 } 328 329 if (logoutURL == null || logoutURL.equals("")) { 330 log.debug("No CAS logout params, skipping CAS2Logout"); 331 return false; 332 } 333 try { 334 httpResponse.sendRedirect(getServiceURL(httpRequest, LOGOUT_ACTION)); 335 } catch (IOException e) { 336 log.error("Unable to redirect to CAS logout screen:", e); 337 return false; 338 } 339 return true; 340 } 341 342 protected String checkProxyCasTicket(String ticket, HttpServletRequest httpRequest) { 343 // Get the service passed by the portlet 344 String service = httpRequest.getParameter(serviceKey); 345 if (service == null) { 346 log.error("checkProxyCasTicket: no service name in the URL"); 347 return null; 348 } 349 350 ProxyTicketValidator proxyValidator; 351 try { 352 proxyValidator = (ProxyTicketValidator) Framework.getRuntime().getContext().loadClass( 353 proxyValidatorClassName).newInstance(); 354 } catch (InstantiationException e) { 355 log.error( 356 "checkProxyCasTicket during the ProxyTicketValidator initialization with InstantiationException:", 357 e); 358 return null; 359 } catch (IllegalAccessException e) { 360 log.error( 361 "checkProxyCasTicket during the ProxyTicketValidator initialization with IllegalAccessException:", 362 e); 363 return null; 364 } catch (ClassNotFoundException e) { 365 log.error( 366 "checkProxyCasTicket during the ProxyTicketValidator initialization with ClassNotFoundException:", 367 e); 368 return null; 369 } 370 371 proxyValidator.setCasValidateUrl(getServiceURL(httpRequest, PROXY_VALIDATE_ACTION)); 372 proxyValidator.setService(service); 373 proxyValidator.setServiceTicket(ticket); 374 try { 375 proxyValidator.validate(); 376 } catch (IOException e) { 377 log.error("checkProxyCasTicket failed with IOException:", e); 378 return null; 379 } catch (SAXException e) { 380 log.error("checkProxyCasTicket failed with SAXException:", e); 381 return null; 382 } catch (ParserConfigurationException e) { 383 log.error("checkProxyCasTicket failed with ParserConfigurationException:", e); 384 return null; 385 } 386 log.debug("checkProxyCasTicket: validation executed without error"); 387 String username = proxyValidator.getUser(); 388 log.debug("checkProxyCasTicket: validation returned username = " + username); 389 390 return username; 391 } 392 393 // Cas2 Ticket management 394 protected String checkCasTicket(String ticket, HttpServletRequest httpRequest) { 395 ServiceTicketValidator ticketValidator; 396 try { 397 ticketValidator = (ServiceTicketValidator) Framework.getRuntime().getContext().loadClass( 398 ticketValidatorClassName).newInstance(); 399 } catch (InstantiationException e) { 400 log.error("checkCasTicket during the ServiceTicketValidator initialization with InstantiationException:", e); 401 return null; 402 } catch (IllegalAccessException e) { 403 log.error("checkCasTicket during the ServiceTicketValidator initialization with IllegalAccessException:", e); 404 return null; 405 } catch (ClassNotFoundException e) { 406 log.error("checkCasTicket during the ServiceTicketValidator initialization with ClassNotFoundException:", e); 407 return null; 408 } 409 410 ticketValidator.setCasValidateUrl(getServiceURL(httpRequest, VALIDATE_ACTION)); 411 ticketValidator.setService(getAppURL(httpRequest)); 412 ticketValidator.setServiceTicket(ticket); 413 try { 414 ticketValidator.validate(); 415 } catch (IOException e) { 416 log.error("checkCasTicket failed with IOException:", e); 417 return null; 418 } catch (SAXException e) { 419 log.error("checkCasTicket failed with SAXException:", e); 420 return null; 421 } catch (ParserConfigurationException e) { 422 log.error("checkCasTicket failed with ParserConfigurationException:", e); 423 return null; 424 } 425 log.debug("checkCasTicket : validation executed without error"); 426 String username = ticketValidator.getUser(); 427 log.debug("checkCasTicket: validation returned username = " + username); 428 return username; 429 } 430 431 @Override 432 public boolean onError(HttpServletRequest request, HttpServletResponse response) { 433 try { 434 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 435 if (errorPage != null) { 436 response.sendRedirect(errorPage); 437 } 438 } catch (IOException e) { 439 log.error(e); 440 return false; 441 } 442 return true; 443 } 444 445 @Override 446 public boolean onSuccess(HttpServletRequest arg0, HttpServletResponse arg1) { 447 // TODO Auto-generated method stub 448 return false; 449 } 450 451 protected NuxeoAuthenticationPlugin getAlternativeAuthPlugin(HttpServletRequest httpRequest, 452 HttpServletResponse httpResponse) { 453 454 Cookie alternativeAuthPluginCookie = getCookie(httpRequest, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME); 455 if (alternativeAuthPluginCookie != null) { 456 String alternativeAuthPluginName = alternativeAuthPluginCookie.getValue(); 457 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime().getComponent( 458 PluggableAuthenticationService.NAME); 459 NuxeoAuthenticationPlugin alternativeAuthPlugin = authService.getPlugin(alternativeAuthPluginName); 460 if (alternativeAuthPlugin == null) { 461 log.error(String.format("No alternative authentication plugin named %s, will remove cookie %s.", 462 alternativeAuthPluginName, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME)); 463 removeCookie(httpRequest, httpResponse, alternativeAuthPluginCookie); 464 } else { 465 return alternativeAuthPlugin; 466 } 467 } 468 return null; 469 } 470 471 protected Cookie getCookie(HttpServletRequest httpRequest, String cookieName) { 472 Cookie cookies[] = httpRequest.getCookies(); 473 if (cookies != null) { 474 for (int i = 0; i < cookies.length; i++) { 475 if (cookieName.equals(cookies[i].getName())) { 476 return cookies[i]; 477 } 478 } 479 } 480 return null; 481 } 482 483 protected void removeCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse, Cookie cookie) { 484 log.debug(String.format("Removing cookie %s.", cookie.getName())); 485 cookie.setMaxAge(0); 486 cookie.setValue(""); 487 cookie.setPath(httpRequest.getContextPath()); 488 httpResponse.addCookie(cookie); 489 } 490}