Part 1: The server side

The resource server

Resource Server (Products)

Tip: You may look into the Spring Boot Reference Documentation and the Spring Security Reference Documentation on how to implement a resource server.

Step 1: Change Maven dependencies for the resource server

To start with this tutorial please navigate to the project initial/product in your IDE. This is the starting point for all following implementation steps.

The existing product server is using the base spring security lib to secure its endpoints using basic authentication and form login.

To change these existing authentication mechanisms to JWT authentication as a resource server we need to adapt the spring security dependencies, i.e. use the corresponding one for building a secure OAuth2/OIDC resource server instead of simple basic authentication.

To perform this required change replace the following dependency in the existing maven pom.xml file:

pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

with this new dependency:

pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Step 2: Add required properties for the resource server

The resource server requires the public key(s) to validate the signature of incoming JSON web tokens (JWT). This way nobody can just issue their own JWT tokens or modify the issued token along the transmission path. The public key(s) will be automatically grabbed from the JSON web key set provided by the identity provider at https://access-me.eu.auth0.com/.well-known/jwks.json.

Spring security provides a predefined property spring.security.oauth2.resourceserver.jwt.jwt-set-uri to specify this.

After adding this new property the updated application.yml should look like this:

application.yml:

spring:
  jpa:
    open-in-view: false
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://access-me.eu.auth0.com/.well-known/jwks.json

Important: Please check that all indents are correct. Otherwise, you may get strange runtime errors when starting the application.

Step 3: Change the security configuration for the resource server

Please navigate to the class com.example.security.WebSecurityConfiguration in your IDE and change this with the following contents.

com.example.security.WebSecurityConfiguration.java:

package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

@EnableMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class WebSecurityConfiguration {

  @Bean
  public SecurityFilterChain api(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(STATELESS)
                .and()
                .httpBasic()
                .disable()
                .formLogin()
                .disable()
                .authorizeHttpRequests()
                .anyRequest()
                .fullyAuthenticated()
                .and()
                .oauth2ResourceServer()
                .jwt();
        return http.build();
    }
}

In this updated security configuration we

  • disable web sessions as with token authentication each request must contain the token in the header and a session cookie is not required anymore

  • disable CSRF protection as we do not use session cookies anymore and therefore are not vulnerable to CSRF attacks

  • disable basic authentication and form-based login

  • enable the application to act as an OAuth2/OIDC resource server requiring JWT tokens in the authorization header

Please note that the bean definition for the PasswordEncoder has been removed as well as password encoding is not required anymore. This will cause compilation errors in ProductInitializer class. To solve these just remove all references to the encoder in that class.

com.example.ProductInitializer.java:

package com.example;

import com.example.product.Product;
import com.example.product.ProductRepository;
import com.example.productuser.ProductUser;
import com.example.productuser.ProductUserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.stream.Stream;

/** Initializes some products in database. */
@Component
public class ProductInitializer implements CommandLineRunner {
  private static final Logger LOG = LoggerFactory.getLogger(ProductInitializer.class.getName());

  private final ProductRepository productRepository;
  private final ProductUserRepository productUserRepository;

  public ProductInitializer(
      ProductRepository productRepository, ProductUserRepository productUserRepository) {
    this.productRepository = productRepository;
    this.productUserRepository = productUserRepository;
  }

  @Override
  public void run(String... strings) {
    Stream.of(
            new Product("Apple", "A green apple", 3.50),
            new Product("Banana", "The perfect banana", 7.00),
            new Product("Orange", "Lots of sweet oranges", 33.00),
            new Product("Pineapple", "Exotic pineapple", 1.50),
            new Product("Grapes", "Red wine grapes", 10.75))
        .forEach(productRepository::save);

    LOG.info("Created " + productRepository.count() + " products");

    Stream.of(
            new ProductUser(
                "auth0|5bc44fceb144eb0173391741",
                "Uwe",
                "User",
                "n/a",
                "user@example.com",
                Collections.singletonList("USER")),
            new ProductUser(
                "auth0|5bc4b1553385d56f61f70e3b",
                "Alex",
                "Admin",
                "n/a",
                "admin@example.com",
                Collections.singletonList("ADMIN")))
        .forEach(productUserRepository::save);

    LOG.info("Created " + productUserRepository.count() + " users");
  }
}

Step 4: Convert the JWT into the ProductUser

With the changes of step 3, the base configuration for a resource server is set up. But there is one issue with this change. In class com.example.product.ProductRestController we do not get ProductUser as input for @AuthenticationPrincipal, instead by default the class org.springframework.security.oauth2.jwt.Jwt will be provided as input.

@RestController
public class ProductRestController {
  ...
  @GetMapping(path = "/products")
  public List<Product> getAllProducts(@AuthenticationPrincipal(errorOnInvalidType = true) ProductUser productUser) {
    ...
  }
}

To change this behavior we have to add our own converter from the JWT token to the ProductUser class. This is done in several steps.

First, we need to define our own type for AuthenticationToken. This is the central point where Spring Security stores all authentication details after authentication has been successfully performed.

com.example.security.ProductUserAuthenticationToken:

package com.example.security;

import com.example.productuser.ProductUser;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class ProductUserAuthenticationToken extends AbstractAuthenticationToken {

  private final ProductUser productUser;

  public ProductUserAuthenticationToken( ProductUser productUser, Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    setAuthenticated(true);
    this.productUser = productUser;
  }

  @Override
  public Object getCredentials() {
    return "n/a";
  }

  @Override
  public Object getPrincipal() {
    return this.productUser;
  }
}

The previous class will now be used as part of the ProductJwtAuthenticationConverter. This converts the contents of the JWT token into attributes of our ProductUser.

com.example.security.ProductJwtAuthenticationConverter:

package com.example.security;

import com.example.productuser.ProductUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;

@Component
public class ProductJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

  private final UserDetailsService userDetailsService;

  @Autowired
  public ProductJwtAuthenticationConverter(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
  }

  @Override
  public AbstractAuthenticationToken convert(Jwt jwt) {
    UserDetails userDetails = userDetailsService.loadUserByUsername(jwt.getSubject());
    if (userDetails instanceof ProductUser) {
      return new ProductUserAuthenticationToken((ProductUser) userDetails, userDetails.getAuthorities());
    } else {
      return null;
    }
  }
}

Also, the existing ProductUserDetailsService class has to be changed because now we will use the attribute userid to identify the user in our database instead of email. The user id is given to us as the subject claim inside the JWT token.

com.example.security.ProductUserDetailsService:

package com.example.security;

import com.example.productuser.ProductUser;
import com.example.productuser.ProductUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Primary
@Service
public class ProductUserDetailsService implements UserDetailsService {

  private final ProductUserService productUserService;

  @Autowired
  public ProductUserDetailsService(ProductUserService productUserService) {
    this.productUserService = productUserService;
  }

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    ProductUser user = productUserService.findByUserId(username);
    if (user == null) {
      throw new UsernameNotFoundException(
          "No user could be found for user name '" + username + "'");
    }

    return user;
  }
}

Finally, we have to add this new ProductJwtAuthenticationConverter to our security configuration.

com.example.security.WebSecurityConfiguration.java:

package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

@EnableMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class WebSecurityConfiguration {

  private final ProductJwtAuthenticationConverter productJwtAuthenticationConverter;

  public WebSecurityConfiguration(ProductJwtAuthenticationConverter productJwtAuthenticationConverter) {
    this.productJwtAuthenticationConverter = productJwtAuthenticationConverter;
  }

  @Bean
  public SecurityFilterChain api(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(STATELESS)
                .and()
                .httpBasic()
                .disable()
                .formLogin()
                .disable()
                .authorizeHttpRequests()
                .anyRequest()
                .fullyAuthenticated()
                .and()
                .oauth2ResourceServer()
                .jwt().jwtAuthenticationConverter(productJwtAuthenticationConverter);
        return http.build();
    }
}

Step 5: Run the product server application

Now we are ready to start the product server. Select the class com.example.ProductApplication and run this (use the right mouse button in your IDE or the spring boot dashboard if applicable).

To test the REST API (http://localhost:9090/server/products) of the running product server we will use Postman. You may also use command-line tools like curl or httpie as well.

After starting Postman you can create a new collection by clicking the button New Collection on the left. Then you can add a new request by clicking the 3 dots next to the collection and selecting Add Request.

Just fill in the URL you see in the picture below.

If you now click send then you will get a 401 error because the JWT token is missing to access this endpoint.

To get such a token, please navigate to the tab Authorization on the request screen and click on the Get New Access Token button. Then you will see a dialog as shown in the picture below.

Just fill in the required values from the table below and then click on Request Token:

After you got a token you can close this dialog and try again to send the request. This time it should work and you should see a list of products as JSON response.

Last updated