001/* 002 * (C) Copyright 2006-2008 Nuxeo SAS (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 * 017 * $Id$ 018 */ 019 020package org.nuxeo.ecm.platform.ui.web.auth.oauth; 021 022import java.io.IOException; 023import java.net.URISyntaxException; 024import java.net.URLEncoder; 025import java.security.Principal; 026import java.util.LinkedHashMap; 027import java.util.Map; 028 029import javax.security.auth.login.LoginContext; 030import javax.security.auth.login.LoginException; 031import javax.servlet.FilterChain; 032import javax.servlet.ServletException; 033import javax.servlet.ServletRequest; 034import javax.servlet.ServletResponse; 035import javax.servlet.http.HttpServletRequest; 036import javax.servlet.http.HttpServletResponse; 037 038import net.oauth.OAuth; 039import net.oauth.OAuthAccessor; 040import net.oauth.OAuthException; 041import net.oauth.OAuthMessage; 042import net.oauth.OAuthValidator; 043import net.oauth.SimpleOAuthValidator; 044import net.oauth.server.OAuthServlet; 045 046import org.apache.commons.logging.Log; 047import org.apache.commons.logging.LogFactory; 048import org.nuxeo.common.utils.URIUtils; 049import org.nuxeo.ecm.platform.oauth.consumers.NuxeoOAuthConsumer; 050import org.nuxeo.ecm.platform.oauth.consumers.OAuthConsumerRegistry; 051import org.nuxeo.ecm.platform.oauth.keys.OAuthServerKeyManager; 052import org.nuxeo.ecm.platform.oauth.tokens.OAuthToken; 053import org.nuxeo.ecm.platform.oauth.tokens.OAuthTokenStore; 054import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter; 055import org.nuxeo.ecm.platform.ui.web.auth.NuxeoSecuredRequestWrapper; 056import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthPreFilter; 057import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 058import org.nuxeo.runtime.api.Framework; 059import org.nuxeo.runtime.transaction.TransactionHelper; 060 061/** 062 * This Filter is registered as a pre-Filter of NuxeoAuthenticationFilter. 063 * <p> 064 * It is used to handle OAuth Authentication : 065 * <ul> 066 * <li>3 legged OAuth negociation 067 * <li>2 legged OAuth (Signed fetch) 068 * </ul> 069 * 070 * @author tiry 071 */ 072public class NuxeoOAuthFilter implements NuxeoAuthPreFilter { 073 074 protected static final Log log = LogFactory.getLog(NuxeoOAuthFilter.class); 075 076 protected static OAuthValidator validator; 077 078 protected static OAuthConsumerRegistry consumerRegistry; 079 080 protected OAuthValidator getValidator() { 081 if (validator == null) { 082 validator = new SimpleOAuthValidator(); 083 } 084 return validator; 085 } 086 087 protected OAuthConsumerRegistry getOAuthConsumerRegistry() { 088 if (consumerRegistry == null) { 089 consumerRegistry = Framework.getLocalService(OAuthConsumerRegistry.class); 090 } 091 return consumerRegistry; 092 } 093 094 protected OAuthTokenStore getOAuthTokenStore() { 095 return Framework.getLocalService(OAuthTokenStore.class); 096 } 097 098 protected boolean isOAuthSignedRequest(HttpServletRequest httpRequest) { 099 100 String authHeader = httpRequest.getHeader("Authorization"); 101 if (authHeader != null && authHeader.contains("OAuth")) { 102 return true; 103 } 104 105 if ("GET".equals(httpRequest.getMethod()) && httpRequest.getParameter("oauth_signature") != null) { 106 return true; 107 } else if ("POST".equals(httpRequest.getMethod()) 108 && "application/x-www-form-urlencoded".equals(httpRequest.getContentType()) 109 && httpRequest.getParameter("oauth_signature") != null) { 110 return true; 111 } 112 113 return false; 114 } 115 116 @Override 117 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, 118 ServletException { 119 if (!accept(request)) { 120 chain.doFilter(request, response); 121 return; 122 } 123 boolean startedTx = false; 124 if (!TransactionHelper.isTransactionActive()) { 125 startedTx = TransactionHelper.startTransaction(); 126 } 127 boolean done = false; 128 try { 129 process(request, response, chain); 130 done = true; 131 } finally { 132 if (startedTx) { 133 if (done == false) { 134 TransactionHelper.setTransactionRollbackOnly(); 135 } 136 TransactionHelper.commitOrRollbackTransaction(); 137 } 138 } 139 } 140 141 protected boolean accept(ServletRequest request) { 142 if (!(request instanceof HttpServletRequest)) { 143 return false; 144 } 145 HttpServletRequest httpRequest = (HttpServletRequest) request; 146 String uri = httpRequest.getRequestURI(); 147 if (uri.contains("/oauth/")) { 148 return true; 149 } 150 if (isOAuthSignedRequest(httpRequest)) { 151 return true; 152 } 153 return false; 154 } 155 156 protected void process(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, 157 ServletException { 158 159 HttpServletRequest httpRequest = (HttpServletRequest) request; 160 HttpServletResponse httpResponse = (HttpServletResponse) response; 161 162 String uri = httpRequest.getRequestURI(); 163 164 // process OAuth 3 legged calls 165 if (uri.contains("/oauth/")) { 166 String call = uri.split("/oauth/")[1]; 167 168 if (call.equals("authorize")) { 169 processAuthorize(httpRequest, httpResponse); 170 } else if (call.equals("request-token")) { 171 processRequestToken(httpRequest, httpResponse); 172 } else if (call.equals("access-token")) { 173 processAccessToken(httpRequest, httpResponse); 174 175 } else { 176 httpResponse.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "OAuth call not supported"); 177 } 178 return; 179 } 180 // Signed request (simple 2 legged OAuth call or signed request 181 // after a 3 legged nego) 182 else if (isOAuthSignedRequest(httpRequest)) { 183 184 LoginContext loginContext = processSignedRequest(httpRequest, httpResponse); 185 // forward the call if authenticated 186 if (loginContext != null) { 187 Principal principal = (Principal) loginContext.getSubject().getPrincipals().toArray()[0]; 188 try { 189 chain.doFilter(new NuxeoSecuredRequestWrapper(httpRequest, principal), response); 190 } finally { 191 try { 192 loginContext.logout(); 193 } catch (LoginException e) { 194 log.warn("Error when loging out", e); 195 } 196 } 197 } else { 198 if (!httpResponse.isCommitted()) { 199 httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); 200 } 201 return; 202 } 203 } 204 // Non OAuth calls can pass through 205 else { 206 throw new RuntimeException("request is not a outh request"); 207 } 208 } 209 210 protected void processAuthorize(HttpServletRequest httpRequest, HttpServletResponse httpResponse) 211 throws IOException, ServletException { 212 213 String token = httpRequest.getParameter(OAuth.OAUTH_TOKEN); 214 215 if (httpRequest.getMethod().equals("GET")) { 216 217 log.debug("OAuth authorize : from end user "); 218 219 // initial access => send to real login page 220 String loginUrl = VirtualHostHelper.getBaseURL(httpRequest); 221 222 httpRequest.getSession(true).setAttribute("OAUTH-INFO", getOAuthTokenStore().getRequestToken(token)); 223 224 String redirectUrl = "oauthGrant.jsp" + "?" + OAuth.OAUTH_TOKEN + "=" + token; 225 redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); 226 loginUrl = loginUrl + "login.jsp?requestedUrl=" + redirectUrl; 227 228 httpResponse.sendRedirect(loginUrl); 229 230 } else { 231 // post after permission validation 232 log.debug("OAuth authorize validate "); 233 234 String nuxeo_login = httpRequest.getParameter("nuxeo_login"); 235 String duration = httpRequest.getParameter("duration"); 236 237 // XXX get what user has granted !!! 238 239 OAuthToken rToken = getOAuthTokenStore().addVerifierToRequestToken(token, Long.parseLong(duration)); 240 rToken.setNuxeoLogin(nuxeo_login); 241 242 String cbUrl = rToken.getCallbackUrl(); 243 if (cbUrl == null) { 244 // get the callback url from the consumer ... 245 String consumerKey = rToken.getConsumerKey(); 246 NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey); 247 if (consumer != null) { 248 cbUrl = consumer.getCallbackURL(); 249 } 250 251 if (cbUrl == null) { 252 // fall back to default Google oauth callback ... 253 cbUrl = "http://oauth.gmodules.com/gadgets/oauthcallback"; 254 } 255 } 256 Map<String, String> parameters = new LinkedHashMap<String, String>(); 257 parameters.put(OAuth.OAUTH_TOKEN, rToken.getToken()); 258 parameters.put("oauth_verifier", rToken.getVerifier()); 259 String targetUrl = URIUtils.addParametersToURIQuery(cbUrl, parameters); 260 261 log.debug("redirecting user after successful grant " + targetUrl); 262 263 httpResponse.sendRedirect(targetUrl); 264 } 265 266 } 267 268 protected void processRequestToken(HttpServletRequest httpRequest, HttpServletResponse httpResponse) 269 throws IOException, ServletException { 270 271 OAuthMessage message = OAuthServlet.getMessage(httpRequest, null); 272 String consumerKey = message.getConsumerKey(); 273 274 NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey, message.getSignatureMethod()); 275 if (consumer == null) { 276 log.error("Consumer " + consumerKey + " is not registered"); 277 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN); 278 httpResponse.sendError(errCode, "Unknown consumer key"); 279 return; 280 } 281 OAuthAccessor accessor = new OAuthAccessor(consumer); 282 283 OAuthValidator validator = getValidator(); 284 try { 285 validator.validateMessage(message, accessor); 286 } catch (OAuthException | URISyntaxException | IOException e) { 287 log.error("Error while validating OAuth signature", e); 288 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID); 289 httpResponse.sendError(errCode, "Can not validate signature"); 290 return; 291 } 292 293 log.debug("OAuth request-token : generate a tmp token"); 294 String callBack = message.getParameter(OAuth.OAUTH_CALLBACK); 295 296 // XXX should not only use consumerKey !!! 297 OAuthToken rToken = getOAuthTokenStore().createRequestToken(consumerKey, callBack); 298 299 httpResponse.setContentType("application/x-www-form-urlencoded"); 300 httpResponse.setStatus(HttpServletResponse.SC_OK); 301 302 StringBuffer sb = new StringBuffer(); 303 sb.append(OAuth.OAUTH_TOKEN); 304 sb.append("="); 305 sb.append(rToken.getToken()); 306 sb.append("&"); 307 sb.append(OAuth.OAUTH_TOKEN_SECRET); 308 sb.append("="); 309 sb.append(rToken.getTokenSecret()); 310 sb.append("&oauth_callback_confirmed=true"); 311 312 log.debug("returning : " + sb.toString()); 313 314 httpResponse.getWriter().write(sb.toString()); 315 } 316 317 protected void processAccessToken(HttpServletRequest httpRequest, HttpServletResponse httpResponse) 318 throws IOException, ServletException { 319 320 OAuthMessage message = OAuthServlet.getMessage(httpRequest, null); 321 String consumerKey = message.getConsumerKey(); 322 String token = message.getToken(); 323 324 NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey, message.getSignatureMethod()); 325 326 if (consumer == null) { 327 log.error("Consumer " + consumerKey + " is not registered"); 328 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN); 329 httpResponse.sendError(errCode, "Unknown consumer key"); 330 return; 331 } 332 333 OAuthAccessor accessor = new OAuthAccessor(consumer); 334 335 OAuthToken rToken = getOAuthTokenStore().getRequestToken(token); 336 337 accessor.requestToken = rToken.getToken(); 338 accessor.tokenSecret = rToken.getTokenSecret(); 339 340 OAuthValidator validator = getValidator(); 341 342 try { 343 validator.validateMessage(message, accessor); 344 } catch (OAuthException | URISyntaxException | IOException e) { 345 log.error("Error while validating OAuth signature", e); 346 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID); 347 httpResponse.sendError(errCode, "Can not validate signature"); 348 return; 349 } 350 351 log.debug("OAuth access-token : generate a real token"); 352 353 String verif = message.getParameter("oauth_verifier"); 354 token = message.getParameter(OAuth.OAUTH_TOKEN); 355 356 log.debug("OAuth verifier = " + verif); 357 358 boolean allowByPassVerifier = false; 359 360 if (verif == null) { 361 // here we don't have the verifier in the request 362 // this is strictly prohibited in the spec 363 // => see http://tools.ietf.org/html/rfc5849 page 11 364 // 365 // Anyway since iGoogle does not seem to forward the verifier 366 // we allow it for designated consumers 367 368 allowByPassVerifier = consumer.allowBypassVerifier(); 369 } 370 371 if (rToken.getVerifier().equals(verif) || allowByPassVerifier) { 372 373 // Ok we can authenticate 374 OAuthToken aToken = getOAuthTokenStore().createAccessTokenFromRequestToken(rToken); 375 376 httpResponse.setContentType("application/x-www-form-urlencoded"); 377 httpResponse.setStatus(HttpServletResponse.SC_OK); 378 379 StringBuilder sb = new StringBuilder(); 380 sb.append(OAuth.OAUTH_TOKEN); 381 sb.append("="); 382 sb.append(aToken.getToken()); 383 sb.append("&"); 384 sb.append(OAuth.OAUTH_TOKEN_SECRET); 385 sb.append("="); 386 sb.append(aToken.getTokenSecret()); 387 388 log.debug("returning : " + sb.toString()); 389 390 httpResponse.getWriter().write(sb.toString()); 391 } else { 392 log.error("Verifier does not match : can not continue"); 393 httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Verifier is not correct"); 394 } 395 } 396 397 protected LoginContext processSignedRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) 398 throws IOException, ServletException { 399 400 String URL = getRequestURL(httpRequest); 401 402 OAuthMessage message = OAuthServlet.getMessage(httpRequest, URL); 403 404 String consumerKey = message.getConsumerKey(); 405 String signatureMethod = message.getSignatureMethod(); 406 407 log.debug("Received OAuth signed request on " + httpRequest.getRequestURI() + " with consumerKey=" 408 + consumerKey + " and signature method " + signatureMethod); 409 410 NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey, signatureMethod); 411 412 if (consumer == null && consumerKey != null) { 413 OAuthServerKeyManager okm = Framework.getLocalService(OAuthServerKeyManager.class); 414 if (consumerKey.equals(okm.getInternalKey())) { 415 consumer = okm.getInternalConsumer(); 416 } 417 } 418 419 if (consumer == null) { 420 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN); 421 log.error("Consumer " + consumerKey + " is unknown, can not authenticated"); 422 httpResponse.sendError(errCode, "Consumer " + consumerKey + " is not registered"); 423 return null; 424 } else { 425 426 OAuthAccessor accessor = new OAuthAccessor(consumer); 427 OAuthValidator validator = getValidator(); 428 429 OAuthToken aToken = getOAuthTokenStore().getAccessToken(message.getToken()); 430 431 String targetLogin; 432 if (aToken != null) { 433 // Auth was done via 3 legged 434 accessor.accessToken = aToken.getToken(); 435 accessor.tokenSecret = aToken.getTokenSecret(); 436 targetLogin = aToken.getNuxeoLogin(); 437 } else { 438 // 2 legged OAuth 439 if (!consumer.allowSignedFetch()) { 440 // int errCode = 441 // OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_METHOD_REJECTED); 442 // We need to send a 403 to force client to ask for a new 443 // token in case the Access Token was deleted !!! 444 int errCode = HttpServletResponse.SC_UNAUTHORIZED; 445 httpResponse.sendError(errCode, "Signed fetch is not allowed"); 446 return null; 447 } 448 targetLogin = consumer.getSignedFetchUser(); 449 if (NuxeoOAuthConsumer.SIGNEDFETCH_OPENSOCIAL_VIEWER.equals(targetLogin)) { 450 targetLogin = message.getParameter("opensocial_viewer_id"); 451 } else if (NuxeoOAuthConsumer.SIGNEDFETCH_OPENSOCIAL_OWNER.equals(targetLogin)) { 452 targetLogin = message.getParameter("opensocial_owner_id"); 453 } 454 } 455 456 try { 457 validator.validateMessage(message, accessor); 458 if (targetLogin != null) { 459 LoginContext loginContext = NuxeoAuthenticationFilter.loginAs(targetLogin); 460 return loginContext; 461 } else { 462 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.USER_REFUSED); 463 httpResponse.sendError(errCode, "No configured login information"); 464 return null; 465 } 466 } catch (OAuthException | URISyntaxException | IOException | LoginException e) { 467 log.error("Error while validating OAuth signature", e); 468 int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID); 469 httpResponse.sendError(errCode, "Can not validate signature"); 470 } 471 } 472 return null; 473 } 474 475 /** 476 * Get the URL used for this request by checking the X-Forwarded-Proto header used in the request. 477 * 478 * @param httpRequest 479 * @return 480 * @since 5.9.5 481 */ 482 public static String getRequestURL(HttpServletRequest httpRequest) { 483 String URL = httpRequest.getRequestURL().toString(); 484 String forwardedProto = httpRequest.getHeader("X-Forwarded-Proto"); 485 if (forwardedProto != null && !URL.startsWith(forwardedProto)) { 486 int protoDelimiterIndex = URL.indexOf("://"); 487 URL = forwardedProto + URL.substring(protoDelimiterIndex); 488 } 489 return URL; 490 } 491 492}