4

I'm using Spring Boot 1.5.13 and with that Spring Security 4.2.6 and Spring Security oAuth2 2.0.15.

I want to find a best practice setup for our Spring Boot applications that serve a mixed set of content: A REST API, and some web pages that provide a convenience "landing page" for developers with some links on it, plus Swagger based API documentation, which is also web content.

I have a configuration that allows me to run the app with proper authorization code flow, hence I can access all web content via Browser and get authenticated by the configured IdP (in my case PingFederate), plus I can make API calls from within the Browser, i.e. directly or with a REST Client, e.g. with RESTClient.

This is my security configuration:

@Slf4j
@Configuration
@EnableWebSecurity
@EnableOAuth2Sso // this annotation must stay here!
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login**", "/webjars/**", "/css/**").permitAll()
                .antMatchers("/cfhealth").permitAll()
                .antMatchers("/").permitAll()
                .antMatchers("/protected", "/api/**").authenticated();
    }

    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }

}

and the oAuth2 configuration:

@Configuration
@Slf4j
public class OAuth2Config extends ResourceServerConfigurerAdapter {

    @Value("${pingfederate.pk-uri}")
    String pingFederatePublicKeyUri;

    @Autowired
    PingFederateKeyUtils pingFederateKeyUtils;

    @Override
    public void configure(ResourceServerSecurityConfigurer config) {
        config.tokenServices(tokenServices());
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        String certificate = pingFederateKeyUtils.getKeyFromServer(pingFederatePublicKeyUri);
        String publicKey = pingFederateKeyUtils.extractPublicKey(certificate);
        converter.setVerifier(pingFederateKeyUtils.createSignatureVerifier(publicKey));
        return converter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        return defaultTokenServices;
    }

}

But when I want to call a REST API programmatically/outside the Browser with a bearer token in the header, e.g. with curl, the authorization code flow kicks in and redirects to the local login endpoint. What I want is that API calls accept the bearer token for authentication, without creating a session, and that all web content/mvc calls in the Browser establish a session.

curl -i -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" -X GET http://localhost:8080/authdemo/api/hello

Adding the @EnableResourceServer annotation to the above SecurityConfig class (and adding security.oauth2.resource.filter-order=3 in the application properties file, I can make the curl command work, but then the authorization code flow is broken, I get the following output in the Browser for all URLs in my application:

<oauth> <error_description> Full authentication is required to access this resource </error_description> <error>unauthorized</error> </oauth>

Now is there a way to get this szenario working nicely? If yes, how would that look like? Or is it only supported in later versions of Spring Boot+Security+oAuth2?

The question at Spring Boot with Security OAuth2 - how to use resource server with web login form? is quite similar

Bernd
  • 370
  • 4
  • 16

1 Answers1

2

I found the solution: It takes multiple HttpSecurity configurations. I found out by reading the great article written by Matt Raible at https://developer.okta.com/blog/2018/02/13/secure-spring-microservices-with-oauth where he introduced me to the notion of requestMatchers(.). This is how I finally implemented it:

 @Configuration
 @EnableResourceServer
 @EnableWebSecurity(debug = true)
 @EnableOAuth2Sso
 public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

     @Bean
     public RequestContextListener requestContextListener() {
         return new RequestContextListener();
     }

     @Override
     public void configure(HttpSecurity http) throws Exception {
         http
             .requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
             .authorizeRequests().anyRequest().fullyAuthenticated();
     }

 }

With that I can access the service with a Browser, leading to a authorization code flow. But accessing the API (or actually any part of the service) leads to a validation of the provided Bearer token.

And to illustrate the way how some endpoints can be exluded/made public in such a case, here's how I configure the actuator endpoints and one very simple 'ping' endpoint I've added myself:

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatcher(new OrRequestMatcher(EndpointRequest.to("health", "info"),
                 new AntPathRequestMatcher("/cfhealth"))).authorizeRequests().anyRequest().permitAll();
    }

 }

And my implementation of the /cfhealth endpoint:

 @Controller
 @Slf4j
 public class MainController {

     @GetMapping(value = "/cfhealth")
     @ResponseBody
     public String cfhealth() {
         return "ok";
     }

 }

I'm happy to learn from others if that's the best practice way of Spring Security configuration or if there are better ways to do it. I've spent quite some time on the topic in the last few weeks on it, and it takes quite some effort to grasp the basic Spring Security concepts.

Bernd
  • 370
  • 4
  • 16
  • I'm also struggling with making a same app work as resource server and as client with SSO. Why is your `ResourceServerConfig` class anotated with `EnableOAuth2Sso`? Don't you have a separate config for Oauth2 client part? – Matt Oct 08 '18 at 13:30