/* * (C) Copyright 2006-2008 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nuxeo - initial API and implementation * * $Id$ */ package org.nuxeo.ecm.platform.ui.web.auth.oauth; import java.io.IOException; import java.net.URISyntaxException; import java.net.URLEncoder; import java.security.Principal; import java.util.LinkedHashMap; import java.util.Map; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.oauth.OAuth; import net.oauth.OAuthAccessor; import net.oauth.OAuthException; import net.oauth.OAuthMessage; import net.oauth.OAuthValidator; import net.oauth.SimpleOAuthValidator; import net.oauth.server.OAuthServlet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.URIUtils; import org.nuxeo.ecm.platform.oauth.consumers.NuxeoOAuthConsumer; import org.nuxeo.ecm.platform.oauth.consumers.OAuthConsumerRegistry; import org.nuxeo.ecm.platform.oauth.keys.OAuthServerKeyManager; import org.nuxeo.ecm.platform.oauth.tokens.OAuthToken; import org.nuxeo.ecm.platform.oauth.tokens.OAuthTokenStore; import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter; import org.nuxeo.ecm.platform.ui.web.auth.NuxeoSecuredRequestWrapper; import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthPreFilter; import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.transaction.TransactionHelper; /** * This Filter is registered as a pre-Filter of NuxeoAuthenticationFilter. * <p> * It is used to handle OAuth Authentication : * <ul> * <li>3 legged OAuth negociation * <li>2 legged OAuth (Signed fetch) * </ul> * * @author tiry */ public class NuxeoOAuthFilter implements NuxeoAuthPreFilter { protected static final Log log = LogFactory.getLog(NuxeoOAuthFilter.class); protected static OAuthValidator validator; protected static OAuthConsumerRegistry consumerRegistry; protected OAuthValidator getValidator() { if (validator == null) { validator = new SimpleOAuthValidator(); } return validator; } protected OAuthConsumerRegistry getOAuthConsumerRegistry() { if (consumerRegistry == null) { consumerRegistry = Framework.getLocalService(OAuthConsumerRegistry.class); } return consumerRegistry; } protected OAuthTokenStore getOAuthTokenStore() { return Framework.getLocalService(OAuthTokenStore.class); } protected boolean isOAuthSignedRequest(HttpServletRequest httpRequest) { String authHeader = httpRequest.getHeader("Authorization"); if (authHeader != null && authHeader.contains("OAuth")) { return true; } if ("GET".equals(httpRequest.getMethod()) && httpRequest.getParameter("oauth_signature") != null) { return true; } else if ("POST".equals(httpRequest.getMethod()) && "application/x-www-form-urlencoded".equals(httpRequest.getContentType()) && httpRequest.getParameter("oauth_signature") != null) { return true; } return false; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!accept(request)) { chain.doFilter(request, response); return; } boolean startedTx = false; if (!TransactionHelper.isTransactionActive()) { startedTx = TransactionHelper.startTransaction(); } boolean done = false; try { process(request, response, chain); done = true; } finally { if (startedTx) { if (done == false) { TransactionHelper.setTransactionRollbackOnly(); } TransactionHelper.commitOrRollbackTransaction(); } } } protected boolean accept(ServletRequest request) { if (!(request instanceof HttpServletRequest)) { return false; } HttpServletRequest httpRequest = (HttpServletRequest) request; String uri = httpRequest.getRequestURI(); if (uri.contains("/oauth/")) { return true; } if (isOAuthSignedRequest(httpRequest)) { return true; } return false; } protected void process(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String uri = httpRequest.getRequestURI(); // process OAuth 3 legged calls if (uri.contains("/oauth/")) { String call = uri.split("/oauth/")[1]; if (call.equals("authorize")) { processAuthorize(httpRequest, httpResponse); } else if (call.equals("request-token")) { processRequestToken(httpRequest, httpResponse); } else if (call.equals("access-token")) { processAccessToken(httpRequest, httpResponse); } else { httpResponse.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "OAuth call not supported"); } return; } // Signed request (simple 2 legged OAuth call or signed request // after a 3 legged nego) else if (isOAuthSignedRequest(httpRequest)) { LoginContext loginContext = processSignedRequest(httpRequest, httpResponse); // forward the call if authenticated if (loginContext != null) { Principal principal = (Principal) loginContext.getSubject().getPrincipals().toArray()[0]; try { chain.doFilter(new NuxeoSecuredRequestWrapper(httpRequest, principal), response); } finally { try { loginContext.logout(); } catch (LoginException e) { log.warn("Error when loging out", e); } } } else { if (!httpResponse.isCommitted()) { httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); } return; } } // Non OAuth calls can pass through else { throw new RuntimeException("request is not a outh request"); } } protected void processAuthorize(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { String token = httpRequest.getParameter(OAuth.OAUTH_TOKEN); if (httpRequest.getMethod().equals("GET")) { log.debug("OAuth authorize : from end user "); // initial access => send to real login page String loginUrl = VirtualHostHelper.getBaseURL(httpRequest); httpRequest.getSession(true).setAttribute("OAUTH-INFO", getOAuthTokenStore().getRequestToken(token)); String redirectUrl = "oauthGrant.jsp" + "?" + OAuth.OAUTH_TOKEN + "=" + token; redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); loginUrl = loginUrl + "login.jsp?requestedUrl=" + redirectUrl; httpResponse.sendRedirect(loginUrl); } else { // post after permission validation log.debug("OAuth authorize validate "); String nuxeo_login = httpRequest.getParameter("nuxeo_login"); String duration = httpRequest.getParameter("duration"); // XXX get what user has granted !!! OAuthToken rToken = getOAuthTokenStore().addVerifierToRequestToken(token, Long.parseLong(duration)); rToken.setNuxeoLogin(nuxeo_login); String cbUrl = rToken.getCallbackUrl(); if (cbUrl == null) { // get the callback url from the consumer ... String consumerKey = rToken.getConsumerKey(); NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey); if (consumer != null) { cbUrl = consumer.getCallbackURL(); } if (cbUrl == null) { // fall back to default Google oauth callback ... cbUrl = "http://oauth.gmodules.com/gadgets/oauthcallback"; } } Map<String, String> parameters = new LinkedHashMap<String, String>(); parameters.put(OAuth.OAUTH_TOKEN, rToken.getToken()); parameters.put("oauth_verifier", rToken.getVerifier()); String targetUrl = URIUtils.addParametersToURIQuery(cbUrl, parameters); log.debug("redirecting user after successful grant " + targetUrl); httpResponse.sendRedirect(targetUrl); } } protected void processRequestToken(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { OAuthMessage message = OAuthServlet.getMessage(httpRequest, null); String consumerKey = message.getConsumerKey(); NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey, message.getSignatureMethod()); if (consumer == null) { log.error("Consumer " + consumerKey + " is not registered"); int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN); httpResponse.sendError(errCode, "Unknown consumer key"); return; } OAuthAccessor accessor = new OAuthAccessor(consumer); OAuthValidator validator = getValidator(); try { validator.validateMessage(message, accessor); } catch (OAuthException | URISyntaxException | IOException e) { log.error("Error while validating OAuth signature", e); int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID); httpResponse.sendError(errCode, "Can not validate signature"); return; } log.debug("OAuth request-token : generate a tmp token"); String callBack = message.getParameter(OAuth.OAUTH_CALLBACK); // XXX should not only use consumerKey !!! OAuthToken rToken = getOAuthTokenStore().createRequestToken(consumerKey, callBack); httpResponse.setContentType("application/x-www-form-urlencoded"); httpResponse.setStatus(HttpServletResponse.SC_OK); StringBuffer sb = new StringBuffer(); sb.append(OAuth.OAUTH_TOKEN); sb.append("="); sb.append(rToken.getToken()); sb.append("&"); sb.append(OAuth.OAUTH_TOKEN_SECRET); sb.append("="); sb.append(rToken.getTokenSecret()); sb.append("&oauth_callback_confirmed=true"); log.debug("returning : " + sb.toString()); httpResponse.getWriter().write(sb.toString()); } protected void processAccessToken(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { OAuthMessage message = OAuthServlet.getMessage(httpRequest, null); String consumerKey = message.getConsumerKey(); String token = message.getToken(); NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey, message.getSignatureMethod()); if (consumer == null) { log.error("Consumer " + consumerKey + " is not registered"); int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN); httpResponse.sendError(errCode, "Unknown consumer key"); return; } OAuthAccessor accessor = new OAuthAccessor(consumer); OAuthToken rToken = getOAuthTokenStore().getRequestToken(token); accessor.requestToken = rToken.getToken(); accessor.tokenSecret = rToken.getTokenSecret(); OAuthValidator validator = getValidator(); try { validator.validateMessage(message, accessor); } catch (OAuthException | URISyntaxException | IOException e) { log.error("Error while validating OAuth signature", e); int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID); httpResponse.sendError(errCode, "Can not validate signature"); return; } log.debug("OAuth access-token : generate a real token"); String verif = message.getParameter("oauth_verifier"); token = message.getParameter(OAuth.OAUTH_TOKEN); log.debug("OAuth verifier = " + verif); boolean allowByPassVerifier = false; if (verif == null) { // here we don't have the verifier in the request // this is strictly prohibited in the spec // => see http://tools.ietf.org/html/rfc5849 page 11 // // Anyway since iGoogle does not seem to forward the verifier // we allow it for designated consumers allowByPassVerifier = consumer.allowBypassVerifier(); } if (rToken.getVerifier().equals(verif) || allowByPassVerifier) { // Ok we can authenticate OAuthToken aToken = getOAuthTokenStore().createAccessTokenFromRequestToken(rToken); httpResponse.setContentType("application/x-www-form-urlencoded"); httpResponse.setStatus(HttpServletResponse.SC_OK); StringBuilder sb = new StringBuilder(); sb.append(OAuth.OAUTH_TOKEN); sb.append("="); sb.append(aToken.getToken()); sb.append("&"); sb.append(OAuth.OAUTH_TOKEN_SECRET); sb.append("="); sb.append(aToken.getTokenSecret()); log.debug("returning : " + sb.toString()); httpResponse.getWriter().write(sb.toString()); } else { log.error("Verifier does not match : can not continue"); httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Verifier is not correct"); } } protected LoginContext processSignedRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { String URL = getRequestURL(httpRequest); OAuthMessage message = OAuthServlet.getMessage(httpRequest, URL); String consumerKey = message.getConsumerKey(); String signatureMethod = message.getSignatureMethod(); log.debug("Received OAuth signed request on " + httpRequest.getRequestURI() + " with consumerKey=" + consumerKey + " and signature method " + signatureMethod); NuxeoOAuthConsumer consumer = getOAuthConsumerRegistry().getConsumer(consumerKey, signatureMethod); if (consumer == null && consumerKey != null) { OAuthServerKeyManager okm = Framework.getLocalService(OAuthServerKeyManager.class); if (consumerKey.equals(okm.getInternalKey())) { consumer = okm.getInternalConsumer(); } } if (consumer == null) { int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN); log.error("Consumer " + consumerKey + " is unknown, can not authenticated"); httpResponse.sendError(errCode, "Consumer " + consumerKey + " is not registered"); return null; } else { OAuthAccessor accessor = new OAuthAccessor(consumer); OAuthValidator validator = getValidator(); OAuthToken aToken = getOAuthTokenStore().getAccessToken(message.getToken()); String targetLogin; if (aToken != null) { // Auth was done via 3 legged accessor.accessToken = aToken.getToken(); accessor.tokenSecret = aToken.getTokenSecret(); targetLogin = aToken.getNuxeoLogin(); } else { // 2 legged OAuth if (!consumer.allowSignedFetch()) { // int errCode = // OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_METHOD_REJECTED); // We need to send a 403 to force client to ask for a new // token in case the Access Token was deleted !!! int errCode = HttpServletResponse.SC_UNAUTHORIZED; httpResponse.sendError(errCode, "Signed fetch is not allowed"); return null; } targetLogin = consumer.getSignedFetchUser(); if (NuxeoOAuthConsumer.SIGNEDFETCH_OPENSOCIAL_VIEWER.equals(targetLogin)) { targetLogin = message.getParameter("opensocial_viewer_id"); } else if (NuxeoOAuthConsumer.SIGNEDFETCH_OPENSOCIAL_OWNER.equals(targetLogin)) { targetLogin = message.getParameter("opensocial_owner_id"); } } try { validator.validateMessage(message, accessor); if (targetLogin != null) { LoginContext loginContext = NuxeoAuthenticationFilter.loginAs(targetLogin); return loginContext; } else { int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.USER_REFUSED); httpResponse.sendError(errCode, "No configured login information"); return null; } } catch (OAuthException | URISyntaxException | IOException | LoginException e) { log.error("Error while validating OAuth signature", e); int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID); httpResponse.sendError(errCode, "Can not validate signature"); } } return null; } /** * Get the URL used for this request by checking the X-Forwarded-Proto header used in the request. * * @param httpRequest * @return * @since 5.9.5 */ public static String getRequestURL(HttpServletRequest httpRequest) { String URL = httpRequest.getRequestURL().toString(); String forwardedProto = httpRequest.getHeader("X-Forwarded-Proto"); if (forwardedProto != null && !URL.startsWith(forwardedProto)) { int protoDelimiterIndex = URL.indexOf("://"); URL = forwardedProto + URL.substring(protoDelimiterIndex); } return URL; } }