Search code examples
spring-securityoauth-2.0spark-java

My SparkJava resource server gets 403 errors when trying to validate access tokens


I want to set up a very basic REST API using Spark-java, which just checks an access token obtained from my own authorisation server. It creates a GET request to the authorisation server's /oauth/authorize endpoint followed by ?token=$ACCESS_TOKEN.

Whenever I try this, I get diverted to the /error endpoint and a 403 error.

Here's my API class:

import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.utils.StringUtils;

import java.io.IOException;

import static spark.Spark.*;

public class SampleAPI {
  private static final Logger logger = LoggerFactory.getLogger("SampleAPI");

  public static void main(String[] args) {
    // Run on port 9782
    port(9782);
    // Just returns "Hello there" to the client's console
    before((preRequest, preResponse) -> {

      System.out.println("Getting token from request");
      final String authHeader = preRequest.headers("Authorization");
      //todo validate token, don't just accept it because it's not null/empty
      if(StringUtils.isEmpty(authHeader) || !isAuthenticated(authHeader)){
        halt(401, "Access not authorised");
      } else {
        System.out.println("Token = " + authHeader);
      }
    });
    get("/", (res, req) -> "Hello there");
  }

  private static boolean isAuthenticated(final String authHeader) {
    String url = "http://localhost:9780/oauth/authorize";
    //"Bearer " is before the actual token in authHeader, so we need to extract the token itself as a substring
    String token = authHeader.substring(7);
    HttpGet getAuthRequest = new HttpGet(url + "?token=" + token);
    getAuthRequest.setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
    CloseableHttpClient httpClient = HttpClients.createMinimal();
    try {
      CloseableHttpResponse response = httpClient.execute(getAuthRequest);
      int statusCode = response.getStatusLine().getStatusCode();
      System.out.println("Status code " + statusCode + " returned for access token " + authHeader);
      return statusCode == 200;
    } catch (IOException ioException) {
      System.out.println("Exception when trying to validate access token " + ioException);
    }
    return false;
  }
}

The System.out.println statements are just for debugging.

Here's my authorisation server's WebSecurityConfigurerAdapter class:

    package main.config;
    
    import main.service.ClientAppDetailsService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.factory.PasswordEncoderFactories;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    @EnableWebSecurity(debug = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
      @Autowired
      private UserDetailsService userDetailsService;
    
      @Override
      @Bean
      public AuthenticationManager authenticationManagerBean() throws Exception {
        //returns AuthenticationManager from the superclass for authenticating users
        return super.authenticationManagerBean();
      }
    
      @Bean
      public PasswordEncoder getPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
      }
    
      @Override
      public void configure(WebSecurity web) throws Exception {
        //Allow for DB access without any credentials
        web.ignoring().antMatchers("/h2-console/**");
      }
    
      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //configures user details, and uses the custom UserDetailsService to check user credentials
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
      }
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
    
        //disable CORS and CSRF protection for Postman testing
        http.cors().disable().anonymous().disable();
        http.headers().frameOptions().disable();
        http.csrf().disable();
      }
    }

And here's my authorisation server's application.properties:

server.port=9780

#in-memory database, will get populated using data.sql
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=admin
spring.datasource.password=syst3m
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.jpa.properties.hibernate.format_sql=true
#adds to existing DB instead of tearing it down and re-populating it every time the app is started
spring.jpa.hibernate.ddl-auto=update

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false

What have I done wrong? Do I need to specify my API as a resource server using Spring Security? Do I need to add it to the authorisation server's application.properties?


Solution

  • If you want to use Spring as a security framework then the most common option is to configure it as a resource server. Here is a getting started tutorial. The API will then never get redirected.

    With Spark another option is to just provide a basic filter that uses a JWT validation library, such as jose4j. This tends to provide better control over error responses and gives you better visibility over what is going on. See this Kotlin example, which will be easy enough to translate to Java.