001/*
002 * (C) Copyright 2015 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 */
016package org.nuxeo.ecm.platform.ui.web.auth.oauth2;
017
018import static org.apache.commons.lang.StringUtils.isBlank;
019import static org.apache.commons.lang.StringUtils.isNotBlank;
020
021import java.io.IOException;
022import java.net.URLDecoder;
023import java.security.Principal;
024import java.util.HashMap;
025import java.util.Map;
026
027import javax.security.auth.login.LoginContext;
028import javax.security.auth.login.LoginException;
029import javax.servlet.FilterChain;
030import javax.servlet.ServletException;
031import javax.servlet.ServletRequest;
032import javax.servlet.ServletResponse;
033import javax.servlet.http.HttpServletRequest;
034import javax.servlet.http.HttpServletResponse;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.codehaus.jackson.map.ObjectMapper;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.platform.oauth2.clients.ClientRegistry;
041import org.nuxeo.ecm.platform.oauth2.request.AuthorizationRequest;
042import org.nuxeo.ecm.platform.oauth2.request.TokenRequest;
043import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
044import org.nuxeo.ecm.platform.oauth2.tokens.OAuth2TokenStore;
045import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter;
046import org.nuxeo.ecm.platform.ui.web.auth.NuxeoSecuredRequestWrapper;
047import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthPreFilter;
048import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
049import org.nuxeo.runtime.api.Framework;
050import org.nuxeo.runtime.transaction.TransactionHelper;
051
052/**
053 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a>
054 * @since 5.9.2
055 */
056public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter {
057
058    private static final Log log = LogFactory.getLog(NuxeoOAuth2Filter.class);
059
060    protected static final String TOKEN_SERVICE = "org.nuxeo.server.token.store";
061
062    protected static final String OAUTH2_SEGMENT = "/oauth2/";
063
064    protected static final String ENDPOINT_AUTH = "authorization";
065
066    protected static final String ENDPOINT_TOKEN = "token";
067
068    public static String USERNAME_KEY = "nuxeo_user";
069
070    public static String AUTHORIZATION_KEY = "authorization_key";
071
072    public static String CLIENTNAME_KEY = "client_name";
073
074    public static enum ERRORS {
075        invalid_request, invalid_grant, unauthorized_client, access_denied, unsupported_response_type, invalid_scope, server_error, temporarily_unavailable
076    }
077
078    @Override
079    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
080            ServletException {
081
082        if (!isValid(request)) {
083            chain.doFilter(request, response);
084            return;
085        }
086
087        boolean startedTx = false;
088        if (!TransactionHelper.isTransactionActive()) {
089            startedTx = TransactionHelper.startTransaction();
090        }
091        boolean done = false;
092        try {
093            process(request, response, chain);
094            done = true;
095        } finally {
096            if (startedTx) {
097                if (!done) {
098                    TransactionHelper.setTransactionRollbackOnly();
099                }
100                TransactionHelper.commitOrRollbackTransaction();
101            }
102        }
103    }
104
105    protected boolean isValid(ServletRequest request) {
106        if (!(request instanceof HttpServletRequest)) {
107            return false;
108        }
109
110        HttpServletRequest httpRequest = (HttpServletRequest) request;
111        return isAuthorizedRequest(httpRequest) || httpRequest.getRequestURI().contains(OAUTH2_SEGMENT);
112    }
113
114    protected void process(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
115            ServletException {
116        HttpServletRequest httpRequest = (HttpServletRequest) request;
117        HttpServletResponse httpResponse = (HttpServletResponse) response;
118
119        String uri = httpRequest.getRequestURI();
120        if (uri.contains(OAUTH2_SEGMENT)) {
121            String endpoint = uri.split(OAUTH2_SEGMENT)[1];
122
123            switch (endpoint) {
124            case ENDPOINT_AUTH:
125                processAuthorization(httpRequest, httpResponse, chain);
126                break;
127            case ENDPOINT_TOKEN:
128                processToken(httpRequest, httpResponse, chain);
129                break;
130            }
131        } else if (isAuthorizedRequest(httpRequest)) {
132            processAuthentication(httpRequest, httpResponse, chain);
133        }
134
135        if (!response.isCommitted()) {
136            chain.doFilter(request, response);
137        }
138    }
139
140    protected void processAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
141            throws IOException, ServletException {
142        String key = URLDecoder.decode(request.getHeader("Authorization").substring(7), "UTF-8").trim();
143        NuxeoOAuth2Token token = getTokenStore().getToken(key);
144
145        if (token == null) {
146            return;
147        }
148
149        if (token.isExpired() || !getClientRegistry().hasClient(token.getClientId())) {
150            response.setStatus(401);
151            return;
152        }
153
154        LoginContext loginContext = buildLoginContext(token);
155        if (loginContext != null) {
156            Principal principal = (Principal) loginContext.getSubject().getPrincipals().toArray()[0];
157            try {
158                chain.doFilter(new NuxeoSecuredRequestWrapper(request, principal), response);
159            } finally {
160                try {
161                    loginContext.logout();
162                } catch (LoginException e) {
163                    log.warn("Error when logging out", e);
164                }
165            }
166        }
167    }
168
169    protected LoginContext buildLoginContext(NuxeoOAuth2Token token) {
170        try {
171            return NuxeoAuthenticationFilter.loginAs(token.getNuxeoLogin());
172        } catch (LoginException e) {
173            log.warn("Error while authenticate user");
174        }
175        return null;
176    }
177
178    protected boolean isAuthorizedRequest(HttpServletRequest request) {
179        String authorization = request.getHeader("Authorization");
180        return authorization != null && authorization.startsWith("Bearer");
181    }
182
183    protected void processAuthorization(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
184            throws IOException {
185        AuthorizationRequest authRequest = AuthorizationRequest.from(request);
186        String error = authRequest.checkError();
187        if (isNotBlank(error)) {
188            handleError(error, request, response);
189            return;
190        }
191
192        // Redirect to grant form
193        if (request.getMethod().equals("GET")) {
194            request.getSession().setAttribute(AUTHORIZATION_KEY, authRequest.getAuthorizationKey());
195            request.getSession().setAttribute("state", authRequest.getState());
196            request.getSession().setAttribute(CLIENTNAME_KEY,
197                    getClientRegistry().getClient(authRequest.getClientId()).getName());
198            String base = VirtualHostHelper.getBaseURL(request);
199            sendRedirect(response, base + "oauth2Grant.jsp", null);
200            return;
201        }
202
203        // Ensure that authorization key is the correct one
204        String authKeyForm = request.getParameter(AUTHORIZATION_KEY);
205        if (!authRequest.getAuthorizationKey().equals(authKeyForm)) {
206            handleError(ERRORS.access_denied, request, response);
207            return;
208        }
209
210        // Save username in request object
211        authRequest.setUsername((String) request.getSession().getAttribute(USERNAME_KEY));
212
213        Map<String, String> params = new HashMap<>();
214        params.put("code", authRequest.getAuthorizationCode());
215        if (isNotBlank(authRequest.getState())) {
216            params.put("state", authRequest.getState());
217        }
218
219        request.getSession().invalidate();
220        sendRedirect(response, authRequest.getRedirectUri(), params);
221    }
222
223    ClientRegistry getClientRegistry() {
224        return Framework.getLocalService(ClientRegistry.class);
225    }
226
227    protected void processToken(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
228            throws IOException {
229        TokenRequest tokRequest = new TokenRequest(request);
230        // Process Authorization code
231        if ("authorization_code".equals(tokRequest.getGrantType())) {
232            AuthorizationRequest authRequest = AuthorizationRequest.fromCode(tokRequest.getCode());
233            ERRORS error = null;
234            if (authRequest == null) {
235                error = ERRORS.access_denied;
236            }
237            // Check that clientId is the good one, already verified in
238            // authorization request
239            else if (!authRequest.getClientId().equals(tokRequest.getClientId())) {
240                error = ERRORS.access_denied;
241            }
242            // Validate client secret
243            else if (!getClientRegistry().isValidClient(tokRequest.getClientId(), tokRequest.getClientSecret())) {
244                error = ERRORS.unauthorized_client;
245            }
246            // Ensure redirect uris are identical
247            else {
248                boolean sameRedirectUri = authRequest.getRedirectUri().equals(tokRequest.getRedirectUri());
249                if (!(isBlank(authRequest.getRedirectUri()) || sameRedirectUri)) {
250                    error = ERRORS.invalid_request;
251                }
252            }
253
254            if (error != null) {
255                handleError(error, request, response);
256                return;
257            }
258
259            // Store token
260            NuxeoOAuth2Token token = new NuxeoOAuth2Token(3600 * 1000, authRequest.getClientId());
261            getTokenStore().store(authRequest.getUsername(), token);
262
263            handleTokenResponse(token, response);
264        } else if ("refresh_token".equals(tokRequest.getGrantType())) {
265            ERRORS error = null;
266            if (isBlank(tokRequest.getClientId())) {
267                error = ERRORS.access_denied;
268            } else if (!getClientRegistry().isValidClient(tokRequest.getClientId(), tokRequest.getClientSecret())) {
269                error = ERRORS.access_denied;
270            }
271
272            if (error != null) {
273                handleError(error, request, response);
274                return;
275            }
276
277            NuxeoOAuth2Token refreshed = getTokenStore().refresh(tokRequest.getRefreshToken(), tokRequest.getClientId());
278            if (refreshed == null) {
279                handleJsonError(ERRORS.invalid_request, request, response);
280            } else {
281                handleTokenResponse(refreshed, response);
282            }
283        } else {
284            handleJsonError(ERRORS.invalid_grant, request, response);
285        }
286    }
287
288    protected void handleTokenResponse(NuxeoOAuth2Token token, HttpServletResponse response) throws IOException {
289        ObjectMapper mapper = new ObjectMapper();
290
291        response.setHeader("Content-Type", "application/json");
292        response.setStatus(200);
293        mapper.writeValue(response.getWriter(), token.toJsonObject());
294    }
295
296    protected void handleError(ERRORS error, HttpServletRequest request, HttpServletResponse response)
297            throws IOException {
298        handleError(error.toString(), request, response);
299    }
300
301    protected void handleError(String error, HttpServletRequest request, HttpServletResponse response)
302            throws IOException {
303        Map<String, String> params = new HashMap<>();
304        params.put("error", error);
305        String state = request.getParameter("state");
306        if (isNotBlank(state)) {
307            params.put("state", state);
308        }
309
310        String redirectUri = request.getParameter("redirect_uri");
311        sendRedirect(response, redirectUri, params);
312    }
313
314    protected void handleJsonError(ERRORS error, HttpServletRequest request, HttpServletResponse response)
315            throws IOException {
316        ObjectMapper mapper = new ObjectMapper();
317
318        response.setHeader("Content-Type", "application/json");
319        response.setStatus(400);
320
321        Map<String, String> object = new HashMap<>();
322        object.put("error", error.toString());
323        mapper.writeValue(response.getWriter(), object);
324    }
325
326    protected void sendRedirect(HttpServletResponse response, String uri, Map<String, String> params)
327            throws IOException {
328        if (uri == null) {
329            uri = "http://dummyurl";
330        }
331
332        StringBuilder sb = new StringBuilder(uri);
333        if (params != null) {
334            if (!uri.contains("?")) {
335                sb.append("?");
336            } else {
337                sb.append("&");
338            }
339
340            for (String key : params.keySet()) {
341                sb.append(key).append("=").append(params.get(key)).append("&");
342            }
343            sb.deleteCharAt(sb.length() - 1);
344        }
345        response.sendRedirect(sb.toString());
346    }
347
348    protected OAuth2TokenStore getTokenStore() {
349        return new OAuth2TokenStore(TOKEN_SERVICE);
350    }
351}