1

I am having a Spring Boot 2.0 application that serves as back-end (provides rest services) for my angular app.

We are having LDAPauthentication for login. It works perfectly fine. We have implemented custom code for it using spring-oauth-2 and spring-ldap.

Now, we have integrated actuator. I want my actuator endpoints to be accessible in browser via basic authentication.

For that I have added an implementation of WebSecurityConfigurerAdapter with @Order(1). It works well from browser. However, when I call login url from my angualar app, it gives me 401 unauthorized error for /oauth/token url and hence I can't login from ui app.

Any help will be appreciated to solve this error.

Class:

    @Configuration
    @Order(1)
    public class FormLoginWebSecurityConfigurerAdapter extends 
          WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("actuator") 
     .password(passwordEncoder().encode("actuator")).roles("ACTUATOR_ADMIN");
  }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
    http.antMatcher("/actuator/**").authorizeRequests().anyRequest().hasRole("ACTUATOR_ADMIN").and()
        .httpBasic();

  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

}

Thanks.

mayur tanna
  • 241
  • 1
  • 3
  • 14

1 Answers1

0

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.

Welsh
  • 5,138
  • 3
  • 29
  • 43