So I ran into this myself (OAuth + Basic Auth) and ended up taking a different path that was a bit more involved for the Basic Auth piece but in the end it worked.
First you need to implement your own Authentication
object:
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
import java.util.Collections;
public class ActuatorAuthentication implements Authentication {
private static final Object DUMMY = new Object();
private static final String NAME = "Actuator Authenticated";
private static final boolean AUTHENTICATED = true;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public Object getCredentials() {
return DUMMY;
}
@Override
public Object getDetails() {
return DUMMY;
}
@Override
public Object getPrincipal() {
return DUMMY;
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean b) throws IllegalArgumentException {
throw new UnsupportedOperationException("Operation setAuthenticated not supported.");
}
@Override
public String getName() {
return NAME;
}
}
We are going to use this to work with the AutoConfiguredHealthEndpointGroup.isAuthorized()
method into toggling the behaviour that we want when we have the following configured:
management:
endpoint:
health:
show-details: when_authorized
So using that we are going to implement our own OncePerRequestFilter
which will perform the actual Basic Auth for all the actuator endpoints except for /health
, /health/liveness
and /health/readiness
.
We are also going to ensure that just /health
will return the basic UP
or DOWN
if Basic Auth was not provided.
The code for this ends up being:
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ActuatorSecurityFilter extends OncePerRequestFilter {
private static final String[] EXCLUDED_ENDPOINTS = {
"/actuator/health/liveness",
"/actuator/health/readiness",
"/actuator/info"
};
private AntPathMatcher pathMatcher = new AntPathMatcher();
private final String username;
private final String password;
public ActuatorSecurityFilter(String username, String password) {
this.username = username;
this.password = password;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
boolean authorized = false;
String authorization = request.getHeader("Authorization");
if (authorization != null) {
// Authorization: Basic base64credentials
String base64Credentials = authorization.substring("Basic".length()).trim();
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
// credentials = username:password
final String[] values = credentials.split(":", 2);
if (values.length == 2 && username.equals(values[0]) && password.equals(values[1])) {
authorized = true;
}
}
// Handle specific non-auth vs. auth for actuator health
if (pathMatcher.match("/actuator/health", request.getRequestURI())) {
if (authorized) {
// Utilize Dummy Authentication to show full details
Authentication authentication = new ActuatorAuthentication();
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
// Ensure no authentication is provided to show basic details if auth is incorrect
SecurityContextHolder.getContext().setAuthentication(null);
}
} else if (!authorized) {
response.sendError(401);
return;
}
chain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
for (String endpoint : EXCLUDED_ENDPOINTS) {
if (pathMatcher.match(endpoint, request.getRequestURI())) {
return true;
}
}
return false;
}
}
Finally we want to wire it all together with our own Configuration class:
import com.cantire.sps.application.filter.ActuatorSecurityFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ActuatorSecurityConfiguration {
@Value("${actuator.username}")
private String username;
@Value("${actuator.password}")
private String password;
@Bean
FilterRegistrationBean<ActuatorSecurityFilter> securityFilterRegistration() {
FilterRegistrationBean<ActuatorSecurityFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ActuatorSecurityFilter(username, password));
registration.addUrlPatterns("/actuator/*");
return registration;
}
}
And then we just need to add this into your application properties:
actuator:
username: monitor
password: password
And Voila you have Basic Auth working for your Actuator endpoints regardless of what WebSecurityConfigurerAdapter you have implemented.