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}