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