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:
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:
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:
packagecom.example;importcom.example.product.Product;importcom.example.product.ProductRepository;importcom.example.productuser.ProductUser;importcom.example.productuser.ProductUserRepository;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.boot.CommandLineRunner;importorg.springframework.stereotype.Component;importjava.util.Collections;importjava.util.stream.Stream;/** Initializes some products in database. */@ComponentpublicclassProductInitializerimplementsCommandLineRunner {privatestaticfinalLogger LOG =LoggerFactory.getLogger(ProductInitializer.class.getName());privatefinalProductRepository productRepository;privatefinalProductUserRepository productUserRepository;publicProductInitializer(ProductRepository productRepository,ProductUserRepository productUserRepository) {this.productRepository= productRepository;this.productUserRepository= productUserRepository; } @Overridepublicvoidrun(String... strings) {Stream.of(newProduct("Apple","A green apple",3.50),newProduct("Banana","The perfect banana",7.00),newProduct("Orange","Lots of sweet oranges",33.00),newProduct("Pineapple","Exotic pineapple",1.50),newProduct("Grapes","Red wine grapes",10.75)).forEach(productRepository::save);LOG.info("Created "+productRepository.count() +" products");Stream.of(newProductUser("auth0|5bc44fceb144eb0173391741","Uwe","User","n/a","user@example.com",Collections.singletonList("USER")),newProductUser("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.
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.
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:
packagecom.example.security;importcom.example.productuser.ProductUser;importcom.example.productuser.ProductUserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Primary;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;@Primary@ServicepublicclassProductUserDetailsServiceimplementsUserDetailsService {privatefinalProductUserService productUserService; @AutowiredpublicProductUserDetailsService(ProductUserService productUserService) {this.productUserService= productUserService; } @OverridepublicUserDetailsloadUserByUsername(String username) throwsUsernameNotFoundException {ProductUser user =productUserService.findByUserId(username);if (user ==null) {thrownewUsernameNotFoundException("No user could be found for user name '"+ username +"'"); }return user; }}
Finally, we have to add this new ProductJwtAuthenticationConverter to our security configuration.
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.