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}