I have a simple Spring REST controller with a GetMapping
that needs to support two authentication types being:
- OAuth2 using JWT
- ApiKey
In order to support the ApiKey, I've added an additional AuthenticationProvider
and referenced that in the httpSecurity
configuration of Spring Security as shown here:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Configuration
@Order(1)
public static class ApiTokenSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/api/special/url").authenticated()
.and()
.addFilterBefore(
new ApiKeyAuthenticationFilter(authenticationManager()),
AnonymousAuthenticationFilter.class);
}
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(apiKeyAuthenticationProvider));
}
}
@Configuration
@Order(2)
public static class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private OAuth2ResourceServerProperties resourceServerProperties;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwkSetUri(this.resourceServerProperties.getJwt().getJwkSetUri());
}
}
}
This special URL in the first httpSecurity
config is the only endpoint that needs to support the apikey and the oauth2 authentication method. All others need to use only OAuth2.
When running this setup in a test, I noticed that both work when I comment out the other. So if I comment the apiKey static class in the listing above, the OAuth2 with JWT token support works. If I comment the OAuth2 static class the apiKey support works.
I've enabled debug logging for Spring Security and noticed that when the apiToke configuration is enabled, I don't see anything related to the OAuth2 authentication provider and vice versa. What am I doing wrong?
Update Thanks to @dur I changed the code to use only one httpSecurity:
@EnableWebSecurity(debug = true)
@Profile("securityOn")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Configuration
@Order(2)
public static class JwtSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(apiKeyAuthenticationProvider));
}
@Autowired
private OAuth2ResourceServerProperties resourceServerProperties;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/**").authenticated()
.and()
.addFilterBefore(
new ApiKeyAuthenticationFilter(authenticationManager()),
BearerTokenAuthenticationFilter.class)
.oauth2ResourceServer().jwt().jwkSetUri(this.resourceServerProperties.getJwt().getJwkSetUri());
}
}
}
I've added the ApiKeyAuthenticationFilter before the BearerTokenAuthenticationFilter. The ApiKeyAuthenticationFilter looks like this:
@Slf4j
public class ApiKeyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public ApiKeyAuthenticationFilter(AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher("/api/special/url/**", "GET"));
this.setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response) {
Optional<String> apiKeyOptional = Optional.ofNullable(request.getHeader("Authorization"));
ApiKeyAuthenticationToken token =
apiKeyOptional.map(ApiKeyAuthenticationToken::new).orElse(new ApiKeyAuthenticationToken());
return getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
The result of this setup is:
apiKey -> "/api/special/url" -> allowed -> success
apiKey -> any other url -> denied. -> success
jwtToken -> "/api/special/url" -> denied -> failure
jwtToken -> any other url. -> allowed -> success
So the request to "api/special/url" with a valid jwtToken fails at the authentication because the ApiKeyAuthentication fails due to the lack of an apiKey and no attempt is made to validate the jwtToken instead.
I looked at the UsernamePasswordAuthenticationFilter and think my setup is similar, but unfortunately it doesn't work out yet.