/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.security.filter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.geoserver.security.config.CredentialsFromRequestHeaderFilterConfig; import org.geoserver.security.config.SecurityNamedServiceConfig; import org.geoserver.security.impl.GeoServerRole; import org.springframework.security.authentication.ProviderNotFoundException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; /** * Security filter to extract user credentials (username and password) * from Request Headers. * It is quite flexible through the capability to extract username and password * from the same or different request headers, using regular expressions to capture * them in a structured header content. * * @author Lorenzo Natali, GeoSolutions * @author Mauro Bartolomeoli, GeoSolutions * */ public class GeoServerCredentialsFromRequestHeaderFilter extends GeoServerSecurityFilter implements AuthenticationCachingFilter, GeoServerAuthenticationFilter { private String userNameHeaderName; private String passwordHeaderName; private Pattern userNameRegex; private Pattern passwordRegex; private boolean decodeURI = true; private MessageDigest digest; protected AuthenticationEntryPoint aep; @Override public void initializeFromConfig(SecurityNamedServiceConfig config) throws IOException { super.initializeFromConfig(config); aep = new Http403ForbiddenEntryPoint(); CredentialsFromRequestHeaderFilterConfig authConfig = (CredentialsFromRequestHeaderFilterConfig) config; userNameHeaderName = authConfig.getUserNameHeaderName(); passwordHeaderName = authConfig.getPasswordHeaderName(); userNameRegex = Pattern.compile(authConfig.getUserNameRegex()); passwordRegex = Pattern.compile(authConfig.getPasswordRegex()); decodeURI = authConfig.isParseAsUriComponents(); // digest used to create a cacheKey containing the user password try { digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("No MD5 algorithm available!"); } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cacheKey = authenticateFromCache(this, (HttpServletRequest) request); if (SecurityContextHolder.getContext().getAuthentication() == null) { doAuthenticate((HttpServletRequest) request, (HttpServletResponse) response); Authentication postAuthentication = SecurityContextHolder.getContext() .getAuthentication(); if (postAuthentication != null && cacheKey != null) { if (cacheAuthentication(postAuthentication, (HttpServletRequest) request)) { getSecurityManager().getAuthenticationCache().put(getName(), cacheKey, postAuthentication); } } } request.setAttribute(GeoServerSecurityFilter.AUTHENTICATION_ENTRY_POINT_HEADER, aep); chain.doFilter(request, response); } /** * Parse an header string to extract the credential. The regular expression must contain a group, that will represent the credential to be * extracted. * * @param header the String to parse * @param pattern the pattern to use. This must contain one group * */ private String parseHeader(String header, Pattern pattern) { Matcher m = pattern.matcher(header); if (m.find() && m.groupCount() == 1) { String res = m.group(1); return res; } else { return null; } } /** * Try to authenticate. * If credentials are found in the configured header(s), * then authentication is delegated to the AuthenticationProvider * chain. * * @param request * @param response */ protected void doAuthenticate(HttpServletRequest request, HttpServletResponse response) throws IOException { String usHeader = request.getHeader(userNameHeaderName); String pwHeader = request.getHeader(passwordHeaderName); if (usHeader == null || pwHeader == null) { return; } String us = parseHeader(usHeader, userNameRegex); String pw = parseHeader(pwHeader, passwordRegex); if (us == null || pw == null) { return; } if (decodeURI) { us = java.net.URLDecoder.decode(us, "UTF-8"); pw = java.net.URLDecoder.decode(pw, "UTF-8"); } UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(us, pw, new ArrayList<GrantedAuthority>()); Authentication auth = null; try { auth = getSecurityManager().authenticationManager().authenticate(result); } catch (ProviderNotFoundException e) { LOGGER.log(Level.WARNING, "couldn't to authenticate user:" + us); return; } LOGGER.log(Level.FINER, "logged in as {0}", us); Collection<GeoServerRole> roles = new ArrayList<GeoServerRole>(); for (GrantedAuthority grauth : auth.getAuthorities()) { roles.add((GeoServerRole) grauth); } if (!roles.contains(GeoServerRole.AUTHENTICATED_ROLE)) { roles.add(GeoServerRole.AUTHENTICATED_ROLE); } response.addHeader("X-GeoServer-Auth-User", us); UsernamePasswordAuthenticationToken newResult = new UsernamePasswordAuthenticationToken( auth.getPrincipal(), auth.getCredentials(), roles); newResult.setDetails(auth.getDetails()); // Set the authentication with the roles injected SecurityContextHolder.getContext().setAuthentication(newResult); } @Override public boolean applicableForHtml() { return true; } @Override public boolean applicableForServices() { return true; } /** * The cache key is the concatenation of the headers' values (global identifier) */ @Override public String getCacheKey(HttpServletRequest req) { String usHeader = req.getHeader(userNameHeaderName); String pwHeader = req.getHeader(passwordHeaderName); if (usHeader == null || pwHeader == null) { return null; } String username = parseHeader(usHeader, userNameRegex); String password = parseHeader(pwHeader, passwordRegex); if (username == null && password == null) { return null; } if (decodeURI) { try { username = java.net.URLDecoder.decode(username, "UTF-8"); } catch (UnsupportedEncodingException e) { LOGGER.log(Level.WARNING, "unsupported decode user name"); } } StringBuffer buff = new StringBuffer(password); buff.append(":"); buff.append(getName()); String digestString = null; try { MessageDigest md = (MessageDigest) digest.clone(); digestString = new String(Hex.encode(md.digest(buff.toString().getBytes("utf-8")))); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } buff = new StringBuffer(username); buff.append(":"); buff.append(digestString); return buff.toString(); } protected boolean cacheAuthentication(Authentication auth, HttpServletRequest request) { // only cache if no HTTP session is available if (request.getSession(false) != null) return false; return true; } }