001/*
002 * (C) Copyright 2018 Nuxeo (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 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.platform.ui.web.auth.oauth;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
023import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
024import static javax.servlet.http.HttpServletResponse.SC_OK;
025import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
026import static net.oauth.OAuth.OAUTH_CALLBACK;
027import static net.oauth.OAuth.OAUTH_TOKEN;
028import static net.oauth.OAuth.OAUTH_TOKEN_SECRET;
029import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.REQUESTED_URL;
030
031import java.io.IOException;
032import java.io.UnsupportedEncodingException;
033import java.net.URISyntaxException;
034import java.net.URLEncoder;
035import java.util.LinkedHashMap;
036import java.util.Map;
037
038import javax.servlet.ServletException;
039import javax.servlet.http.HttpServlet;
040import javax.servlet.http.HttpServletRequest;
041import javax.servlet.http.HttpServletResponse;
042
043import org.apache.commons.logging.Log;
044import org.apache.commons.logging.LogFactory;
045import org.nuxeo.common.utils.URIUtils;
046import org.nuxeo.ecm.core.api.NuxeoException;
047import org.nuxeo.ecm.platform.oauth.consumers.NuxeoOAuthConsumer;
048import org.nuxeo.ecm.platform.oauth.consumers.OAuthConsumerRegistry;
049import org.nuxeo.ecm.platform.oauth.tokens.OAuthToken;
050import org.nuxeo.ecm.platform.oauth.tokens.OAuthTokenStore;
051import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
052import org.nuxeo.runtime.api.Framework;
053
054import net.oauth.OAuth;
055import net.oauth.OAuthAccessor;
056import net.oauth.OAuthException;
057import net.oauth.OAuthMessage;
058import net.oauth.OAuthValidator;
059import net.oauth.SimpleOAuthValidator;
060import net.oauth.server.OAuthServlet;
061
062/**
063 * Servlet for the /oauth endpoint.
064 *
065 * @since 10.3
066 */
067public class NuxeoOAuth1Servlet extends HttpServlet {
068
069    private static final long serialVersionUID = 1L;
070
071    private static final Log log = LogFactory.getLog(NuxeoOAuth1Servlet.class);
072
073    public static final String ENDPOINT_REQUEST_TOKEN = "/request-token";
074
075    public static final String ENDPOINT_AUTHORIZE = "/authorize";
076
077    public static final String ENDPOINT_ACCESS_TOKEN = "/access-token";
078
079    public static final String OAUTH_VERIFIER = "oauth_verifier";
080
081    public static final String OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed";
082
083    public static final String NUXEO_LOGIN_PARAM = "nuxeo_login";
084
085    public static final String DURATION_PARAM = "duration";
086
087    public static final String OAUTH_INFO_SESSION_KEY = "OAUTH-INFO";
088
089    public static final String GRANT_PAGE = "oauthGrant.jsp";
090
091    public static final String LOGIN_PAGE = "login.jsp";
092
093    public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
094
095    protected static String urlEncode(String string) {
096        try {
097            return URLEncoder.encode(string, UTF_8.name());
098        } catch (UnsupportedEncodingException e) {
099            throw new NuxeoException(e); // cannot happen
100        }
101    }
102
103    @Override
104    protected void doGet(HttpServletRequest request, HttpServletResponse response)
105            throws ServletException, IOException {
106        String pathInfo = request.getPathInfo();
107        if (pathInfo == null) {
108            response.sendError(SC_NOT_FOUND);
109        } else if (pathInfo.equals(ENDPOINT_REQUEST_TOKEN)) {
110            doGetRequestToken(request, response);
111        } else if (pathInfo.equals(ENDPOINT_AUTHORIZE)) {
112            doGetAuthorize(request, response);
113        } else if (pathInfo.equals(ENDPOINT_ACCESS_TOKEN)) {
114            doGetAccessToken(request, response);
115        } else {
116            response.sendError(SC_NOT_FOUND);
117        }
118    }
119
120    @Override
121    protected void doPost(HttpServletRequest request, HttpServletResponse response)
122            throws ServletException, IOException {
123        String pathInfo = request.getPathInfo();
124        if (pathInfo == null) {
125            response.sendError(SC_NOT_FOUND);
126        } else if (pathInfo.equals(ENDPOINT_AUTHORIZE)) {
127            doPostAuthorize(request, response);
128        } else {
129            response.sendError(SC_NOT_FOUND);
130        }
131    }
132
133    /**
134     * Generates a request token, redirects to the Nuxeo login page, and provides a later redirect URL to the OAuth
135     * grant page.
136     */
137    protected void doGetAuthorize(HttpServletRequest request, HttpServletResponse response) throws IOException {
138        String token = request.getParameter(OAUTH_TOKEN);
139        // create request token
140        OAuthTokenStore tokenStore = Framework.getService(OAuthTokenStore.class);
141        OAuthToken rToken = tokenStore.getRequestToken(token);
142        // store it in session
143        request.getSession(true).setAttribute(OAUTH_INFO_SESSION_KEY, rToken);
144        // redirect to login page with appropriate further redirect URL
145        String redirectUrl = GRANT_PAGE + "?" + OAUTH_TOKEN + "=" + urlEncode(token);
146        String url = VirtualHostHelper.getBaseURL(request) + LOGIN_PAGE + "?" + REQUESTED_URL + "="
147                + urlEncode(redirectUrl);
148        response.sendRedirect(url);
149    }
150
151    /**
152     * Adds a verifier and username to the request token and redirects to the callback URL.
153     */
154    protected void doPostAuthorize(HttpServletRequest request, HttpServletResponse response) throws IOException {
155        String token = request.getParameter(OAUTH_TOKEN);
156        String username = request.getParameter(NUXEO_LOGIN_PARAM);
157        String duration = request.getParameter(DURATION_PARAM);
158        // add verifier and username to request token
159        OAuthTokenStore tokenStore = Framework.getService(OAuthTokenStore.class);
160        OAuthToken rToken = tokenStore.addVerifierToRequestToken(token, Long.valueOf(duration));
161        rToken.setNuxeoLogin(username);
162        // find callback url
163        String callbackUrl = rToken.getCallbackUrl();
164        if (callbackUrl == null) {
165            // get the callback url from the consumer
166            OAuthConsumerRegistry consumerRegistry = Framework.getService(OAuthConsumerRegistry.class);
167            NuxeoOAuthConsumer consumer = consumerRegistry.getConsumer(rToken.getConsumerKey());
168            if (consumer == null) {
169                int sc = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN).intValue();
170                response.sendError(sc, "Unknown consumer key");
171                return;
172            }
173            callbackUrl = consumer.getCallbackURL();
174            if (callbackUrl == null) {
175                log.error("No callback URL configured for consumer: " + rToken.getConsumerKey());
176                response.sendError(SC_INTERNAL_SERVER_ERROR);
177                return;
178            }
179        }
180        Map<String, String> params = new LinkedHashMap<>();
181        params.put(OAUTH_TOKEN, rToken.getToken());
182        params.put(OAUTH_VERIFIER, rToken.getVerifier());
183        String targetUrl = URIUtils.addParametersToURIQuery(callbackUrl, params);
184        response.sendRedirect(targetUrl);
185    }
186
187    protected void doGetRequestToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
188        // get consumer
189        OAuthMessage message = OAuthServlet.getMessage(request, null);
190        String consumerKey = message.getConsumerKey();
191        OAuthConsumerRegistry consumerRegistry = Framework.getService(OAuthConsumerRegistry.class);
192        NuxeoOAuthConsumer consumer = consumerRegistry.getConsumer(consumerKey, message.getSignatureMethod());
193        if (consumer == null) {
194            int sc = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN).intValue();
195            response.sendError(sc, "Unknown consumer key");
196            return;
197        }
198        OAuthAccessor accessor = new OAuthAccessor(consumer);
199        OAuthValidator validator = new SimpleOAuthValidator();
200        try {
201            validator.validateMessage(message, accessor);
202        } catch (OAuthException | URISyntaxException | IOException e) {
203            int sc = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID).intValue();
204            response.sendError(sc, "Cannot validate signature");
205            return;
206        }
207        String callbackUrl = message.getParameter(OAUTH_CALLBACK);
208        OAuthTokenStore tokenStore = Framework.getService(OAuthTokenStore.class);
209        OAuthToken rToken = tokenStore.createRequestToken(consumerKey, callbackUrl);
210
211        Map<String, String> params = new LinkedHashMap<>();
212        params.put(OAUTH_TOKEN, rToken.getToken());
213        params.put(OAUTH_TOKEN_SECRET, rToken.getTokenSecret());
214        params.put(OAUTH_CALLBACK_CONFIRMED, "true");
215        String body = URIUtils.getURIQuery(params);
216        response.setStatus(SC_OK);
217        response.setContentType(APPLICATION_X_WWW_FORM_URLENCODED);
218        response.getWriter().write(body);
219    }
220
221    protected void doGetAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
222        OAuthMessage message = OAuthServlet.getMessage(request, null);
223        String consumerKey = message.getConsumerKey();
224        String token = message.getToken();
225        OAuthConsumerRegistry consumerRegistry = Framework.getService(OAuthConsumerRegistry.class);
226        NuxeoOAuthConsumer consumer = consumerRegistry.getConsumer(consumerKey, message.getSignatureMethod());
227        if (consumer == null) {
228            int sc = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.CONSUMER_KEY_UNKNOWN).intValue();
229            response.sendError(sc, "Unknown consumer key");
230            return;
231        }
232        // get request token
233        OAuthAccessor accessor = new OAuthAccessor(consumer);
234        OAuthTokenStore tokenStore = Framework.getService(OAuthTokenStore.class);
235        OAuthToken rToken = tokenStore.getRequestToken(token);
236        accessor.requestToken = rToken.getToken();
237        accessor.tokenSecret = rToken.getTokenSecret();
238        // validate signature
239        OAuthValidator validator = new SimpleOAuthValidator();
240        try {
241            validator.validateMessage(message, accessor);
242        } catch (OAuthException | URISyntaxException | IOException e) {
243            int errCode = OAuth.Problems.TO_HTTP_CODE.get(OAuth.Problems.SIGNATURE_INVALID).intValue();
244            response.sendError(errCode, "Cannot validate signature");
245            return;
246        }
247
248        String verifier = message.getParameter(OAUTH_VERIFIER);
249        boolean allowByPassVerifier = false;
250        if (verifier == null) {
251            // here we don't have the verifier in the request
252            // this is strictly prohibited in the spec
253            // => see http://tools.ietf.org/html/rfc5849 page 11
254            // Anyway since iGoogle does not seem to forward the verifier
255            // we allow it for designated consumers
256            allowByPassVerifier = consumer.allowBypassVerifier();
257        }
258        if (!rToken.getVerifier().equals(verifier) && !allowByPassVerifier) {
259            response.sendError(SC_UNAUTHORIZED, "Verifier is not correct");
260            return;
261        }
262        OAuthToken aToken = tokenStore.createAccessTokenFromRequestToken(rToken);
263        response.setStatus(SC_OK);
264        response.setContentType(APPLICATION_X_WWW_FORM_URLENCODED);
265        Map<String, String> params = new LinkedHashMap<>();
266        params.put(OAUTH_TOKEN, aToken.getToken());
267        params.put(OAUTH_TOKEN_SECRET, aToken.getTokenSecret());
268        String body = URIUtils.getURIQuery(params);
269        response.getWriter().write(body);
270    }
271
272}