001/*
002 * (C) Copyright 2017 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 *     Thomas Roger
018 *
019 */
020package org.nuxeo.ecm.platform.oauth2;
021
022import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
023import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
024import static javax.servlet.http.HttpServletResponse.SC_OK;
025import static org.nuxeo.ecm.platform.oauth2.Constants.AUTHORIZATION_CODE_GRANT_TYPE;
026import static org.nuxeo.ecm.platform.oauth2.Constants.AUTHORIZATION_CODE_PARAM;
027import static org.nuxeo.ecm.platform.oauth2.Constants.REFRESH_TOKEN_GRANT_TYPE;
028import static org.nuxeo.ecm.platform.oauth2.Constants.STATE_PARAM;
029import static org.nuxeo.ecm.platform.oauth2.Constants.TOKEN_SERVICE;
030
031import java.io.IOException;
032import java.util.HashMap;
033import java.util.Map;
034
035import javax.servlet.RequestDispatcher;
036import javax.servlet.ServletException;
037import javax.servlet.http.HttpServlet;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040
041import org.apache.commons.lang.StringUtils;
042import org.codehaus.jackson.map.ObjectMapper;
043import org.nuxeo.common.utils.URIUtils;
044import org.nuxeo.ecm.platform.oauth2.clients.OAuth2Client;
045import org.nuxeo.ecm.platform.oauth2.clients.OAuth2ClientService;
046import org.nuxeo.ecm.platform.oauth2.request.AuthorizationRequest;
047import org.nuxeo.ecm.platform.oauth2.request.TokenRequest;
048import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
049import org.nuxeo.ecm.platform.oauth2.tokens.OAuth2TokenStore;
050import org.nuxeo.runtime.api.Framework;
051import org.nuxeo.runtime.transaction.TransactionHelper;
052
053/**
054 * @since 9.2
055 */
056public class NuxeoOAuth2Servlet extends HttpServlet {
057
058    private static final long serialVersionUID = 1L;
059
060    public static final String ENDPOINT_AUTH = "authorize";
061
062    public static final String ENDPOINT_TOKEN = "token";
063
064    public static final String ENDPOINT_AUTH_SUBMIT = "authorize_submit";
065
066    public static final String AUTHORIZATION_KEY = "authorization_key";
067
068    public static final String ERROR_PARAM = "error";
069
070    public static final String ERROR_DESCRIPTION_PARAM = "error_description";
071
072    public static final String CLIENT_NAME = "client_name";
073
074    public static final String GRANT_JSP_PAGE_PATH = "/oauth2Grant.jsp";
075
076    public static final String GRANT_ACCESS_PARAM = "grant_access";
077
078    public static final String ERROR_JSP_PAGE_PATH = "/oauth2error.jsp";
079
080    public static final int ACCESS_TOKEN_EXPIRATION_TIME = 3600 * 1000;
081
082    protected OAuth2TokenStore tokenStore = new OAuth2TokenStore(TOKEN_SERVICE);
083
084    @Override
085    protected void doGet(HttpServletRequest request, HttpServletResponse response)
086            throws ServletException, IOException {
087        String pathInfo = request.getPathInfo();
088        if (pathInfo.endsWith(ENDPOINT_AUTH)) {
089            doGetAuthorize(request, response);
090        } else {
091            response.sendError(SC_NOT_FOUND);
092        }
093    }
094
095    @Override
096    protected void doPost(HttpServletRequest request, HttpServletResponse response)
097            throws ServletException, IOException {
098        String pathInfo = request.getPathInfo();
099        if (pathInfo.endsWith(ENDPOINT_AUTH_SUBMIT)) {
100            doPostAuthorizeSubmit(request, response);
101        } else if (pathInfo.endsWith(ENDPOINT_TOKEN)) {
102            doPostToken(request, response);
103        } else {
104            response.sendError(SC_NOT_FOUND);
105        }
106    }
107
108    protected void doGetAuthorize(HttpServletRequest request, HttpServletResponse response)
109            throws IOException, ServletException {
110        AuthorizationRequest authRequest = AuthorizationRequest.fromRequest(request);
111        OAuth2Error error = authRequest.checkError();
112        if (error != null) {
113            handleError(error, request, response);
114            return;
115        }
116
117        AuthorizationRequest.store(authRequest.getAuthorizationKey(), authRequest);
118        OAuth2ClientService clientService = Framework.getService(OAuth2ClientService.class);
119        request.setAttribute(AUTHORIZATION_KEY, authRequest.getAuthorizationKey());
120        request.setAttribute(CLIENT_NAME, clientService.getClient(authRequest.getClientId()).getName());
121        String state = request.getParameter(STATE_PARAM);
122        if (StringUtils.isNotBlank(state)) {
123            request.setAttribute(STATE_PARAM, state);
124        }
125
126        RequestDispatcher requestDispatcher = request.getRequestDispatcher(GRANT_JSP_PAGE_PATH);
127        requestDispatcher.forward(request, response);
128    }
129
130    protected void doPostAuthorizeSubmit(HttpServletRequest request, HttpServletResponse response)
131            throws IOException, ServletException {
132        String authKeyForm = request.getParameter(AUTHORIZATION_KEY);
133        AuthorizationRequest authRequest = AuthorizationRequest.get(authKeyForm);
134        if (authRequest == null) {
135            handleError(OAuth2Error.invalidRequest(String.format("Invalid %s: %s.", AUTHORIZATION_KEY, authKeyForm)),
136                    request, response);
137            return;
138        }
139
140        AuthorizationRequest.remove(authRequest.getAuthorizationKey());
141
142        OAuth2Error error = authRequest.checkError();
143        if (error != null) {
144            handleError(error, request, response);
145            return;
146        }
147
148        // If the redirect URI was included in the authorization request use it else fall back on the first one
149        // registered for the client
150        String redirectURI = authRequest.getRedirectURI();
151        if (StringUtils.isBlank(redirectURI)) {
152            redirectURI = Framework.getService(OAuth2ClientService.class)
153                                   .getClient(authRequest.getClientId())
154                                   .getRedirectURIs()
155                                   .get(0);
156        }
157        String state = request.getParameter(STATE_PARAM);
158        String grantAccess = request.getParameter(GRANT_ACCESS_PARAM);
159        if (grantAccess == null) {
160            // the user deny access
161            error = OAuth2Error.accessDenied();
162            Map<String, String> params = new HashMap<>();
163            params.put(ERROR_PARAM, error.getId());
164            String errorDescription = error.getDescription();
165            if (StringUtils.isNotBlank(errorDescription)) {
166                params.put(ERROR_DESCRIPTION_PARAM, errorDescription);
167            }
168            if (StringUtils.isNotBlank(state)) {
169                params.put(STATE_PARAM, state);
170            }
171            sendRedirect(request, response, redirectURI, params);
172            return;
173        }
174
175        // now store the authorization request according to its code
176        // to be able to retrieve it in the "/oauth2/token" endpoint
177        String authorizationCode = authRequest.getAuthorizationCode();
178        AuthorizationRequest.store(authorizationCode, authRequest);
179        Map<String, String> params = new HashMap<>();
180        params.put(AUTHORIZATION_CODE_PARAM, authorizationCode);
181        if (StringUtils.isNotBlank(state)) {
182            params.put(STATE_PARAM, state);
183        }
184
185        sendRedirect(request, response, redirectURI, params);
186    }
187
188    protected void doPostToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
189        TokenRequest tokenRequest = new TokenRequest(request);
190        OAuth2ClientService clientService = Framework.getService(OAuth2ClientService.class);
191        // Process Authorization code
192        if (AUTHORIZATION_CODE_GRANT_TYPE.equals(tokenRequest.getGrantType())) {
193            AuthorizationRequest authRequest = AuthorizationRequest.get(tokenRequest.getCode());
194            OAuth2Error error = null;
195            if (authRequest == null) {
196                error = OAuth2Error.accessDenied();
197            }
198            // Check that clientId is the good one, already verified in
199            // authorization request
200            else if (!authRequest.getClientId().equals(tokenRequest.getClientId())) {
201                error = OAuth2Error.accessDenied();
202            } else {
203                OAuth2Client client = clientService.getClient(authRequest.getClientId());
204                // Validate client secret
205                if (client == null || !client.isValidWith(tokenRequest.getClientId(), tokenRequest.getClientSecret())) {
206                    error = OAuth2Error.unauthorizedClient();
207                }
208                // Ensure redirect URIs are identical if the redirect_uri parameter was included in the authorization
209                // request
210                else {
211                    String authRequestRedirectURI = authRequest.getRedirectURI();
212                    if (StringUtils.isNotBlank(authRequestRedirectURI)
213                            && !authRequestRedirectURI.equals(tokenRequest.getRedirectURI())) {
214                        error = OAuth2Error.invalidRequest();
215                    }
216                }
217            }
218
219            if (authRequest != null) {
220                AuthorizationRequest.remove(authRequest.getAuthorizationCode());
221            }
222
223            if (error != null) {
224                handleJsonError(error, response);
225                return;
226            }
227
228            // Store token
229            NuxeoOAuth2Token token = new NuxeoOAuth2Token(ACCESS_TOKEN_EXPIRATION_TIME, authRequest.getClientId());
230            TransactionHelper.runInTransaction(() -> tokenStore.store(authRequest.getUsername(), token));
231
232            handleTokenResponse(token, response);
233        } else if (REFRESH_TOKEN_GRANT_TYPE.equals(tokenRequest.getGrantType())) {
234            OAuth2Error error = null;
235            if (StringUtils.isBlank(tokenRequest.getClientId())) {
236                error = OAuth2Error.accessDenied();
237            } else if (!clientService.isValidClient(tokenRequest.getClientId(), tokenRequest.getClientSecret())) {
238                error = OAuth2Error.accessDenied();
239            }
240
241            if (error != null) {
242                handleJsonError(error, response);
243                return;
244            }
245
246            NuxeoOAuth2Token refreshed = TransactionHelper.runInTransaction(
247                    () -> tokenStore.refresh(tokenRequest.getRefreshToken(), tokenRequest.getClientId()));
248
249            if (refreshed == null) {
250                handleJsonError(OAuth2Error.invalidRequest(), response);
251            } else {
252                handleTokenResponse(refreshed, response);
253            }
254        } else {
255            handleJsonError(OAuth2Error.invalidGrant(), response);
256        }
257    }
258
259    protected void handleTokenResponse(NuxeoOAuth2Token token, HttpServletResponse response) throws IOException {
260        response.setHeader("Content-Type", "application/json");
261        response.setStatus(SC_OK);
262        ObjectMapper mapper = new ObjectMapper();
263        mapper.writeValue(response.getWriter(), token.toJsonObject());
264    }
265
266    protected void handleError(OAuth2Error error, HttpServletRequest request, HttpServletResponse response)
267            throws IOException, ServletException {
268        request.getSession().invalidate();
269        response.reset();
270        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
271        request.setAttribute("error", error);
272        RequestDispatcher requestDispatcher = request.getRequestDispatcher(ERROR_JSP_PAGE_PATH);
273        requestDispatcher.forward(request, response);
274    }
275
276    protected void handleJsonError(OAuth2Error error, HttpServletResponse response) throws IOException {
277        response.setHeader("Content-Type", "application/json");
278        response.setStatus(SC_BAD_REQUEST);
279
280        Map<String, String> object = new HashMap<>();
281        object.put(ERROR_PARAM, error.getId());
282        if (StringUtils.isNotBlank(error.getDescription())) {
283            object.put(ERROR_DESCRIPTION_PARAM, error.getDescription());
284        }
285        ObjectMapper mapper = new ObjectMapper();
286        mapper.writeValue(response.getWriter(), object);
287    }
288
289    protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String redirectURI,
290            Map<String, String> params) throws IOException {
291        request.getSession().invalidate();
292        if (redirectURI == null) {
293            response.sendError(SC_BAD_REQUEST, "No redirect URI");
294            return;
295        }
296
297        String url = URIUtils.addParametersToURIQuery(redirectURI, params);
298        response.sendRedirect(url);
299    }
300}