Skip to content
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.cbioportal.application.security.config;

import java.util.ArrayList;
import java.util.List;
import org.cbioportal.application.security.token.RestAuthenticationEntryPoint;
import org.cbioportal.application.security.token.TokenAuthenticationFilter;
import org.cbioportal.application.security.token.TokenAuthenticationSuccessHandler;
import org.cbioportal.legacy.service.DataAccessTokenService;
import org.cbioportal.legacy.utils.config.annotation.ConditionalOnProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
Expand All @@ -20,7 +23,11 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

@Configuration
@ConditionalOnProperty(
Expand All @@ -29,11 +36,16 @@
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
@Value("${api.access.token.required:false}")
private boolean accessTokenRequired;

static final String[] PUBLIC_API_Matchers = {
"/api/swagger-resources/**",
"/api/swagger-ui.html",
"/api/health",
"/api/public_virtual_studies/**",
"/api/cache/**"
};

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
Expand All @@ -45,12 +57,7 @@ public SecurityFilterChain securityFilterChain(
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers(
"/api/swagger-resources/**",
"/api/swagger-ui.html",
"/api/health",
"/api/public_virtual_studies/**",
"/api/cache/**")
.requestMatchers(PUBLIC_API_Matchers)
.permitAll()
.anyRequest()
.authenticated())
Expand All @@ -64,7 +71,7 @@ public SecurityFilterChain securityFilterChain(
// 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));
http.apply(ApiTokenFilterDsl.tokenFilterDsl(tokenService, accessTokenRequired));
}
return http.build();
}
Expand All @@ -88,10 +95,12 @@ public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {

class ApiTokenFilterDsl extends AbstractHttpConfigurer<ApiTokenFilterDsl, HttpSecurity> {

private final boolean accessTokenRequired;
private final DataAccessTokenService tokenService;

public ApiTokenFilterDsl(DataAccessTokenService tokenService) {
private ApiTokenFilterDsl(DataAccessTokenService tokenService, boolean accessTokenRequired) {
this.tokenService = tokenService;
this.accessTokenRequired = accessTokenRequired;
}

@Override
Expand All @@ -100,12 +109,30 @@ public void configure(HttpSecurity http) {
TokenAuthenticationSuccessHandler tokenAuthenticationSuccessHandler =
new TokenAuthenticationSuccessHandler();
TokenAuthenticationFilter filter =
new TokenAuthenticationFilter("/**", authenticationManager, tokenService);
new TokenAuthenticationFilter(
"/**", authenticationManager, tokenService, accessTokenRequired);

// Explicitly set the request matcher to exclude public paths if enforcement is enabled
if (accessTokenRequired) {
// Filter applies to /api/** BUT NOT the public paths
List<RequestMatcher> matchers = new ArrayList<>();
matchers.add(new AntPathRequestMatcher("/api/**"));

List<RequestMatcher> publicMatchers = new ArrayList<>();
for (String pattern : ApiSecurityConfig.PUBLIC_API_Matchers) {
publicMatchers.add(new AntPathRequestMatcher(pattern));
}
matchers.add(new NegatedRequestMatcher(new OrRequestMatcher(publicMatchers)));

filter.setRequiresAuthenticationRequestMatcher(new AndRequestMatcher(matchers));
}

filter.setAuthenticationSuccessHandler(tokenAuthenticationSuccessHandler);
http.addFilterAfter(filter, SecurityContextHolderFilter.class);
}

public static ApiTokenFilterDsl tokenFilterDsl(DataAccessTokenService tokenService) {
return new ApiTokenFilterDsl(tokenService);
public static ApiTokenFilterDsl tokenFilterDsl(
DataAccessTokenService tokenService, boolean accessTokenRequired) {
return new ApiTokenFilterDsl(tokenService, accessTokenRequired);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.cbioportal.legacy.service.DataAccessTokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
Expand All @@ -53,6 +54,7 @@
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

private DataAccessTokenService tokenService;
private boolean accessTokenRequired = false;

private static final String BEARER = "Bearer";

Expand All @@ -73,12 +75,26 @@ public TokenAuthenticationFilter(
this.tokenService = tokenService;
}

public TokenAuthenticationFilter(
String s,
AuthenticationManager authenticationManager,
DataAccessTokenService tokenService,
boolean accessTokenRequired) {
super(s, authenticationManager);
this.tokenService = tokenService;
this.accessTokenRequired = accessTokenRequired;
}

@Override
protected boolean requiresAuthentication(
HttpServletRequest request, HttpServletResponse response) {
// only required if we do see an authorization header
// only required if we do see an authorization header OR if configuration requires it
String param = request.getHeader(AUTHORIZATION);
if (param == null) {
if (accessTokenRequired) {
LOG.debug("attemptAuthentication(), authorization header is null, but token is required.");
return true;
}
LOG.debug(
"attemptAuthentication(), authorization header is null, continue on to other security filters");
return false;
Expand All @@ -94,7 +110,8 @@ public Authentication attemptAuthentication(
String token = extractHeaderToken(request);

if (token == null) {
LOG.error("No token was found in request header.");
// DEBUG level to avoid noise for unauthorized requests
LOG.debug("No token was found in request header.");
throw new BadCredentialsException("No token was found in request header.");
}

Expand All @@ -111,7 +128,22 @@ protected void successfulAuthentication(
Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);

boolean mdcSet = false;
if (authResult != null && authResult.getName() != null) {
MDC.put("user", authResult.getName());
MDC.put("auth_method", "token");
mdcSet = true;
}

try {
chain.doFilter(request, response);
} finally {
if (mdcSet) {
MDC.remove("user");
MDC.remove("auth_method");
}
}
}

/**
Expand Down