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}