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