From d700c8adb430f846020276c921bdfc1342e1f61f Mon Sep 17 00:00:00 2001 From: pvannierop Date: Mon, 8 Jan 2024 14:23:14 +0100 Subject: [PATCH] Working version of OAuth2Token mechanism --- .../security/config/ApiSecurityConfig.java | 87 +++++--- .../config/DatAccessApiSecurityConfig.java | 58 ------ .../security/config/Saml2SecurityConfig.java | 19 +- .../token/TokenAuthenticationFilter.java | 10 +- .../OAuth2AccessTokenRefreshFilter.java | 192 ------------------ .../OAuth2TokenAuthenticationProvider.java | 2 +- 6 files changed, 82 insertions(+), 286 deletions(-) delete mode 100644 src/main/java/org/cbioportal/security/config/DatAccessApiSecurityConfig.java delete mode 100644 src/main/java/org/cbioportal/security/token/oauth2/OAuth2AccessTokenRefreshFilter.java diff --git a/src/main/java/org/cbioportal/security/config/ApiSecurityConfig.java b/src/main/java/org/cbioportal/security/config/ApiSecurityConfig.java index de63aebe803..17bd1764085 100644 --- a/src/main/java/org/cbioportal/security/config/ApiSecurityConfig.java +++ b/src/main/java/org/cbioportal/security/config/ApiSecurityConfig.java @@ -1,61 +1,98 @@ package org.cbioportal.security.config; import org.cbioportal.security.token.RestAuthenticationEntryPoint; -import org.cbioportal.security.token.oauth2.OAuth2AccessTokenRefreshFilter; +import org.cbioportal.security.token.TokenAuthenticationFilter; +import org.cbioportal.security.token.TokenAuthenticationSuccessHandler; +import org.cbioportal.service.DataAccessTokenService; import org.cbioportal.utils.config.annotation.ConditionalOnProperty; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.context.SecurityContextPersistenceFilter; @Configuration -@Order(SecurityProperties.BASIC_AUTH_ORDER - 2) -@ConditionalOnProperty(name = "authenticate", havingValue = {"saml", "oauth2"}) +@ConditionalOnProperty(name = "authenticate", havingValue = {"false", "noauthsessionservice"}, isNot = true) public class ApiSecurityConfig { // Add security filter chains that handle calls to the API endpoints. // Different chains are added for the '/api' and legacy '/webservice.do' paths. // Both are able to handle API tokens provided in the request. // see: "Creating and Customizing Filter Chains" @ https://spring.io/guides/topicals/spring-security-architecture - - // Only available when using OAuth2 authentication. - @Autowired(required = false) - private OAuth2AccessTokenRefreshFilter oAuth2AccessTokenRefreshFilter; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + @Autowired(required = false) + private DataAccessTokenService tokenService; + + @Autowired(required = false) + private AuthenticationProvider tokenAuthenticationProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // FIXME - csrf should be enabled .csrf(AbstractHttpConfigurer::disable) // This filter chain only grabs requests to the '/api' path. - .securityMatcher("/api/**") + .securityMatcher("/api/**", "/webservice.do") .authorizeHttpRequests(authorize -> authorize - .requestMatchers( - "/api/swagger-resources/**", - "/api/swagger-ui.html", - "/api/health", - "/api/cache/**").permitAll() - .anyRequest().authenticated() + .requestMatchers( + "/api/swagger-resources/**", + "/api/swagger-ui.html", + "/api/health", + "/api/cache/**").permitAll() + .anyRequest().authenticated() ) .sessionManagement(sessionManagement -> sessionManagement.sessionFixation().none()) .exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(restAuthenticationEntryPoint()) ); - if (oAuth2AccessTokenRefreshFilter != null) { - http - .addFilterAfter(oAuth2AccessTokenRefreshFilter, SecurityContextPersistenceFilter.class); + // When dat.method is not 'none' and a tokenService bean is present, + // the apiTokenAuthenticationFilter is added to the filter chain. + if (tokenService != null) { + http.apply(ApiTokenFilterDsl.tokenFilterDsl(tokenService)); + } + return http.build(); + } + + @Autowired + public AuthenticationManagerBuilder buildAuthenticationManager(AuthenticationManagerBuilder authenticationManagerBuilder) { + if (tokenAuthenticationProvider != null) { + authenticationManagerBuilder.authenticationProvider(tokenAuthenticationProvider); } - return http.build(); - } + return authenticationManagerBuilder; + } @Bean public RestAuthenticationEntryPoint restAuthenticationEntryPoint() { return new RestAuthenticationEntryPoint(); } - + +} + + +class ApiTokenFilterDsl extends AbstractHttpConfigurer { + + private final DataAccessTokenService tokenService; + + public ApiTokenFilterDsl(DataAccessTokenService tokenService) { + this.tokenService = tokenService; + } + + @Override + public void configure(HttpSecurity http) { + AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); + TokenAuthenticationSuccessHandler tokenAuthenticationSuccessHandler = new TokenAuthenticationSuccessHandler(); + TokenAuthenticationFilter filter = new TokenAuthenticationFilter("/**", authenticationManager, tokenService); + filter.setAuthenticationSuccessHandler(tokenAuthenticationSuccessHandler); + http.addFilterAfter(filter, SecurityContextPersistenceFilter.class); + } + + public static ApiTokenFilterDsl tokenFilterDsl(DataAccessTokenService tokenService) { + return new ApiTokenFilterDsl(tokenService); + } + } diff --git a/src/main/java/org/cbioportal/security/config/DatAccessApiSecurityConfig.java b/src/main/java/org/cbioportal/security/config/DatAccessApiSecurityConfig.java deleted file mode 100644 index 913011f6ec8..00000000000 --- a/src/main/java/org/cbioportal/security/config/DatAccessApiSecurityConfig.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.cbioportal.security.config; - -import org.cbioportal.security.token.TokenAuthenticationFilter; -import org.cbioportal.security.token.TokenAuthenticationSuccessHandler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.security.SecurityProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.context.SecurityContextPersistenceFilter; - -@Configuration -@Order(SecurityProperties.BASIC_AUTH_ORDER - 2) -@ConditionalOnExpression("'${authenticate}' ne 'false' && '${authenticate}' ne 'noauthsessionservice' && '${dat.method:none}' ne 'none'") -public class DatAccessApiSecurityConfig extends ApiSecurityConfig { - - @Autowired - private AuthenticationProvider tokenAuthenticationProvider; - - @Autowired - private TokenAuthenticationSuccessHandler tokenAuthenticationSuccessHandler; - - @Autowired - private AuthenticationManager authenticationManager; - - // Update the Spring Boot AuthenticationManager to contain a tokenAuthenticationProvider - // (see: "Customizing Authentication Managers" @ https://spring.io/guides/topicals/spring-security-architecture - @Autowired - public void initialize(AuthenticationManagerBuilder builder) { - if (tokenAuthenticationProvider != null) { - builder.authenticationProvider(tokenAuthenticationProvider); - } - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - super.filterChain(http); - http - .addFilterAfter(tokenAuthenticationFilter(), SecurityContextPersistenceFilter.class); - return http.build(); - } - - @Bean - public TokenAuthenticationFilter tokenAuthenticationFilter() throws Exception { - TokenAuthenticationFilter tokenAuthenticationFilter = - new TokenAuthenticationFilter("/**", authenticationManager()); - tokenAuthenticationFilter.setAuthenticationSuccessHandler( - tokenAuthenticationSuccessHandler); - return tokenAuthenticationFilter; - } - -} diff --git a/src/main/java/org/cbioportal/security/config/Saml2SecurityConfig.java b/src/main/java/org/cbioportal/security/config/Saml2SecurityConfig.java index 5607b702da8..45bf44138ef 100644 --- a/src/main/java/org/cbioportal/security/config/Saml2SecurityConfig.java +++ b/src/main/java/org/cbioportal/security/config/Saml2SecurityConfig.java @@ -8,8 +8,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -25,7 +24,6 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import java.util.Collection; @@ -33,6 +31,8 @@ import java.util.Objects; import java.util.Set; +import static org.springframework.security.config.Customizer.withDefaults; + @Configuration @EnableWebSecurity @ConditionalOnProperty(value = "authenticate", havingValue = "saml") @@ -43,11 +43,16 @@ public class Saml2SecurityConfig { @Autowired(required = false) private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + + @Autowired + public void configure(AuthenticationManagerBuilder builder) { + OpenSaml4AuthenticationProvider saml4AuthenticationProvider = new OpenSaml4AuthenticationProvider(); + saml4AuthenticationProvider.setResponseAuthenticationConverter(rolesConverter()); + builder.authenticationProvider(saml4AuthenticationProvider); + } @Bean public SecurityFilterChain samlFilterChain(HttpSecurity http) throws Exception { - OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); - authenticationProvider.setResponseAuthenticationConverter(rolesConverter()); DefaultSecurityFilterChain build = http // FIXME - csrf should be enabled .csrf(AbstractHttpConfigurer::disable) @@ -59,9 +64,7 @@ public SecurityFilterChain samlFilterChain(HttpSecurity http) throws Exception { new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), AntPathRequestMatcher.antMatcher("/api/**") ) ) - .saml2Login(saml2 -> saml2 - .authenticationManager(new ProviderManager(authenticationProvider)) - ) + .saml2Login(withDefaults()) // NOTE: I did not get the official .saml2Logout() DSL to work as // described at https://docs.spring.io/spring-security/reference/6.1/servlet/saml2/logout.html // Logout Service POST Binding URL: http://localhost:8080/logout/saml2/slo diff --git a/src/main/java/org/cbioportal/security/token/TokenAuthenticationFilter.java b/src/main/java/org/cbioportal/security/token/TokenAuthenticationFilter.java index 782171ab2cb..25edd0a31e6 100644 --- a/src/main/java/org/cbioportal/security/token/TokenAuthenticationFilter.java +++ b/src/main/java/org/cbioportal/security/token/TokenAuthenticationFilter.java @@ -48,6 +48,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.stereotype.Component; /** * @@ -55,7 +56,6 @@ */ public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { - @Autowired private DataAccessTokenService tokenService; private static final String BEARER = "Bearer"; @@ -70,7 +70,12 @@ public TokenAuthenticationFilter() { public TokenAuthenticationFilter(String s, AuthenticationManager authenticationManagerBean) { super(s, authenticationManagerBean); } - + + public TokenAuthenticationFilter(String s, AuthenticationManager authenticationManager, DataAccessTokenService tokenService) { + super(s, authenticationManager); + this.tokenService = tokenService; + } + @Override protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { // only required if we do see an authorization header @@ -118,4 +123,5 @@ protected String extractHeaderToken(HttpServletRequest request) { } return null; } + } diff --git a/src/main/java/org/cbioportal/security/token/oauth2/OAuth2AccessTokenRefreshFilter.java b/src/main/java/org/cbioportal/security/token/oauth2/OAuth2AccessTokenRefreshFilter.java deleted file mode 100644 index 14dde30e6b9..00000000000 --- a/src/main/java/org/cbioportal/security/token/oauth2/OAuth2AccessTokenRefreshFilter.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.cbioportal.security.token.oauth2; - -import static org.springframework.http.HttpHeaders.CONTENT_TYPE; -import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; - - -import java.io.IOException; -import java.net.URI; -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.FormHttpMessageConverter; -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.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2AuthorizationException; -import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.filter.GenericFilterBean; - -// TODO add tests - -/** - * Spring Security filter that checks the expiration of the OAuth2 access token. When expired, - * this filter will renew the access token using the refresh token. When token exchange is successful, - * the user permissions/authorities are retrieved from the user-info endpoint (using the accces token). - * Finally, the Authentication object for the respective user is updated with possible changes in permissions. - * This mechanism ensures that update of user permissions is controlled by the lifespan of the access - * token. - */ -public class OAuth2AccessTokenRefreshFilter extends GenericFilterBean { - - private static final Logger log = LoggerFactory.getLogger(OAuth2AccessTokenRefreshFilter.class); - private static final OAuth2UserService oAuth2UserService = - new DefaultOAuth2UserService(); - - @Value("#{T(java.time.Duration).ofMinutes(${spring.security.oauth2.allowed-clock-skew:1})}") - private Duration accessTokenExpiresSkew; - - @Value("${spring.security.oauth2.client.jwt-roles-path:resource_access::cbioportal::roles}") - private String jwtRolesPath; - - @Autowired - private OAuth2AuthorizedClientService authorizedClientService; - - @Override - public void doFilter(ServletRequest servletRequest, - ServletResponse servletResponse, - FilterChain filterChain) - throws IOException, ServletException, OAuth2AuthorizationException { - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.isAuthenticated() && - authentication instanceof OAuth2AuthenticationToken) { - - OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; - OAuth2AuthorizedClient currentClient = authorizedClientService.loadAuthorizedClient( - token.getAuthorizedClientRegistrationId(), - token.getName()); - - OAuth2AccessToken currentAccessToken = currentClient.getAccessToken(); - if (currentAccessToken != null && isExpired(currentAccessToken)) { - log.debug("OAuth2 access token has expired. Refreshing the token using the refresh token"); - OAuth2AccessTokenResponse accessTokenResponse = refreshAccessToken(currentClient); - if (accessTokenResponse != null && accessTokenResponse.getAccessToken() != null) { - - log.debug("OAuth2 access token was refreshed."); - - OAuth2AuthorizedClient updatedClient = - getUpdatedClient(currentClient, accessTokenResponse); - - // Get token with up-to-date user permissions. - OAuth2AuthenticationToken authenticationToken = - createAuthenticationToken(accessTokenResponse, updatedClient); - - // Register client with updated access token and user information. - updateSecurityContext(updatedClient, authenticationToken); - - } else { - log.error("Failed to refresh token for {}", token.getPrincipal().getName()); - } - } - } - // Always pass processing of the request to the next filter. - filterChain.doFilter(servletRequest, servletResponse); - } - - private OAuth2AuthorizedClient getUpdatedClient(OAuth2AuthorizedClient currentClient, - OAuth2AccessTokenResponse accessTokenResponse) { - OAuth2RefreshToken refreshToken = accessTokenResponse.getRefreshToken() != null ? - accessTokenResponse.getRefreshToken() : - currentClient.getRefreshToken(); - return new OAuth2AuthorizedClient( - currentClient.getClientRegistration(), - currentClient.getPrincipalName(), - accessTokenResponse.getAccessToken(), - refreshToken - ); - } - - private OAuth2AuthenticationToken createAuthenticationToken( - OAuth2AccessTokenResponse accessTokenResponse, OAuth2AuthorizedClient updatedClient) { - OAuth2User newPrincipal = getUserInfo(updatedClient.getClientRegistration(), - accessTokenResponse.getAccessToken()); - Collection newStudyPermissions = - new UserInfoAuthoritiesMapper(jwtRolesPath).mapAuthorities(newPrincipal.getAuthorities()); - return new OAuth2AuthenticationToken(newPrincipal, newStudyPermissions, - updatedClient.getClientRegistration().getRegistrationId()); - } - - private void updateSecurityContext(OAuth2AuthorizedClient updatedClient, - OAuth2AuthenticationToken authenticationToken) { - log.debug("Resetting user Authorization object."); - authorizedClientService.saveAuthorizedClient(updatedClient, authenticationToken); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); - } - - private OAuth2AccessTokenResponse refreshAccessToken(OAuth2AuthorizedClient client) - throws OAuth2AuthorizationException { - - LinkedMultiValueMap formParameters = - new LinkedMultiValueMap<>(); - formParameters.add(OAuth2ParameterNames.GRANT_TYPE, - AuthorizationGrantType.REFRESH_TOKEN.getValue()); - formParameters.add(OAuth2ParameterNames.REFRESH_TOKEN, - client.getRefreshToken().getTokenValue()); - formParameters.add(OAuth2ParameterNames.REDIRECT_URI, - client.getClientRegistration().getRedirectUri()); - - RequestEntity> requestEntity = RequestEntity - .post(URI.create(client.getClientRegistration().getProviderDetails().getTokenUri())) - .header(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_VALUE) - .body(formParameters); - - RestTemplate restTemplate = - tokenExchangeRestTemplate(client.getClientRegistration().getClientId(), - client.getClientRegistration().getClientSecret()); - ResponseEntity responseEntity = - restTemplate.exchange(requestEntity, OAuth2AccessTokenResponse.class); - return responseEntity.getBody(); - - } - - private boolean isExpired(OAuth2AccessToken accessToken) { - return accessToken.getExpiresAt().isBefore(Instant.now().minus(accessTokenExpiresSkew)); - } - - private RestTemplate tokenExchangeRestTemplate(String clientId, String clientSecret) { - return new RestTemplateBuilder() - .additionalMessageConverters( - new FormHttpMessageConverter(), - new OAuth2AccessTokenResponseHttpMessageConverter()) - .errorHandler(new OAuth2ErrorResponseErrorHandler()) - .basicAuthentication(clientId, clientSecret) - .build(); - } - - private OAuth2User getUserInfo(ClientRegistration clientRegistration, - OAuth2AccessToken accessToken) { - return oAuth2UserService.loadUser( - new OAuth2UserRequest(clientRegistration, accessToken) - ); - } - -} diff --git a/src/main/java/org/cbioportal/security/token/oauth2/OAuth2TokenAuthenticationProvider.java b/src/main/java/org/cbioportal/security/token/oauth2/OAuth2TokenAuthenticationProvider.java index ae8bd0b598f..9ed0b677b7a 100644 --- a/src/main/java/org/cbioportal/security/token/oauth2/OAuth2TokenAuthenticationProvider.java +++ b/src/main/java/org/cbioportal/security/token/oauth2/OAuth2TokenAuthenticationProvider.java @@ -62,7 +62,7 @@ public class OAuth2TokenAuthenticationProvider implements AuthenticationProvider public boolean supports(Class authentication) { return authentication.isAssignableFrom(OAuth2BearerAuthenticationToken.class); } - + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException {