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.web.common.vh.VirtualHostHelper; 045import org.nuxeo.runtime.api.Framework; 046import org.xml.sax.SAXException; 047 048import edu.yale.its.tp.cas.client.ProxyTicketValidator; 049import edu.yale.its.tp.cas.client.ServiceTicketValidator; 050 051/** 052 * @author Thierry Delprat 053 * @author Olivier Adam 054 * @author M.-A. Darche 055 * @author Benjamin Jalon 056 * @author Thierry Martins 057 */ 058public class Cas2Authenticator 059 implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension, LoginResponseHandler { 060 061 protected static final String CAS_SERVER_HEADER_KEY = "CasServer"; 062 063 protected static final String CAS_SERVER_PATTERN_KEY = "$CASSERVER"; 064 065 protected static final String NUXEO_SERVER_PATTERN_KEY = "$NUXEO"; 066 067 protected static final String LOGIN_ACTION = "Login"; 068 069 protected static final String LOGOUT_ACTION = "Logout"; 070 071 protected static final String VALIDATE_ACTION = "Valid"; 072 073 protected static final String PROXY_VALIDATE_ACTION = "ProxyValid"; 074 075 protected static final Log log = LogFactory.getLog(Cas2Authenticator.class); 076 077 protected static final String EXCLUDE_PROMPT_KEY = "excludePromptURL"; 078 079 protected static final String ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME = "org.nuxeo.auth.plugin.alternative"; 080 081 protected String ticketKey = "ticket"; 082 083 protected String proxyKey = "proxy"; 084 085 protected String appURL = "http://127.0.0.1:8080/nuxeo/"; 086 087 protected String serviceLoginURL = "http://127.0.0.1:8080/cas/login"; 088 089 protected String serviceValidateURL = "http://127.0.0.1:8080/cas/serviceValidate"; 090 091 /** 092 * We tell the CAS server whether we want a plain text (CAS 1.0) or XML (CAS 2.0) response by making the request 093 * either to the '.../validate' or '.../serviceValidate' URL. The older protocol supports only the CAS 1.0 094 * functionality, which is left around as the legacy '.../validate' URL. 095 */ 096 protected String proxyValidateURL = "http://127.0.0.1:8080/cas/proxyValidate"; 097 098 protected String serviceKey = "service"; 099 100 protected String logoutURL = ""; 101 102 protected String defaultCasServer = ""; 103 104 protected String ticketValidatorClassName = "edu.yale.its.tp.cas.client.ServiceTicketValidator"; 105 106 protected String proxyValidatorClassName = "edu.yale.its.tp.cas.client.ProxyTicketValidator"; 107 108 protected boolean promptLogin = true; 109 110 protected List<String> excludePromptURLs; 111 112 protected String errorPage; 113 114 @Override 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 @Override 147 public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) { 148 149 // Check for an alternative authentication plugin in request cookies 150 NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse); 151 if (alternativeAuthPlugin != null) { 152 log.debug(String.format("Found alternative authentication plugin %s, using it to handle login prompt.", 153 alternativeAuthPlugin)); 154 return alternativeAuthPlugin.handleLoginPrompt(httpRequest, httpResponse, baseURL); 155 } 156 157 // Redirect to CAS Login screen 158 // passing our application URL as service name 159 String location = null; 160 try { 161 Map<String, String> urlParameters = new HashMap<>(); 162 urlParameters.put("service", getAppURL(httpRequest)); 163 location = URIUtils.addParametersToURIQuery(getServiceURL(httpRequest, LOGIN_ACTION), urlParameters); 164 httpResponse.sendRedirect(location); 165 } catch (IOException e) { 166 log.error("Unable to redirect to CAS login screen to " + location, e); 167 return false; 168 } 169 return true; 170 } 171 172 protected String getAppURL(HttpServletRequest httpRequest) { 173 if (isValidStartupPage(httpRequest)) { 174 StringBuilder sb = new StringBuilder(VirtualHostHelper.getServerURL(httpRequest)); 175 if (VirtualHostHelper.getServerURL(httpRequest).endsWith("/")) { 176 sb.deleteCharAt(sb.length() - 1); 177 } 178 sb.append(httpRequest.getRequestURI()); 179 if (httpRequest.getQueryString() != null) { 180 sb.append("?"); 181 sb.append(httpRequest.getQueryString()); 182 183 // remove ticket parameter from URL to correctly validate the 184 // service 185 int indexTicketKey = sb.lastIndexOf(ticketKey + "="); 186 if (indexTicketKey != -1) { 187 sb.delete(indexTicketKey - 1, sb.length()); 188 } 189 } 190 191 return sb.toString(); 192 } 193 if (appURL == null || appURL.equals("")) { 194 appURL = NUXEO_SERVER_PATTERN_KEY; 195 } 196 if (appURL.contains(NUXEO_SERVER_PATTERN_KEY)) { 197 String nxurl = VirtualHostHelper.getBaseURL(httpRequest); 198 return appURL.replace(NUXEO_SERVER_PATTERN_KEY, nxurl); 199 } else { 200 return appURL; 201 } 202 } 203 204 private boolean isValidStartupPage(HttpServletRequest httpRequest) { 205 if (httpRequest.getRequestURI() == null) { 206 return false; 207 } 208 PluggableAuthenticationService service = Framework.getService(PluggableAuthenticationService.class); 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 @Override 222 public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest, 223 HttpServletResponse httpResponse) { 224 String casTicket = httpRequest.getParameter(ticketKey); 225 226 // Retrieve the proxy parameter for knowing if the caller is à proxy 227 // CAS 228 String proxy = httpRequest.getParameter(proxyKey); 229 230 if (casTicket == null) { 231 log.debug("No ticket found"); 232 return null; 233 } 234 235 String userName; 236 237 if (proxy == null) { 238 // no ticket found 239 userName = checkCasTicket(casTicket, httpRequest); 240 } else { 241 userName = checkProxyCasTicket(casTicket, httpRequest); 242 } 243 244 if (userName == null) { 245 return null; 246 } 247 248 UserIdentificationInfo uui = new UserIdentificationInfo(userName); 249 uui.setToken(casTicket); 250 251 return uui; 252 } 253 254 @Override 255 public void initPlugin(Map<String, String> parameters) { 256 if (parameters.containsKey(CAS2Parameters.TICKET_NAME_KEY)) { 257 ticketKey = parameters.get(CAS2Parameters.TICKET_NAME_KEY); 258 } 259 if (parameters.containsKey(CAS2Parameters.PROXY_NAME_KEY)) { 260 proxyKey = parameters.get(CAS2Parameters.PROXY_NAME_KEY); 261 } 262 if (parameters.containsKey(CAS2Parameters.NUXEO_APP_URL_KEY)) { 263 appURL = parameters.get(CAS2Parameters.NUXEO_APP_URL_KEY); 264 } 265 if (parameters.containsKey(CAS2Parameters.SERVICE_LOGIN_URL_KEY)) { 266 serviceLoginURL = parameters.get(CAS2Parameters.SERVICE_LOGIN_URL_KEY); 267 } 268 if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATE_URL_KEY)) { 269 serviceValidateURL = parameters.get(CAS2Parameters.SERVICE_VALIDATE_URL_KEY); 270 } 271 if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATE_URL_KEY)) { 272 proxyValidateURL = parameters.get(CAS2Parameters.PROXY_VALIDATE_URL_KEY); 273 } 274 if (parameters.containsKey(CAS2Parameters.SERVICE_NAME_KEY)) { 275 serviceKey = parameters.get(CAS2Parameters.SERVICE_NAME_KEY); 276 } 277 if (parameters.containsKey(CAS2Parameters.LOGOUT_URL_KEY)) { 278 logoutURL = parameters.get(CAS2Parameters.LOGOUT_URL_KEY); 279 } 280 if (parameters.containsKey(CAS2Parameters.DEFAULT_CAS_SERVER_KEY)) { 281 defaultCasServer = parameters.get(CAS2Parameters.DEFAULT_CAS_SERVER_KEY); 282 } 283 if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATOR_CLASS)) { 284 ticketValidatorClassName = parameters.get(CAS2Parameters.SERVICE_VALIDATOR_CLASS); 285 } 286 if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATOR_CLASS)) { 287 proxyValidatorClassName = parameters.get(CAS2Parameters.PROXY_VALIDATOR_CLASS); 288 } 289 if (parameters.containsKey(CAS2Parameters.PROMPT_LOGIN)) { 290 promptLogin = Boolean.parseBoolean(parameters.get(CAS2Parameters.PROMPT_LOGIN)); 291 } 292 excludePromptURLs = new ArrayList<>(); 293 for (String key : parameters.keySet()) { 294 if (key.startsWith(EXCLUDE_PROMPT_KEY)) { 295 excludePromptURLs.add(parameters.get(key)); 296 } 297 } 298 if (parameters.containsKey(CAS2Parameters.ERROR_PAGE)) { 299 errorPage = parameters.get(CAS2Parameters.ERROR_PAGE); 300 } 301 } 302 303 @Override 304 public Boolean needLoginPrompt(HttpServletRequest httpRequest) { 305 String requestedURI = httpRequest.getRequestURI(); 306 String context = httpRequest.getContextPath() + '/'; 307 requestedURI = requestedURI.substring(context.length()); 308 for (String prefixURL : excludePromptURLs) { 309 if (requestedURI.startsWith(prefixURL)) { 310 return false; 311 } 312 } 313 return promptLogin; 314 } 315 316 @Override 317 public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 318 319 // Check for an alternative authentication plugin in request cookies 320 NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse); 321 if (alternativeAuthPlugin != null) { 322 if (alternativeAuthPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) { 323 log.debug(String.format("Found alternative authentication plugin %s, using it to handle logout.", 324 alternativeAuthPlugin)); 325 return ((NuxeoAuthenticationPluginLogoutExtension) alternativeAuthPlugin).handleLogout(httpRequest, 326 httpResponse); 327 } else { 328 log.debug(String.format( 329 "Found alternative authentication plugin %s which cannot handle logout, letting authentication filter handle it.", 330 alternativeAuthPlugin)); 331 return false; 332 } 333 } 334 335 if (logoutURL == null || logoutURL.equals("")) { 336 log.debug("No CAS logout params, skipping CAS2Logout"); 337 return false; 338 } 339 try { 340 httpResponse.sendRedirect(getServiceURL(httpRequest, LOGOUT_ACTION)); 341 } catch (IOException e) { 342 log.error("Unable to redirect to CAS logout screen:", e); 343 return false; 344 } 345 return true; 346 } 347 348 protected String checkProxyCasTicket(String ticket, HttpServletRequest httpRequest) { 349 // Get the service passed by the portlet 350 String service = httpRequest.getParameter(serviceKey); 351 if (service == null) { 352 log.error("checkProxyCasTicket: no service name in the URL"); 353 return null; 354 } 355 356 ProxyTicketValidator proxyValidator; 357 try { 358 proxyValidator = (ProxyTicketValidator) Framework.getRuntime() 359 .getContext() 360 .loadClass(proxyValidatorClassName) 361 .getDeclaredConstructor() 362 .newInstance(); 363 } catch (ReflectiveOperationException e) { 364 log.error("checkProxyCasTicket during the ProxyTicketValidator initialization", e); 365 return null; 366 } 367 368 proxyValidator.setCasValidateUrl(getServiceURL(httpRequest, PROXY_VALIDATE_ACTION)); 369 proxyValidator.setService(service); 370 proxyValidator.setServiceTicket(ticket); 371 try { 372 proxyValidator.validate(); 373 } catch (IOException e) { 374 log.error("checkProxyCasTicket failed with IOException:", e); 375 return null; 376 } catch (SAXException e) { 377 log.error("checkProxyCasTicket failed with SAXException:", e); 378 return null; 379 } catch (ParserConfigurationException e) { 380 log.error("checkProxyCasTicket failed with ParserConfigurationException:", e); 381 return null; 382 } 383 log.debug("checkProxyCasTicket: validation executed without error"); 384 String username = proxyValidator.getUser(); 385 log.debug("checkProxyCasTicket: validation returned username = " + username); 386 387 return username; 388 } 389 390 // Cas2 Ticket management 391 protected String checkCasTicket(String ticket, HttpServletRequest httpRequest) { 392 ServiceTicketValidator ticketValidator; 393 try { 394 ticketValidator = (ServiceTicketValidator) Framework.getRuntime() 395 .getContext() 396 .loadClass(ticketValidatorClassName) 397 .getDeclaredConstructor() 398 .newInstance(); 399 } catch (ReflectiveOperationException e) { 400 log.error("checkCasTicket during the ServiceTicketValidator initialization", e); 401 return null; 402 } 403 404 ticketValidator.setCasValidateUrl(getServiceURL(httpRequest, VALIDATE_ACTION)); 405 ticketValidator.setService(getAppURL(httpRequest)); 406 ticketValidator.setServiceTicket(ticket); 407 try { 408 ticketValidator.validate(); 409 } catch (IOException e) { 410 log.error("checkCasTicket failed with IOException:", e); 411 return null; 412 } catch (SAXException e) { 413 log.error("checkCasTicket failed with SAXException:", e); 414 return null; 415 } catch (ParserConfigurationException e) { 416 log.error("checkCasTicket failed with ParserConfigurationException:", e); 417 return null; 418 } 419 log.debug("checkCasTicket : validation executed without error"); 420 String username = ticketValidator.getUser(); 421 log.debug("checkCasTicket: validation returned username = " + username); 422 return username; 423 } 424 425 @Override 426 public boolean onError(HttpServletRequest request, HttpServletResponse response) { 427 try { 428 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 429 if (errorPage != null) { 430 response.sendRedirect(errorPage); 431 } 432 } catch (IOException e) { 433 log.error(e); 434 return false; 435 } 436 return true; 437 } 438 439 @Override 440 public boolean onSuccess(HttpServletRequest arg0, HttpServletResponse arg1) { 441 // TODO Auto-generated method stub 442 return false; 443 } 444 445 protected NuxeoAuthenticationPlugin getAlternativeAuthPlugin(HttpServletRequest httpRequest, 446 HttpServletResponse httpResponse) { 447 448 Cookie alternativeAuthPluginCookie = getCookie(httpRequest, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME); 449 if (alternativeAuthPluginCookie != null) { 450 String alternativeAuthPluginName = alternativeAuthPluginCookie.getValue(); 451 PluggableAuthenticationService authService = Framework.getService(PluggableAuthenticationService.class); 452 NuxeoAuthenticationPlugin alternativeAuthPlugin = authService.getPlugin(alternativeAuthPluginName); 453 if (alternativeAuthPlugin == null) { 454 log.error(String.format("No alternative authentication plugin named %s, will remove cookie %s.", 455 alternativeAuthPluginName, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME)); 456 removeCookie(httpRequest, httpResponse, alternativeAuthPluginCookie); 457 } else { 458 return alternativeAuthPlugin; 459 } 460 } 461 return null; 462 } 463 464 protected Cookie getCookie(HttpServletRequest httpRequest, String cookieName) { 465 Cookie cookies[] = httpRequest.getCookies(); 466 if (cookies != null) { 467 for (int i = 0; i < cookies.length; i++) { 468 if (cookieName.equals(cookies[i].getName())) { 469 return cookies[i]; 470 } 471 } 472 } 473 return null; 474 } 475 476 protected void removeCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse, Cookie cookie) { 477 log.debug(String.format("Removing cookie %s.", cookie.getName())); 478 cookie.setMaxAge(0); 479 cookie.setValue(""); 480 cookie.setPath(httpRequest.getContextPath()); 481 httpResponse.addCookie(cookie); 482 } 483}