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}