/******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. * * This product includes a number of subcomponents with * separate copyright notices and license terms. Your use of these * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.authentication; import java.io.IOException; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.login.AccountSavingAuthenticationSuccessHandler; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.util.Assert; import static java.util.Optional.ofNullable; /** * Filter which processes authentication submitted through the * <code>/authorize</code> endpoint. * * Checks the submitted information for a parameter named "credentials" (or * specified via the {@link #setParameterNames(List) parameter name}), in JSON * format. * <p> * If the parameter is found, it will submit an authentication request to the * AuthenticationManager and attempt to authenticate the user. If authentication * fails, it will return an error message. Otherwise, it creates a security * context and allows the request to continue. * <p> * If the parameter is not present, the filter will have no effect. * * See <a * href="https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.md">UUA * API Docs</a> */ public class AuthzAuthenticationFilter implements Filter { private final Log logger = LogFactory.getLog(getClass()); private AuthenticationManager authenticationManager; private List<String> parameterNames = Collections.emptyList(); private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint(); private Set<String> methods = Collections.singleton(HttpMethod.POST.toString()); private AccountSavingAuthenticationSuccessHandler successHandler; /** * The filter fails on requests that don't have one of these HTTP methods. * * @param methods the methods to set (defaults to POST) */ public void setMethods(Set<String> methods) { this.methods = new HashSet<>(); for (String method : methods) { this.methods.add(method.toUpperCase()); } } /** * @param authenticationEntryPoint the authenticationEntryPoint to set */ public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { this.authenticationEntryPoint = authenticationEntryPoint; } public void setSuccessHandler(AccountSavingAuthenticationSuccessHandler successHandler) { this.successHandler = successHandler; } /** * The name of the parameter to extract credentials from. Request parameters * with these names are extracted and * passed as credentials to the authentication manager. A request that * doesn't have any of the specified parameters * is ignored. * * @param parameterNames the parameter names to set (default empty) */ public void setParameterNames(List<String> parameterNames) { this.parameterNames = parameterNames; } public AuthzAuthenticationFilter(AuthenticationManager authenticationManager) { Assert.notNull(authenticationManager); this.authenticationManager = authenticationManager; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; Map<String, String> loginInfo = getCredentials(req); boolean buggyVmcAcceptHeader = false; try { if (loginInfo.isEmpty()) { throw new BadCredentialsException("Request does not contain credentials."); } else { logger.debug("Located credentials in request, with keys: " + loginInfo.keySet()); if (methods != null && !methods.contains(req.getMethod().toUpperCase())) { throw new BadCredentialsException("Credentials must be sent by (one of methods): " + methods); } Authentication result = authenticationManager.authenticate(new AuthzAuthenticationRequest(loginInfo, new UaaAuthenticationDetails(req))); SecurityContextHolder.getContext().setAuthentication(result); ofNullable(successHandler).ifPresent( s -> s.setSavedAccountOptionCookie(req, res, result) ); } } catch (AuthenticationException e) { logger.debug("Authentication failed"); String acceptHeaderValue = req.getHeader("accept"); String clientId = req.getParameter("client_id"); if ("*/*; q=0.5, application/xml".equals(acceptHeaderValue) && "vmc".equals(clientId)) { buggyVmcAcceptHeader = true; } if (buggyVmcAcceptHeader) { HttpServletRequest jsonAcceptingRequest = new HttpServletRequestWrapper(req) { @SuppressWarnings("unchecked") @Override public Enumeration<String> getHeaders(String name) { if ("accept".equalsIgnoreCase(name)) { return new JsonInjectedEnumeration(((HttpServletRequest) getRequest()).getHeaders(name)); } else { return ((HttpServletRequest) getRequest()).getHeaders(name); } } @Override public String getHeader(String name) { if (name.equalsIgnoreCase("accept")) { return "application/json"; } else { return ((HttpServletRequest) getRequest()).getHeader(name); } } }; authenticationEntryPoint.commence(jsonAcceptingRequest, res, e); } else { authenticationEntryPoint.commence(req, res, e); } return; } chain.doFilter(request, response); } private Map<String, String> getCredentials(HttpServletRequest request) { Map<String, String> credentials = new HashMap<String, String>(); for (String paramName : parameterNames) { String value = request.getParameter(paramName); if (value != null) { if (value.startsWith("{")) { try { Map<String, String> jsonCredentials = JsonUtils.readValue(value, new TypeReference<Map<String, String>>() { }); credentials.putAll(jsonCredentials); } catch (JsonUtils.JsonUtilException e) { logger.warn("Unknown format of value for request param: " + paramName + ". Ignoring."); } } else { credentials.put(paramName, value); } } } return credentials; } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } static class JsonInjectedEnumeration implements Enumeration<String> { private Enumeration<String> underlying; public JsonInjectedEnumeration(Enumeration<String> underlying) { this.underlying = underlying; } @Override public boolean hasMoreElements() { return underlying.hasMoreElements(); } @Override public String nextElement() { underlying.nextElement(); return "application/json"; } } }