This entire answer is backed by a working Spring Boot application with unit tests to confirm it.
If you find this answer helpful, please up vote it.
The short answer is that your security configuration could look like this
http
.sessionManagement()
.disable()
//application security
.authorizeRequests()
.anyRequest().hasAuthority("API_KEY")
.and()
.addFilterBefore(new ApiKeyFilter(), HeaderWriterFilter.class)
.addFilterAfter(new UserCredentialsFilter(), ApiKeyFilter.class)
.csrf().ignoringAntMatchers(
"/api-key-only",
"/dual-auth"
)
;
// @formatter:on
}
}
Let me tell you a little bit what is going on. I encourage you to review my sample, specifically the unit tests that cover many of your scenarios.
We have two levels of security
1. Every API must be secured by ApiKey
2. Only some APIs must be secured by UserCredentials
In my example project I opted for the following solution
I use a WebSecurityConfigurerAdapter to meet the ApiKey requirement
.authorizeRequests()
.anyRequest().hasAuthority("API_KEY")
I use method level security by enabling it
@EnableGlobalMethodSecurity(prePostEnabled = true)
and then requiring it in my controller
@PreAuthorize("hasAuthority('USER_CREDENTIALS')")
public String twoLayersOfAuth() {
//only logic here
}
The ApiKey filter is super simple
public class ApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorization = request.getHeader("Authorization");
final String prefix = "ApiKey ";
if (hasText(authorization) && authorization.startsWith(prefix)) {
String key = authorization.substring(prefix.length());
if ("this-is-a-valid-key".equals(key)) {
RestAuthentication<SimpleGrantedAuthority> authentication = new RestAuthentication<>(
key,
Collections.singletonList(new SimpleGrantedAuthority("API_KEY"))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
and the second tier of authentication even simple (and it relies on the first tier to have performed)
public class UserCredentialsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String userCredentials = request.getHeader("X-User-Credentials");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if ("valid-user".equals(userCredentials) && authentication instanceof RestAuthentication) {
RestAuthentication<SimpleGrantedAuthority> restAuthentication =
(RestAuthentication<SimpleGrantedAuthority>)authentication;
restAuthentication.addAuthority(new SimpleGrantedAuthority("USER_CREDENTIALS"));
}
filterChain.doFilter(request, response);
}
}
Please note: How each filter is not concerned about what happens when there is no authentication or insufficient authentication. That is all taken care of for you. Your filter only has to validate correct data;
Spring, Spring Boot and Spring Security have some stellar testing facilities.
I can invoke api-only endpoint with both level of security
mvc.perform(
post("/api-key-only")
.header("Authorization", "ApiKey this-is-a-valid-key")
.header("X-User-Credentials", "valid-user")
)
.andExpect(status().isOk())
.andExpect(authenticated()
.withAuthorities(
asList(
new SimpleGrantedAuthority("API_KEY"),
new SimpleGrantedAuthority("USER_CREDENTIALS")
)
)
)
.andExpect(content().string("API KEY ONLY"))
;
or I can pass the first level of security and be rejected by the 2nd
mvc.perform(
post("/dual-auth")
.header("Authorization", "ApiKey this-is-a-valid-key")
)
.andExpect(status().is4xxClientError())
.andExpect(authenticated()
.withAuthorities(
asList(
new SimpleGrantedAuthority("API_KEY")
)
)
)
;
of course, we always have a happy path
mvc.perform(
post("/dual-auth")
.header("Authorization", "ApiKey this-is-a-valid-key")
.header("X-User-Credentials", "valid-user")
)
.andExpect(status().isOk())
.andExpect(content().string("DUAL AUTH"))
.andExpect(authenticated()
.withAuthorities(
asList(
new SimpleGrantedAuthority("API_KEY"),
new SimpleGrantedAuthority("USER_CREDENTIALS")
)
)
)
;