001/* 002 * (C) Copyright 2017-2018 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_METHOD_NOT_ALLOWED; 024import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; 025import static javax.servlet.http.HttpServletResponse.SC_OK; 026import static org.nuxeo.ecm.platform.oauth2.Constants.AUTHORIZATION_CODE_GRANT_TYPE; 027import static org.nuxeo.ecm.platform.oauth2.Constants.AUTHORIZATION_CODE_PARAM; 028import static org.nuxeo.ecm.platform.oauth2.Constants.CLIENT_ID_PARAM; 029import static org.nuxeo.ecm.platform.oauth2.Constants.CODE_CHALLENGE_METHOD_PARAM; 030import static org.nuxeo.ecm.platform.oauth2.Constants.CODE_CHALLENGE_PARAM; 031import static org.nuxeo.ecm.platform.oauth2.Constants.CODE_VERIFIER_PARAM; 032import static org.nuxeo.ecm.platform.oauth2.Constants.GRANT_TYPE_PARAM; 033import static org.nuxeo.ecm.platform.oauth2.Constants.REDIRECT_URI_PARAM; 034import static org.nuxeo.ecm.platform.oauth2.Constants.REFRESH_TOKEN_GRANT_TYPE; 035import static org.nuxeo.ecm.platform.oauth2.Constants.RESPONSE_TYPE_PARAM; 036import static org.nuxeo.ecm.platform.oauth2.Constants.SCOPE_PARAM; 037import static org.nuxeo.ecm.platform.oauth2.Constants.STATE_PARAM; 038import static org.nuxeo.ecm.platform.oauth2.Constants.TOKEN_SERVICE; 039 040import java.io.IOException; 041import java.util.HashMap; 042import java.util.Map; 043 044import javax.servlet.RequestDispatcher; 045import javax.servlet.ServletException; 046import javax.servlet.http.HttpServlet; 047import javax.servlet.http.HttpServletRequest; 048import javax.servlet.http.HttpServletResponse; 049 050import org.apache.commons.lang3.StringUtils; 051import org.apache.commons.logging.Log; 052import org.apache.commons.logging.LogFactory; 053import org.nuxeo.common.utils.URIUtils; 054import org.nuxeo.ecm.platform.oauth2.clients.OAuth2Client; 055import org.nuxeo.ecm.platform.oauth2.clients.OAuth2ClientService; 056import org.nuxeo.ecm.platform.oauth2.request.AuthorizationRequest; 057import org.nuxeo.ecm.platform.oauth2.request.TokenRequest; 058import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token; 059import org.nuxeo.ecm.platform.oauth2.tokens.OAuth2TokenStore; 060import org.nuxeo.runtime.api.Framework; 061import org.nuxeo.runtime.transaction.TransactionHelper; 062 063import com.fasterxml.jackson.databind.ObjectMapper; 064 065/** 066 * @since 9.2 067 */ 068public class NuxeoOAuth2Servlet extends HttpServlet { 069 070 private static final long serialVersionUID = 1L; 071 072 private static final Log log = LogFactory.getLog(NuxeoOAuth2Servlet.class); 073 074 public static final String ENDPOINT_AUTH = "authorize"; 075 076 public static final String ENDPOINT_TOKEN = "token"; 077 078 public static final String ENDPOINT_AUTH_SUBMIT = "authorize_submit"; 079 080 public static final String ERROR_PARAM = "error"; 081 082 public static final String ERROR_DESCRIPTION_PARAM = "error_description"; 083 084 public static final String CLIENT_NAME = "client_name"; 085 086 public static final String GRANT_JSP_PAGE_PATH = "/oauth2Grant.jsp"; 087 088 public static final String GRANT_ACCESS_PARAM = "grant_access"; 089 090 public static final String ERROR_JSP_PAGE_PATH = "/oauth2error.jsp"; 091 092 public static final int ACCESS_TOKEN_EXPIRATION_TIME = 3600 * 1000; 093 094 protected final OAuth2TokenStore tokenStore = new OAuth2TokenStore(TOKEN_SERVICE); 095 096 @Override 097 protected void doGet(HttpServletRequest request, HttpServletResponse response) 098 throws ServletException, IOException { 099 String pathInfo = request.getPathInfo(); 100 if (pathInfo.endsWith(ENDPOINT_AUTH)) { 101 doGetAuthorize(request, response); 102 } else if (pathInfo.endsWith(ENDPOINT_AUTH_SUBMIT)) { 103 doGetNotAllowed(ENDPOINT_AUTH_SUBMIT, request, response); 104 } else if (pathInfo.endsWith(ENDPOINT_TOKEN)) { 105 doGetNotAllowed(ENDPOINT_TOKEN, request, response); 106 } else { 107 response.sendError(SC_NOT_FOUND); 108 } 109 } 110 111 @Override 112 protected void doPost(HttpServletRequest request, HttpServletResponse response) 113 throws ServletException, IOException { 114 String pathInfo = request.getPathInfo(); 115 if (pathInfo.endsWith(ENDPOINT_AUTH_SUBMIT)) { 116 doPostAuthorizeSubmit(request, response); 117 } else if (pathInfo.endsWith(ENDPOINT_TOKEN)) { 118 doPostToken(request, response); 119 } else { 120 response.sendError(SC_NOT_FOUND); 121 } 122 } 123 124 protected void doGetAuthorize(HttpServletRequest request, HttpServletResponse response) 125 throws IOException, ServletException { 126 OAuth2ClientService clientService = Framework.getService(OAuth2ClientService.class); 127 AuthorizationRequest authRequest = AuthorizationRequest.fromRequest(request); 128 OAuth2Error error = authRequest.checkError(); 129 if (error != null) { 130 handleError(error, request, response); 131 return; 132 } 133 134 // If auto-grant is checked on the client or a token exists for the (client, username) passed in the 135 // authorization request, bypass the grant page and redirect to the redirect_uri 136 // with an authorization code parameter 137 String clientId = authRequest.getClientId(); 138 OAuth2Client client = clientService.getClient(clientId); 139 if (client.isAutoGrant() || tokenStore.getToken(clientId, authRequest.getUsername()) != null) { 140 String redirectURI = getRedirectURI(authRequest); 141 String authorizationCode = storeAuthorizationRequest(authRequest); 142 String state = request.getParameter(STATE_PARAM); 143 Map<String, String> params = new HashMap<>(); 144 params.put(AUTHORIZATION_CODE_PARAM, authorizationCode); 145 if (StringUtils.isNotBlank(state)) { 146 params.put(STATE_PARAM, state); 147 } 148 sendRedirect(request, response, redirectURI, params); 149 return; 150 151 } 152 153 // Set the required request attributes and redirect to the grant page 154 request.setAttribute(RESPONSE_TYPE_PARAM, authRequest.getResponseType()); 155 request.setAttribute(CLIENT_ID_PARAM, clientId); 156 String redirectURI = authRequest.getRedirectURI(); 157 if (StringUtils.isNotBlank(redirectURI)) { 158 request.setAttribute(REDIRECT_URI_PARAM, redirectURI); 159 } 160 String scope = authRequest.getScope(); 161 if (StringUtils.isNotBlank(scope)) { 162 request.setAttribute(SCOPE_PARAM, scope); 163 } 164 String state = request.getParameter(STATE_PARAM); 165 if (StringUtils.isNotBlank(state)) { 166 request.setAttribute(STATE_PARAM, state); 167 } 168 String codeChallenge = authRequest.getCodeChallenge(); 169 String codeChallengeMethod = authRequest.getCodeChallengeMethod(); 170 if (codeChallenge != null && codeChallengeMethod != null) { 171 request.setAttribute(CODE_CHALLENGE_PARAM, codeChallenge); 172 request.setAttribute(CODE_CHALLENGE_METHOD_PARAM, codeChallengeMethod); 173 } 174 request.setAttribute(CLIENT_NAME, client.getName()); 175 176 RequestDispatcher requestDispatcher = request.getRequestDispatcher(GRANT_JSP_PAGE_PATH); 177 requestDispatcher.forward(request, response); 178 } 179 180 protected void doGetNotAllowed(String endpoint, HttpServletRequest request, HttpServletResponse response) 181 throws IOException, ServletException { 182 OAuth2Error error = OAuth2Error.invalidRequest( 183 String.format("The /oauth2/%s endpoint only accepts POST requests.", endpoint)); 184 handleError(error, SC_METHOD_NOT_ALLOWED, request, response); 185 } 186 187 protected void doPostAuthorizeSubmit(HttpServletRequest request, HttpServletResponse response) 188 throws IOException, ServletException { 189 AuthorizationRequest authRequest = AuthorizationRequest.fromRequest(request); 190 OAuth2Error error = authRequest.checkError(); 191 if (error != null) { 192 handleError(error, request, response); 193 return; 194 } 195 196 String redirectURI = getRedirectURI(authRequest); 197 String state = request.getParameter(STATE_PARAM); 198 String grantAccess = request.getParameter(GRANT_ACCESS_PARAM); 199 if (grantAccess == null) { 200 // the user deny access 201 error = OAuth2Error.accessDenied("Access denied by the user"); 202 Map<String, String> params = new HashMap<>(); 203 params.put(ERROR_PARAM, error.getId()); 204 String errorDescription = error.getDescription(); 205 if (StringUtils.isNotBlank(errorDescription)) { 206 params.put(ERROR_DESCRIPTION_PARAM, errorDescription); 207 } 208 if (StringUtils.isNotBlank(state)) { 209 params.put(STATE_PARAM, state); 210 } 211 sendRedirect(request, response, redirectURI, params); 212 return; 213 } 214 215 // now store the authorization request according to its code 216 // to be able to retrieve it in the "/oauth2/token" endpoint 217 String authorizationCode = storeAuthorizationRequest(authRequest); 218 Map<String, String> params = new HashMap<>(); 219 params.put(AUTHORIZATION_CODE_PARAM, authorizationCode); 220 if (StringUtils.isNotBlank(state)) { 221 params.put(STATE_PARAM, state); 222 } 223 224 sendRedirect(request, response, redirectURI, params); 225 } 226 227 /** 228 * Returns the redirect URI included in the given authorization request or fall back on the first one registered for 229 * the related client. 230 */ 231 protected String getRedirectURI(AuthorizationRequest authRequest) { 232 String redirectURI = authRequest.getRedirectURI(); 233 if (StringUtils.isBlank(redirectURI)) { 234 return Framework.getService(OAuth2ClientService.class) 235 .getClient(authRequest.getClientId()) 236 .getRedirectURIs() 237 .get(0); 238 } else { 239 return redirectURI; 240 } 241 } 242 243 protected String storeAuthorizationRequest(AuthorizationRequest authRequest) { 244 String authorizationCode = authRequest.getAuthorizationCode(); 245 AuthorizationRequest.store(authorizationCode, authRequest); 246 return authorizationCode; 247 } 248 249 protected void doPostToken(HttpServletRequest request, HttpServletResponse response) throws IOException { 250 TokenRequest tokenRequest = new TokenRequest(request); 251 OAuth2ClientService clientService = Framework.getService(OAuth2ClientService.class); 252 String grantType = tokenRequest.getGrantType(); 253 // Process Authorization code 254 if (AUTHORIZATION_CODE_GRANT_TYPE.equals(grantType)) { 255 String authorizationCode = tokenRequest.getCode(); 256 AuthorizationRequest authRequest = AuthorizationRequest.get(authorizationCode); 257 final String clientId = tokenRequest.getClientId(); 258 OAuth2Error error = null; 259 if (authRequest == null) { 260 error = OAuth2Error.invalidGrant("Invalid authorization code"); 261 } 262 // Check that clientId is the good one, already verified in authorization request 263 else { 264 if (!authRequest.getClientId().equals(clientId)) { 265 error = OAuth2Error.invalidClient(String.format("Invalid client id: %s", clientId)); 266 } else { 267 OAuth2Client client = clientService.getClient(clientId); 268 // Validate client secret 269 if (client == null 270 || !client.isValidWith(tokenRequest.getClientId(), tokenRequest.getClientSecret())) { 271 error = OAuth2Error.invalidClient("Disabled client or invalid client secret"); 272 } else { 273 // Ensure redirect URIs are identical if the redirect_uri parameter was included in the 274 // authorization request 275 String authRequestRedirectURI = authRequest.getRedirectURI(); 276 String tokenRequestRedirectURI = tokenRequest.getRedirectURI(); 277 if (StringUtils.isNotBlank(authRequestRedirectURI) 278 && !authRequestRedirectURI.equals(tokenRequestRedirectURI)) { 279 error = OAuth2Error.invalidGrant( 280 String.format("Invalid redirect URI: %s", tokenRequestRedirectURI)); 281 } else { 282 // Check PKCE 283 String codeChallenge = authRequest.getCodeChallenge(); 284 if (codeChallenge != null) { 285 String codeVerifier = tokenRequest.getCodeVerifier(); 286 if (codeVerifier == null) { 287 error = OAuth2Error.invalidRequest( 288 String.format("Missing %s parameter", CODE_VERIFIER_PARAM)); 289 } else if (!authRequest.isCodeVerifierValid(codeVerifier)) { 290 error = OAuth2Error.invalidGrant( 291 String.format("Invalid %s parameter", CODE_VERIFIER_PARAM)); 292 } 293 } 294 } 295 } 296 } 297 } 298 299 if (authRequest != null) { 300 AuthorizationRequest.remove(authorizationCode); 301 } 302 303 if (error != null) { 304 handleJsonError(error, response); 305 return; 306 } 307 308 // If no token exists for the client id and username passed in the token request store a new one, 309 // else retrieve the existing token, refreshing it if needed 310 String username = authRequest.getUsername(); 311 NuxeoOAuth2Token token = tokenStore.getToken(clientId, username); 312 if (token == null) { 313 final NuxeoOAuth2Token newToken = new NuxeoOAuth2Token(ACCESS_TOKEN_EXPIRATION_TIME, clientId); 314 TransactionHelper.runInTransaction(() -> tokenStore.store(username, newToken)); 315 token = newToken; 316 } else if (token.isExpired()) { 317 final String refreshToken = token.getRefreshToken(); 318 token = TransactionHelper.runInTransaction(() -> tokenStore.refresh(refreshToken, clientId)); 319 } 320 321 handleTokenResponse(token, response); 322 } else if (REFRESH_TOKEN_GRANT_TYPE.equals(grantType)) { 323 OAuth2Error error = null; 324 if (StringUtils.isBlank(tokenRequest.getClientId())) { 325 error = OAuth2Error.invalidRequest("Empty client id"); 326 } else if (!clientService.isValidClient(tokenRequest.getClientId(), tokenRequest.getClientSecret())) { 327 error = OAuth2Error.invalidClient("Disabled client or invalid client secret"); 328 } 329 330 if (error != null) { 331 handleJsonError(error, response); 332 return; 333 } 334 335 NuxeoOAuth2Token refreshed = TransactionHelper.runInTransaction( 336 () -> tokenStore.refresh(tokenRequest.getRefreshToken(), tokenRequest.getClientId())); 337 338 if (refreshed == null) { 339 handleJsonError(OAuth2Error.invalidGrant("Cannot refresh token"), response); 340 } else { 341 handleTokenResponse(refreshed, response); 342 } 343 } else { 344 handleJsonError(OAuth2Error.unsupportedGrantType( 345 String.format("Unknown %s: got \"%s\", expecting \"%s\" or \"%s\".", GRANT_TYPE_PARAM, grantType, 346 AUTHORIZATION_CODE_GRANT_TYPE, REFRESH_TOKEN_GRANT_TYPE)), 347 response); 348 } 349 } 350 351 protected void handleTokenResponse(NuxeoOAuth2Token token, HttpServletResponse response) throws IOException { 352 response.setHeader("Content-Type", "application/json"); 353 response.setStatus(SC_OK); 354 ObjectMapper mapper = new ObjectMapper(); 355 mapper.writeValue(response.getWriter(), token.toJsonObject()); 356 } 357 358 protected void handleError(OAuth2Error error, HttpServletRequest request, HttpServletResponse response) 359 throws IOException, ServletException { 360 handleError(error, SC_BAD_REQUEST, request, response); 361 } 362 363 protected void handleError(OAuth2Error error, int status, HttpServletRequest request, HttpServletResponse response) 364 throws IOException, ServletException { 365 log.warn(String.format("OAuth2 authorization request error: %s", error)); 366 response.reset(); 367 response.setStatus(status); 368 request.setAttribute("error", error); 369 RequestDispatcher requestDispatcher = request.getRequestDispatcher(ERROR_JSP_PAGE_PATH); 370 requestDispatcher.forward(request, response); 371 } 372 373 protected void handleJsonError(OAuth2Error error, HttpServletResponse response) throws IOException { 374 log.warn(String.format("OAuth2 token request error: %s", error)); 375 response.setHeader("Content-Type", "application/json"); 376 response.setStatus(SC_BAD_REQUEST); 377 378 Map<String, String> object = new HashMap<>(); 379 object.put(ERROR_PARAM, error.getId()); 380 if (StringUtils.isNotBlank(error.getDescription())) { 381 object.put(ERROR_DESCRIPTION_PARAM, error.getDescription()); 382 } 383 ObjectMapper mapper = new ObjectMapper(); 384 mapper.writeValue(response.getWriter(), object); 385 } 386 387 protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String redirectURI, 388 Map<String, String> params) throws IOException { 389 if (redirectURI == null) { 390 response.sendError(SC_BAD_REQUEST, "No redirect URI"); 391 return; 392 } 393 394 String url = URIUtils.addParametersToURIQuery(redirectURI, params); 395 response.sendRedirect(url); 396 } 397}