From 9fc49f57f648054c325ea54b24337c726bcbfec1 Mon Sep 17 00:00:00 2001 From: Thomas Roger Date: Tue, 24 Jul 2018 10:51:42 +0200 Subject: [PATCH] NXP-25342: make OAuth2 authorization work in cluster --- .../oauth2/request/AuthorizationRequest.java | 162 +++++++++++++----- .../ui/web/auth/oauth2/NuxeoOAuth2Filter.java | 33 ++-- .../resources/web/nuxeo.war/oauth2Grant.jsp | 8 +- .../oauth/tests/TestAuthorizationRequest.java | 44 ----- .../oauth/tests/TestOauth2Challenge.java | 26 ++- 5 files changed, 166 insertions(+), 107 deletions(-) delete mode 100644 nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestAuthorizationRequest.java diff --git a/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/oauth2/request/AuthorizationRequest.java b/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/oauth2/request/AuthorizationRequest.java index 03f76d3b842..343605db9ce 100644 --- a/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/oauth2/request/AuthorizationRequest.java +++ b/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/oauth2/request/AuthorizationRequest.java @@ -24,9 +24,10 @@ import static org.nuxeo.ecm.platform.ui.web.auth.oauth2.NuxeoOAuth2Filter.ERRORS import static org.nuxeo.ecm.platform.ui.web.auth.oauth2.NuxeoOAuth2Filter.ERRORS.unauthorized_client; import static org.nuxeo.ecm.platform.ui.web.auth.oauth2.NuxeoOAuth2Filter.ERRORS.unsupported_response_type; -import java.io.UnsupportedEncodingException; +import java.io.Serializable; +import java.security.Principal; import java.util.Date; -import java.util.Iterator; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -35,6 +36,8 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.nuxeo.ecm.core.transientstore.api.TransientStore; +import org.nuxeo.ecm.core.transientstore.api.TransientStoreService; import org.nuxeo.ecm.directory.DirectoryException; import org.nuxeo.ecm.platform.oauth2.clients.ClientRegistry; import org.nuxeo.runtime.api.Framework; @@ -44,16 +47,30 @@ import org.nuxeo.runtime.api.Framework; * @since 5.9.2 */ public class AuthorizationRequest extends Oauth2Request { + private static final Log log = LogFactory.getLog(AuthorizationRequest.class); + /** + * @deprecated since 8.10-HF34 + */ + @Deprecated protected static Map requests = new ConcurrentHashMap<>(); + /** + * @since 8.10-HF34 + */ + public static final String STORE_NAME = "authorizationRequestStore"; + protected String responseType; protected String scope; protected String state; + /** + * @deprecated since 8.10-HF34 + */ + @Deprecated protected String sessionId; protected Date creationDate; @@ -70,6 +87,46 @@ public class AuthorizationRequest extends Oauth2Request { public static final String STATE = "state"; + public static AuthorizationRequest from(HttpServletRequest request) { + return new AuthorizationRequest(request); + } + + /** + * @since 8.10-HF34 + */ + public static AuthorizationRequest fromMap(Map map) { + return new AuthorizationRequest(map); + } + + /** + * @since 8.10-HF34 + */ + public static void store(String key, AuthorizationRequest authorizationRequest) { + TransientStoreService transientStoreService = Framework.getService(TransientStoreService.class); + TransientStore store = transientStoreService.getStore(STORE_NAME); + store.putParameters(key, authorizationRequest.toMap()); + } + + public static AuthorizationRequest fromCode(String key) { + TransientStoreService transientStoreService = Framework.getService(TransientStoreService.class); + TransientStore store = transientStoreService.getStore(STORE_NAME); + Map parameters = store.getParameters(key); + if (parameters != null) { + AuthorizationRequest authorizationRequest = AuthorizationRequest.fromMap(parameters); + return authorizationRequest.isExpired() ? null : authorizationRequest; + } + return null; + } + + /** + * @since 8.10-HF34 + */ + public static void remove(String key) { + TransientStoreService transientStoreService = Framework.getService(TransientStoreService.class); + TransientStore store = transientStoreService.getStore(STORE_NAME); + store.remove(key); + } + public AuthorizationRequest() { } @@ -79,12 +136,31 @@ public class AuthorizationRequest extends Oauth2Request { scope = request.getParameter(SCOPE); state = request.getParameter(STATE); - sessionId = request.getSession(true).getId(); + + Principal principal = request.getUserPrincipal(); + if (principal != null) { + username = principal.getName(); + } creationDate = new Date(); authorizationKey = RandomStringUtils.random(6, true, false); } + /** + * @since 8.10-HF34 + */ + protected AuthorizationRequest(Map map) { + clientId = (String) map.get("clientId"); + redirectUri = (String) map.get("redirectUri"); + responseType = (String) map.get("responseType"); + scope = (String) map.get("scope"); + state = (String) map.get("state"); + creationDate = (Date) map.get("creationDate"); + authorizationCode = (String) map.get("authorizationCode"); + authorizationKey = (String) map.get("authorizationKey"); + username = (String) map.get("username"); + } + public String checkError() { // Check mandatory fields if (isBlank(responseType) || isBlank(clientId) || isBlank(redirectUri)) { @@ -109,6 +185,14 @@ public class AuthorizationRequest extends Oauth2Request { return null; } + public String getResponseType() { + return responseType; + } + + public String getScope() { + return scope; + } + public boolean isExpired() { // RFC 4.1.2, Authorization code lifetime is 10 return new Date().getTime() - creationDate.getTime() > 10 * 60 * 1000; @@ -122,14 +206,6 @@ public class AuthorizationRequest extends Oauth2Request { return username; } - public String getResponseType() { - return responseType; - } - - public String getScope() { - return scope; - } - public String getState() { return state; } @@ -145,45 +221,43 @@ public class AuthorizationRequest extends Oauth2Request { return authorizationKey; } - private static void deleteExpiredRequests() { - Iterator iterator = requests.values().iterator(); - AuthorizationRequest req; - while (iterator.hasNext() && (req = iterator.next()) != null) { - if (req.isExpired()) { - requests.remove(req.sessionId); - } + /** + * @since 8.10-HF34 + */ + public Map toMap() { + Map map = new HashMap<>(); + if (clientId != null) { + map.put("clientId", clientId); } - } - - public static AuthorizationRequest from(HttpServletRequest request) throws UnsupportedEncodingException { - deleteExpiredRequests(); - - String sessionId = request.getSession(true).getId(); - if (requests.containsKey(sessionId)) { - AuthorizationRequest authRequest = requests.get(sessionId); - if (!authRequest.isExpired() && authRequest.isValidState(request)) { - return authRequest; - } + if (redirectUri != null) { + map.put("redirectUri", redirectUri); } - - AuthorizationRequest authRequest = new AuthorizationRequest(request); - requests.put(sessionId, authRequest); - return authRequest; - } - - public static AuthorizationRequest fromCode(String authorizationCode) { - for (AuthorizationRequest auth : requests.values()) { - if (auth.authorizationCode != null && auth.authorizationCode.equals(authorizationCode)) { - if (auth.sessionId != null) { - requests.remove(auth.sessionId); - } - return auth.isExpired() ? null : auth; - } + if (responseType != null) { + map.put("responseType", responseType); } - return null; + if (scope != null) { + map.put("scope", scope); + } + if (state != null) { + map.put("state", state); + } + if (creationDate != null) { + map.put("creationDate", creationDate); + } + if (authorizationCode != null) { + map.put("authorizationCode", authorizationCode); + } + if (authorizationKey != null) { + map.put("authorizationKey", authorizationKey); + } + if (username != null) { + map.put("username", username); + } + return map; } public void setUsername(String username) { this.username = username; } + } diff --git a/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/ui/web/auth/oauth2/NuxeoOAuth2Filter.java b/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/ui/web/auth/oauth2/NuxeoOAuth2Filter.java index bb8466dc48a..62a17a7cc71 100644 --- a/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/ui/web/auth/oauth2/NuxeoOAuth2Filter.java +++ b/nuxeo-features/nuxeo-platform-oauth/src/main/java/org/nuxeo/ecm/platform/ui/web/auth/oauth2/NuxeoOAuth2Filter.java @@ -180,6 +180,7 @@ public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter { protected void processAuthorization(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException { + AuthorizationRequest authRequest = AuthorizationRequest.from(request); String error = authRequest.checkError(); if (isNotBlank(error)) { @@ -189,7 +190,9 @@ public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter { // Redirect to grant form if (request.getMethod().equals("GET")) { - request.getSession().setAttribute(AUTHORIZATION_KEY, authRequest.getAuthorizationKey()); + request.getSession().setAttribute("response_type", "code"); + request.getSession().setAttribute("client_id", authRequest.getClientId()); + request.getSession().setAttribute("redirect_uri", authRequest.getRedirectUri()); request.getSession().setAttribute("state", authRequest.getState()); request.getSession().setAttribute(CLIENTNAME_KEY, getClientRegistry().getClient(authRequest.getClientId()).getName()); @@ -198,18 +201,11 @@ public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter { return; } - // Ensure that authorization key is the correct one - String authKeyForm = request.getParameter(AUTHORIZATION_KEY); - if (!authRequest.getAuthorizationKey().equals(authKeyForm)) { - handleError(ERRORS.access_denied, request, response); - return; - } - - // Save username in request object - authRequest.setUsername((String) request.getSession().getAttribute(USERNAME_KEY)); - + // now store the authorization request according to its code + // to be able to retrieve it in the "/oauth2/token" endpoint + String authorizationCode = storeAuthorizationRequest(authRequest); Map params = new HashMap<>(); - params.put("code", authRequest.getAuthorizationCode()); + params.put("code", authorizationCode); if (isNotBlank(authRequest.getState())) { params.put("state", authRequest.getState()); } @@ -218,6 +214,12 @@ public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter { sendRedirect(response, authRequest.getRedirectUri(), params); } + protected String storeAuthorizationRequest(AuthorizationRequest authRequest) { + String authorizationCode = authRequest.getAuthorizationCode(); + AuthorizationRequest.store(authorizationCode, authRequest); + return authorizationCode; + } + ClientRegistry getClientRegistry() { return Framework.getLocalService(ClientRegistry.class); } @@ -227,7 +229,8 @@ public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter { TokenRequest tokRequest = new TokenRequest(request); // Process Authorization code if ("authorization_code".equals(tokRequest.getGrantType())) { - AuthorizationRequest authRequest = AuthorizationRequest.fromCode(tokRequest.getCode()); + String authorizationCode = tokRequest.getCode(); + AuthorizationRequest authRequest = AuthorizationRequest.fromCode(authorizationCode); ERRORS error = null; if (authRequest == null) { error = ERRORS.access_denied; @@ -249,6 +252,10 @@ public class NuxeoOAuth2Filter implements NuxeoAuthPreFilter { } } + if (authRequest != null) { + AuthorizationRequest.remove(authorizationCode); + } + if (error != null) { handleError(error, request, response); return; diff --git a/nuxeo-features/nuxeo-platform-oauth/src/main/resources/web/nuxeo.war/oauth2Grant.jsp b/nuxeo-features/nuxeo-platform-oauth/src/main/resources/web/nuxeo.war/oauth2Grant.jsp index 0a9372713a6..5d1b88f534a 100644 --- a/nuxeo-features/nuxeo-platform-oauth/src/main/resources/web/nuxeo.war/oauth2Grant.jsp +++ b/nuxeo-features/nuxeo-platform-oauth/src/main/resources/web/nuxeo.war/oauth2Grant.jsp @@ -111,8 +111,12 @@
- "/> + "/> + "/> + "/> "/> diff --git a/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestAuthorizationRequest.java b/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestAuthorizationRequest.java deleted file mode 100644 index 851b12eb7f8..00000000000 --- a/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestAuthorizationRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Contributors: - * Arnaud Kervern - */ -package org.nuxeo.ecm.platform.oauth.tests; - -import java.util.Date; -import java.util.Map; - -import org.nuxeo.ecm.platform.oauth2.request.AuthorizationRequest; - -/** - * @author Arnaud Kervern - * @since 5.9.2 - */ -public class TestAuthorizationRequest extends AuthorizationRequest { - - public static Map getRequests() { - return AuthorizationRequest.requests; - } - - public TestAuthorizationRequest(String clientId, String responseType, String state, String redirectUri, - Date creationDate) { - this.clientId = clientId; - this.responseType = responseType; - this.state = state; - this.creationDate = creationDate; - this.redirectUri = redirectUri; - } -} diff --git a/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestOauth2Challenge.java b/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestOauth2Challenge.java index acb53c6723b..cd9a6d02a1a 100644 --- a/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestOauth2Challenge.java +++ b/nuxeo-features/nuxeo-platform-oauth/src/test/java/org/nuxeo/ecm/platform/oauth/tests/TestOauth2Challenge.java @@ -18,10 +18,14 @@ */ package org.nuxeo.ecm.platform.oauth.tests; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; import static org.nuxeo.ecm.platform.ui.web.auth.oauth2.NuxeoOAuth2Filter.ERRORS; import java.io.IOException; +import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -33,6 +37,8 @@ import org.codehaus.jackson.map.ObjectMapper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.nuxeo.ecm.core.transientstore.api.TransientStore; +import org.nuxeo.ecm.core.transientstore.api.TransientStoreService; import org.nuxeo.ecm.platform.oauth2.clients.ClientRegistry; import org.nuxeo.ecm.platform.oauth2.clients.OAuth2Client; import org.nuxeo.ecm.platform.oauth2.request.AuthorizationRequest; @@ -67,8 +73,13 @@ public class TestOauth2Challenge { @Inject protected ClientRegistry clientRegistry; + @Inject + protected TransientStoreService transientStoreService; + protected Client client; + protected TransientStore store; + @Before public void initOAuthClient() { @@ -89,7 +100,8 @@ public class TestOauth2Challenge { client.setReadTimeout(TIMEOUT); client.setFollowRedirects(Boolean.FALSE); - TestAuthorizationRequest.getRequests().clear(); + store = transientStoreService.getStore(AuthorizationRequest.STORE_NAME); + store.removeAll(); } @Test @@ -123,8 +135,14 @@ public class TestOauth2Challenge { @Test public void tokenShouldCreateAndRefreshWithDummyAuthorization() throws IOException { - AuthorizationRequest request = new TestAuthorizationRequest(CLIENT_ID, "code", null, "Dummy", new Date()); - TestAuthorizationRequest.getRequests().put("fake", request); + Map map = new HashMap<>(); + map.put("clientId", CLIENT_ID); + map.put("responseType", "code"); + map.put("state", null); + map.put("redirectUri", "Dummy"); + map.put("creationDate", new Date()); + AuthorizationRequest request = AuthorizationRequest.fromMap(map); + store.putParameters(request.getAuthorizationCode(), request.toMap()); // Request a token Map params = new HashMap<>(); -- 2.19.0