How to implement Spring Boot Microservice with two levels of token authentication using Spring Security?

Viewed 917

Hello Team,

I am working on a Spring Boot project in which I want to setup two levels of token authentication using Spring Security + JWT.

A similar application has already been built by some of my colleagues using Dropwizard framework.

I want to implement the same architecture in my Spring Boot project. I have added link to the architecture of the API at the end of this question.

I am able to setup the first level of token authentication with Spring Boot (using Spring Security + JWT), but I am unable to find the correct way of setting up second level of token authentication.

I tried searching out for related articles but couldn't find any.

It would be helpful if you could share a code snippet implementing both levels of token authentication in Spring Boot (using Spring Security) for better understanding.

Thanking you in anticipation!

Refer Web API Architecture here.

1 Answers

The following is a proof of concept to achieve what you need. The first class is the security configuration:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

   public static final List<String> WHITE_LIST = asList("/authenticate");
   public static final List<String> TOKEN1_LIST = asList("/get-access-token");
   public static final List<String> TOKEN2_LIST = asList("/add-new-user");

   @Autowired
   private Token1Filter token1Filter;

   @Autowired
   private Token2Filter token2Filter;


   @Override
   protected void configure(HttpSecurity http) throws Exception {
     http.csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
            // Make sure we use stateless session; session won't be used to store user's state
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            // Handle an authorized attempts
            .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and()
            .authorizeRequests()
            // List of services do not require authentication
            .antMatchers(OPTIONS).permitAll()
            .antMatchers(GET, WHITE_LIST.toArray(new String[WHITE_LIST.size()])).permitAll()
            // Any other request must be authenticated
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(token1Filter, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(token2Filter, UsernamePasswordAuthenticationFilter.class);
  }
}

As you can see, the different Urls are included in different properties and every List related with them is configured in HttpSecurity class. The filters used to manage every subset, I mean, Urls securized using Jwt 1 and Jwt 2 are the following ones:

@AllArgsConstructor
@Component
public class Token1Filter extends OncePerRequestFilter {

  private static final String TOKEN_PREFIX = "Bearer ";

  @Override
  protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response,
                                 FilterChain filterChain) throws ServletException, IOException {
    getJwt(request)
            .ifPresent(jwt1 -> {
                /**
                 *    Here you can use functionality to check provided Jwt token 1,
                 * adding included data into Spring SecurityContextHolder.
                 */

                // Used for testing purpose
                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken("testUserToken1", null, new ArrayList<>())
                );
            });
    filterChain.doFilter(request, response);
  }


  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return !WebSecurityConfiguration.TOKEN1_LIST.contains(request.getRequestURI());
  }


  private Optional<String> getJwt(HttpServletRequest request) {
    return ofNullable(request)
            .map(r -> r.getHeader(AUTHORIZATION))
            .filter(Predicate.not(String::isEmpty))
            .map(t -> t.replace(TOKEN_PREFIX, ""))
            .filter(Predicate.not(String::isEmpty));
  }
}



@AllArgsConstructor
@Component
public class Token2Filter extends OncePerRequestFilter {

  private static final String TOKEN_PREFIX = "Bearer ";

  @Override
  protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response,
                                 FilterChain filterChain) throws ServletException, IOException {
    getJwt(request)
            .ifPresent(jwt2 -> {
                /**
                 *    Here you can use functionality to check provided Jwt token 2,
                 * adding included data into Spring SecurityContextHolder.
                 */

                // Used for testing purpose
                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken("testUserToken2", null,
                                asList(new SimpleGrantedAuthority("ADMIN")))
                );
            });
    filterChain.doFilter(request, response);
  }


  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return !WebSecurityConfiguration.TOKEN2_LIST.contains(request.getRequestURI());
  }


  private Optional<String> getJwt(HttpServletRequest request) {
    return ofNullable(request)
            .map(r -> r.getHeader(AUTHORIZATION))
            .filter(Predicate.not(String::isEmpty))
            .map(t -> t.replace(TOKEN_PREFIX, ""))
            .filter(Predicate.not(String::isEmpty));
  }
}

Every one include an implementation of shouldNotFilter method to know if it have to manage the current request. And contains a comment on location on which you need to add the functionality to extract and verify the provided Jwt token.

And finally, only a dummy controller to test every use case:

@AllArgsConstructor
@RestController
public class TestController {

  @GetMapping("/authenticate")
  public ResponseEntity<String> authenticate(@RequestParam String username, @RequestParam String password) {
    return new ResponseEntity("[authenticate] Testing purpose", OK);
  }

  @GetMapping("/get-access-token")
  public ResponseEntity<String> getAccessToken() {
    return new ResponseEntity("[get-access-token] Testing purpose", OK);
  }

  @GetMapping("/add-new-user")
  @PreAuthorize("hasAuthority('ADMIN')")
  public ResponseEntity<String> addNewUser() {
    return new ResponseEntity("[add-new-user] Testing purpose", OK);
  }
}

Several improvements could be added but this is a good initial point that works as expected.

Related