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}