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