Authentication using JSON Web Tokens (JWT) sent via HTTP header
Validation of the JWT (Signature and expiry date/time) with loading public key for validating
Authorize users by using the roles claim inside the access token
Step 1: Explore the existing server application
To start with this tutorial please navigate to the project labs/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.
Before diving into changing the server application into a resource server it is recommended to explore the existing application. The application exposes several API endpoints. All endpoints are documented by OpenAPI 3.
To start the product server select the class com.example.ProductApplication and run this in your IDE.
If you call the users endpoint then you will get these results:
Both endpoints are secured by basic authentication or form based login. you can access the endpoints by using the following user credentials (access for users list requires ADMIN role):
Username/Password
Role(s)
bruce.wayne@example.com/wayne
USER
clark.kent@example.com/kent
USER
peter.parker@example.com/parker
ADMIN, USER
To make accessing the APIs more convenient you can use the provided postman collection. Please check the setup section for details.
Now let's start with changing the server into a resource server using modern authentication with a JSON web token (JWT).
Step 2: Change Maven dependencies for resource server
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 3: Add required properties for 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 http://localhost:9000/oauth2/jwks.
This endpoint publishes the public key(s) that are each identified by their key id (kid).
Whenever the resource server gets a JSON Web Token (JWT) with a kid in the header part (like the following sample), then Spring Security fetches the public key with the matching key id from this JWKS endpoint, caches it (so it does not have to load it for every request) and validates the JWt with the loaded public key.
Spring security provides a predefined property spring.security.oauth2.resourceserver.jwt.jwt-set-uri to specify this JWKS endpoint for loading and caching public keys.
After adding this new property the updated application.yml should look like this:
In this updated security configuration (the second SecurityFilterChain called api(...)) we:
disable web sessions as with token authentication each request must contain the token in the header and a session cookie is not required any more (stateless authentication)
disable CSRF protection as we do not use session cookies anymore and therefore are not vulnerable for CSRF attacks
disable basic authentication and formula based login (we only support token based authentication now)
enable the application to act as an OAuth2/OIDC resource server requiring JWT as bearer tokens in the authorization header
Please note that the bean definition for the PasswordEncoder has been removed as well as the password encoding is not required any more.
This will cause compilation errors in ProductInitializer class. To solve these just remove all references to the encoder in that class. There is also no need anymore to create users as users are not required any more to log in. This will be achieved by the OAuth authorization server (only this component knows about user credentials and other user details).
com.example.ProductInitializer.java:
packagecom.example;importcom.example.product.ProductEntity;importcom.example.product.ProductEntityRepository;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.boot.CommandLineRunner;importorg.springframework.stereotype.Component;importjava.math.BigDecimal;importjava.util.stream.Stream;/** Initializes some products in database. */@ComponentpublicclassProductInitializerimplementsCommandLineRunner {privatestaticfinalLogger LOG =LoggerFactory.getLogger(ProductInitializer.class.getName());privatefinalProductEntityRepository productEntityRepository;publicProductInitializer(ProductEntityRepository productEntityRepository) {this.productEntityRepository= productEntityRepository; } @Overridepublicvoidrun(String... strings) {Stream.of(newProductEntity("Apple","A green apple",BigDecimal.valueOf(3.50)),newProductEntity("Banana","The perfect banana",BigDecimal.valueOf(7.00)),newProductEntity("Orange","Lots of sweet oranges",BigDecimal.valueOf(33.00)),newProductEntity("Pineapple","Exotic pineapple",BigDecimal.valueOf(1.50)),newProductEntity("Grapes","Red wine grapes",BigDecimal.valueOf(10.75))).forEach(productEntityRepository::save);LOG.info("Created "+productEntityRepository.count() +" products"); }}
Step 5: Convert the JWT into the ProductUser object
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 (as this is the standard authenticated principle object when JWT is used in spring security).
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 of the interface AuthenticationToken. This is the central point where Spring Security stores all authentication details after authentication has been successfully performed.
For convenience spring security provides the AbstractAuthenticationToken that implements most parts of AuthenticationToken.
The previous class will now be used as part of the ProductJwtAuthenticationConverter. This converts contents of the JWT token into attributes of our ProductUser.
packagecom.example.security;importcom.example.productuser.ProductUser;importcom.example.productuser.ProductUserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.core.convert.converter.Converter;importorg.springframework.security.authentication.AbstractAuthenticationToken;importorg.springframework.security.oauth2.jwt.Jwt;importorg.springframework.stereotype.Component;importjava.util.ArrayList;importjava.util.List;@ComponentpublicclassProductJwtAuthenticationConverterimplementsConverter<Jwt,AbstractAuthenticationToken> {privatestaticfinalList<String> ROLE_CLAIMS =List.of("roles","permissions");privatestaticfinalString FIRST_NAME_CLAIM ="given_name";privatestaticfinalString LAST_NAME_CLAIM ="family_name";privatestaticfinalString EMAIL_CLAIM ="email";privatefinalProductUserService productUserService; @AutowiredpublicProductJwtAuthenticationConverter(ProductUserService productUserService) {this.productUserService= productUserService; } @OverridepublicAbstractAuthenticationTokenconvert(Jwt jwt) {ProductUser productUser =newProductUser(jwt.getSubject(),jwt.getClaimAsString(FIRST_NAME_CLAIM),jwt.getClaimAsString(LAST_NAME_CLAIM),"n/a",jwt.getClaimAsString(EMAIL_CLAIM), getRolesFromToken(jwt));// register the user, so we know what users are known to our systemif (productUserService.findByUserId(productUser.getUserId()).isEmpty()) {productUserService.save(productUser); }returnnewProductUserAuthenticationToken(productUser,productUser.getAuthorities()); }privateList<String> getRolesFromToken(Jwt jwt) {List<String> roles =newArrayList<>();for (String claim : ROLE_CLAIMS) {if (jwt.hasClaim(claim)) {roles.addAll(jwt.getClaimAsStringList(claim)); } }returnroles.stream().map(String::toUpperCase).toList(); }}
Please note:
The existing ProductUserDetailsService class is not required any more and is replaced by the ProductJwtAuthenticationConverter above.
So this class can be deleted completely.
Finally, we have to add this new ProductJwtAuthenticationConverter to the security configuration.
Now we are ready to re-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.
If you have imported the postman collection as described in the setup section then the authorization part should be pre-filled.
These are the required values that should be already configured in the Authorization tab:
Input
Value
Grant Type
Authorization Code with PKCE
Authorization URL
https://localhost:9000/oauth/authorize
Access Token URL
https://localhost:9000/oauth/token
Client ID
demo-client-pkce
All requests of the postman collection inside the folder OAuth2 Bearer Token require a valid JWT. So if you perform such request you will get a 401 error because the JWT token is missing to access this endpoint.
To get such a token click on the folder OAuth2 Bearer Token, then navigate to the tab Authorization click on the Get New Access Token button.
After you got a token click proceed and then click on _Use Token_try again to send the request. This time it should work, and you should see a list of products as JSON response.
In the next step we will make accessing the backend service a bit more user-friendly by enabling a provided client frontend to retrieve products from the backend using OAuth2/OIDC access tokens.