/* (c) 2016 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.oauth2;
import static com.google.common.collect.Lists.newArrayList;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.geoserver.security.GeoServerUserGroupService;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.config.SecurityNamedServiceConfig;
import org.geoserver.security.filter.AuthenticationCachingFilter;
import org.geoserver.security.filter.GeoServerAuthenticationFilter;
import org.geoserver.security.filter.GeoServerLogoutFilter;
import org.geoserver.security.filter.GeoServerPreAuthenticatedUserNameFilter;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.impl.GeoServerUser;
import org.springframework.security.authentication.BadCredentialsException;
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.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.web.client.ResourceAccessException;
/**
* OAuth2 Authentication filter receiving/validating proxy tickets and service tickets.
*
* @author Alessio Fabiani, GeoSolutions
*
*/
public abstract class GeoServerOAuthAuthenticationFilter
extends GeoServerPreAuthenticatedUserNameFilter
implements GeoServerAuthenticationFilter, LogoutHandler {
static final String GEONODE_COOKIE_NAME = "sessionid";
OAuth2FilterConfig filterConfig;
OAuth2RestOperations restTemplate;
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
"/");
ResourceServerTokenServices tokenServices;
GeoServerOAuth2SecurityConfiguration oauth2SecurityConfiguration;
public GeoServerOAuthAuthenticationFilter(SecurityNamedServiceConfig config,
RemoteTokenServices tokenServices,
GeoServerOAuth2SecurityConfiguration oauth2SecurityConfiguration,
OAuth2RestOperations oauth2RestTemplate) {
this.filterConfig = (OAuth2FilterConfig) config;
this.tokenServices = tokenServices;
this.oauth2SecurityConfiguration = oauth2SecurityConfiguration;
this.restTemplate = oauth2RestTemplate;
}
@Override
public void initializeFromConfig(SecurityNamedServiceConfig config) throws IOException {
super.initializeFromConfig(config);
aep = filterConfig.getAuthenticationEntryPoint();
}
@Override
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return this.aep;
}
/**
* Try to authenticate if there is no authenticated principal
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String cacheKey = authenticateFromCache(this, (HttpServletRequest) request,
(HttpServletResponse) response);
// Search for an access_token on the request (simulating SSO)
String accessToken = request.getParameter("access_token");
OAuth2AccessToken token = restTemplate.getOAuth2ClientContext().getAccessToken();
if (accessToken != null && token != null && !token.getValue().equals(accessToken)) {
restTemplate.getOAuth2ClientContext().setAccessToken(null);
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
/*
* This cookie works only locally, when accessing the GeoServer GUI and on the same domain.
* For remote access you need to logout from the GeoServer GUI.
*/
final String gnCookie = getGeoNodeCookieValue(httpRequest);
final Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
final Collection<? extends GrantedAuthority> authorities = (authentication != null
? authentication.getAuthorities() : null);
if (accessToken == null && gnCookie == null &&
(authentication != null && (authentication instanceof PreAuthenticatedAuthenticationToken) &&
!(authorities.size() == 1 && authorities.contains(GeoServerRole.ANONYMOUS_ROLE)))) {
final AccessTokenRequest accessTokenRequest = restTemplate.getOAuth2ClientContext()
.getAccessTokenRequest();
if (accessTokenRequest != null && accessTokenRequest.getStateKey() != null) {
restTemplate.getOAuth2ClientContext()
.removePreservedState(accessTokenRequest.getStateKey());
}
try {
accessTokenRequest.remove("access_token");
} finally {
SecurityContextHolder.clearContext();
httpRequest.getSession(false).invalidate();
try {
httpRequest.logout();
} catch (ServletException e) {
LOGGER.fine(e.getLocalizedMessage());
}
LOGGER.fine("Cleaned out Session Access Token Request!");
}
}
if (accessToken != null || authentication == null || (authentication != null
&& authorities.size() == 1 && authorities.contains(GeoServerRole.ANONYMOUS_ROLE))) {
doAuthenticate((HttpServletRequest) request, (HttpServletResponse) response);
Authentication postAuthentication = authentication;
if (postAuthentication != null && cacheKey != null) {
if (cacheAuthentication(postAuthentication, (HttpServletRequest) request)) {
getSecurityManager().getAuthenticationCache().put(getName(), cacheKey,
postAuthentication);
}
}
}
chain.doFilter(request, response);
}
protected String authenticateFromCache(AuthenticationCachingFilter filter,
HttpServletRequest request, HttpServletResponse response) {
Authentication authFromCache = null;
String cacheKey = null;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
cacheKey = getCacheKey(request, response);
if (cacheKey != null) {
authFromCache = getSecurityManager().getAuthenticationCache().get(getName(),
cacheKey);
if (authFromCache != null)
SecurityContextHolder.getContext().setAuthentication(authFromCache);
else
return cacheKey;
}
}
return null;
}
protected String getCacheKey(HttpServletRequest request, HttpServletResponse response) {
if (request.getSession(false) != null) // no caching if there is an HTTP session
return null;
String retval;
try {
retval = getPreAuthenticatedPrincipal(request, response);
} catch (Exception e) {
return null;
}
if (GeoServerUser.ROOT_USERNAME.equals(retval))
return null;
return retval;
}
private String getGeoNodeCookieValue(HttpServletRequest request) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Inspecting the http request looking for the GeoNode Session ID.");
}
Cookie[] cookies = request.getCookies();
if (cookies != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Found " + cookies.length + " cookies!");
}
for (Cookie c : cookies) {
if (GEONODE_COOKIE_NAME.equals(c.getName())) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Found GeoNode cookie: " + c.getValue());
}
return c.getValue();
}
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Found no cookies!");
}
}
return null;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
OAuth2AccessToken token = restTemplate.getOAuth2ClientContext().getAccessToken();
if ((token != null && token.getTokenType().equalsIgnoreCase(OAuth2AccessToken.BEARER_TYPE))
|| request.getRequestURI().endsWith(filterConfig.getLogoutEndpoint())) {
final AccessTokenRequest accessTokenRequest = restTemplate.getOAuth2ClientContext()
.getAccessTokenRequest();
if (accessTokenRequest != null && accessTokenRequest.getStateKey() != null) {
restTemplate.getOAuth2ClientContext()
.removePreservedState(accessTokenRequest.getStateKey());
}
try {
accessTokenRequest.remove("access_token");
} finally {
SecurityContextHolder.clearContext();
request.getSession(false).invalidate();
try {
request.logout();
} catch (ServletException e) {
LOGGER.fine(e.getLocalizedMessage());
}
LOGGER.fine("Cleaned out Session Access Token Request!");
}
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
Cookie[] allCookies = request.getCookies();
for (int i = 0; i < allCookies.length; i++) {
String name = allCookies[i].getName();
if (name.equalsIgnoreCase("JSESSIONID")) {
Cookie cookieToDelete = allCookies[i];
cookieToDelete.setMaxAge(-1);
cookieToDelete.setPath("/");
cookieToDelete.setComment("EXPIRING COOKIE at " + System.currentTimeMillis());
response.addCookie(cookieToDelete);
}
}
request.setAttribute(GeoServerLogoutFilter.LOGOUT_REDIRECT_ATTR,
filterConfig.getLogoutUri());
}
}
@Override
protected void doAuthenticate(HttpServletRequest request, HttpServletResponse response) {
String principal = null;
try {
principal = getPreAuthenticatedPrincipal(request, response);
} catch (IOException e1) {
LOGGER.log(Level.FINE, e1.getMessage(), e1);
principal = null;
} catch (ServletException e1) {
LOGGER.log(Level.FINE, e1.getMessage(), e1);
principal = null;
}
LOGGER.log(Level.FINE,
"preAuthenticatedPrincipal = " + principal + ", trying to authenticate");
PreAuthenticatedAuthenticationToken result = null;
if (principal == null || principal.trim().length() == 0) {
result = new PreAuthenticatedAuthenticationToken(principal, null,
Collections.singleton(GeoServerRole.ANONYMOUS_ROLE));
} else {
if (GeoServerUser.ROOT_USERNAME.equals(principal)) {
result = new PreAuthenticatedAuthenticationToken(principal, null,
Collections.singleton(GeoServerRole.ADMIN_ROLE));
} else {
Collection<GeoServerRole> roles = null;
try {
roles = getRoles(request, principal);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (roles.contains(GeoServerRole.AUTHENTICATED_ROLE) == false)
roles.add(GeoServerRole.AUTHENTICATED_ROLE);
result = new PreAuthenticatedAuthenticationToken(principal, null, roles);
}
}
result.setDetails(getAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(result);
}
@Override
protected String getPreAuthenticatedPrincipal(HttpServletRequest request) {
try {
return getPreAuthenticatedPrincipal(request, null);
} catch (IOException e) {
return null;
} catch (ServletException e) {
return null;
}
}
protected String getPreAuthenticatedPrincipal(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
// Make sure the REST Resource Template has been correctly configured
configureRestTemplate();
// Avoid retrieving the user name more than once
/*
* if (req.getAttribute(UserNameAlreadyRetrieved) != null) return (String) req.getAttribute(UserName);
*/
// Search for an access_token on the request (simulating SSO)
String accessToken = req.getParameter("access_token");
if (accessToken != null) {
restTemplate.getOAuth2ClientContext()
.setAccessToken(new DefaultOAuth2AccessToken(accessToken));
}
// Setting up OAuth2 Filter services and resource template
filter.setRestTemplate(restTemplate);
filter.setTokenServices(tokenServices);
// Validating the access_token
Authentication authentication = null;
try {
authentication = filter.attemptAuthentication(req, null);
} catch (Exception e) {
if (e instanceof UserRedirectRequiredException) {
if (filterConfig.getEnableRedirectAuthenticationEntryPoint()
|| req.getRequestURI().endsWith(filterConfig.getLoginEndpoint())) {
// Intercepting a "UserRedirectRequiredException" and redirect to the OAuth2 Provider login URI
this.aep.commence(req, resp, null);
} else {
if (resp.getStatus() != 302) {
// AEP redirection failed
final AccessTokenRequest accessTokenRequest = restTemplate
.getOAuth2ClientContext().getAccessTokenRequest();
if (accessTokenRequest.getPreservedState() != null
&& accessTokenRequest.getStateKey() != null) {
// restTemplate.getOAuth2ClientContext().removePreservedState(accessTokenRequest.getStateKey());
accessTokenRequest.remove("state");
accessTokenRequest.remove(accessTokenRequest.getStateKey());
accessTokenRequest.setPreservedState(null);
}
}
}
} else if (e instanceof BadCredentialsException || e instanceof ResourceAccessException) {
if (e.getCause() instanceof OAuth2AccessDeniedException) {
LOGGER.log(Level.WARNING, "Error while trying to authenticate to OAuth2 Provider with the following Exception cause:", e.getCause());
}
if (e instanceof ResourceAccessException) {
LOGGER.log(Level.SEVERE, "Could not Authorize OAuth2 Resource due to the following exception:", e);
}
if (e instanceof ResourceAccessException || e.getCause() instanceof OAuth2AccessDeniedException) {
LOGGER.log(Level.WARNING, "It is worth notice that if you try to validate credentials against an SSH protected Endpoint, you need either your server exposed on a secure SSL channel or OAuth2 Provider Certificate to be trusted on your JVM!");
LOGGER.info("Please refer to the GeoServer OAuth2 Plugin Documentation in order to find the steps for importing the SSH certificates.");
}
}
}
String principal = (authentication != null ? (String) authentication.getPrincipal() : null);
if (principal != null && principal.trim().length() == 0)
principal = null;
try {
if (principal != null && PreAuthenticatedUserNameRoleSource.UserGroupService
.equals(getRoleSource())) {
GeoServerUserGroupService service = getSecurityManager()
.loadUserGroupService(getUserGroupServiceName());
GeoServerUser u = service.getUserByUsername(principal);
if (u != null && u.isEnabled() == false) {
principal = null;
handleDisabledUser(u, req);
}
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
req.setAttribute(UserNameAlreadyRetrieved, Boolean.TRUE);
if (principal != null)
req.setAttribute(UserName, principal);
return principal;
}
protected void configureRestTemplate() {
AuthorizationCodeResourceDetails details = (AuthorizationCodeResourceDetails) restTemplate
.getResource();
details.setClientId(filterConfig.getCliendId());
details.setClientSecret(filterConfig.getClientSecret());
((GeoServerOAuthRemoteTokenServices) this.tokenServices)
.setClientId(filterConfig.getCliendId());
((GeoServerOAuthRemoteTokenServices) this.tokenServices)
.setClientSecret(filterConfig.getClientSecret());
details.setAccessTokenUri(filterConfig.getAccessTokenUri());
details.setUserAuthorizationUri(filterConfig.getUserAuthorizationUri());
details.setPreEstablishedRedirectUri(filterConfig.getRedirectUri());
((GeoServerOAuthRemoteTokenServices) this.tokenServices)
.setCheckTokenEndpointUrl(filterConfig.getCheckTokenEndpointUrl());
details.setScope(parseScopes(filterConfig.getScopes()));
}
protected List<String> parseScopes(String commaSeparatedScopes) {
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
}
@Override
protected String getPreAuthenticatedPrincipalName(HttpServletRequest request) {
// Nothing to do; everything is handled by {@link getPreAuthenticatedPrincipal}
return null;
}
}