15 Token Extension How to Use JWT to Implement Custom Tokens

15 Token Extension How to Use JWT to Implement Custom Tokens #

In the previous lecture, we discussed in detail how to use tokens to control access to microservices in a microservices architecture. The token we used in the previous lecture is a string structure similar to “b7c2c7e0-0223-40e2-911d-eff82d125b80”. Obviously, this type of token has limited content. Is there a way to create a more richly structured token? The answer is yes.

In fact, the OAuth2 protocol does not specify the specific structure of the token. In practical applications, I do not recommend using the token format we used in the previous lecture, but rather prefer using JWT. Today, we will discuss how to implement customized tokens based on JWT.

What is JWT? #

JWT stands for JSON Web Token, so it is essentially a token based on JSON representation. JWT is designed to provide a standard structure for tokens used in the OAuth2 protocol, so it is often used in conjunction with the OAuth2 protocol.

Basic Structure of JWT #

In terms of structure, JWT consists of three parts: the header, the payload, and the signature. As shown below:

header.payload.signature

From a data format perspective, the contents of these three parts are all JSON objects. In JWT, each JSON object is encoded using Base64, and the encoded content is joined together with a “.”. Therefore, JWT is essentially a string. The following is an example of a JWT string:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NwcmluZy5leGFtcGxlLmNvbSIsInN1YiI6Im1haWx0bzpzcHJpbmdAZXhhbXBsZS5jb20iLCJuYmYiOjE2MTU4MTg2NDYsImV4cCI6MTYxNTgyMjI0NiwiaWF0IjoxNjE1ODE4NjQ2LCJqdGkiOiJpZDEyMzQ1NiIsInR5cCI6Imh0dHBzOi8vc3ByaW5nLmV4YW1wbGUuY29tL3JlZ2lzdGVyIn0.Nweh3OPKl-p0PrSNDUQZ9LkJVWxjAP76uQscYJFQr9w

Obviously, we cannot get any useful information from this Base64 encoded string. There are also online tools available for generating and parsing JWT. With these tools, we can extract the original JSON data contained in the JWT string mentioned above, as shown below:

{
  alg: "HS256",
  typ: "JWT"
}.
{
  iss: "https://spring.example.com",
  sub: "mailto:example[email protected]",
  nbf: 1615818646,
  exp: 1615822246,
  iat: 1615818646,
  jti: "id123456",
  typ: "https://spring.example.com/register"
}.
[signature]

We can clearly see the content of the header and payload sections of the JWT. For security reasons, JWT parsing tools usually do not display the signature section data.

Advantages of JWT #

JWT has many excellent features. Its data representation format uses language-independent JSON format, which can be integrated with various heterogeneous systems. At the same time, JWT is a standard way to represent data, and everyone can follow this standard to pass data.

In the field of security, we usually use it to pass authenticated user identity information in order to obtain resources from the resource server. At the same time, JWT also provides good extensibility in terms of structure, and developers can add additional information to handle complex business logic as needed. Because the data in JWT is encrypted, it can be used for both authentication and encryption requirements.

How to Integrate OAuth2 with JWT? #

By now, you may have realized that JWT and OAuth2 are targeting different application scenarios and are not inherently related. However, in many cases, we use JWT as an authentication mechanism when discussing OAuth2 implementation.

Spring Security provides out-of-the-box support for generating and validating JWT. Of course, to send and consume JWT, the OAuth2 authorization server and protected microservices need to be configured differently. The entire development process is consistent with generating ordinary tokens as discussed in the previous lecture, with the difference lying in the content and manner of configuration. Next, we will look at how to configure JWT in the OAuth2 authorization server.

For all independent services that need to use JWT, the first step is to add the corresponding dependency packages to the Maven pom file, as shown below:

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

The next step is to provide a configuration class to generate and convert JWT. In fact, the OAuth2 protocol provides a specific interface for managing token storage, which is the TokenStore interface, and the JwtTokenStore class is specifically used to store JWT tokens. Accordingly, we will also create a configuration class named JWTTokenStoreConfig to configure JwtTokenStore, as shown below:

@Configuration
public class JWTTokenStoreConfig {

  @Bean
  public TokenStore tokenStore() {
    return new JwtTokenStore(jwtAccessTokenConverter());
  }

  @Bean
  public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123456");
    return converter;
  }

  @Bean
  // ...
}
public DefaultTokenServices tokenServices() {

DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

defaultTokenServices.setTokenStore(tokenStore());

defaultTokenServices.setSupportRefreshToken(true);

return defaultTokenServices;

}
}

As you can see, a JwtTokenStore object is constructed here, and a JwtAccessTokenConverter is passed into its constructor. JwtAccessTokenConverter is a converter used to convert JWT, and the conversion process requires a signing key. After creating the JwtTokenStore, we use the tokenServices method to return the DefaultTokenServices object that has already been set with the JwtTokenStore object.

The purpose of the above JWTTokenStoreConfig is to create a series of objects for the Spring container to use. When do we use these objects? The answer is in the process of integrating JWT into the OAuth2 authorization server, which is a familiar process. Based on the discussion in Lesson 13 “[Authorization System: How to Build an OAuth2 Authorization Server?]”, we can create a configuration class to override the configure method in AuthorizationServerConfigurerAdapter. Recall the original implementation of this configure method:

@Override

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

 

        endpoints.authenticationManager(authenticationManager)

                .userDetailsService(userDetailsService);

}

After integrating JWT, the implementation of this method needs to be adjusted, as shown below:

@Override

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();

              tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter).tokenEnhancer(tokenEnhancerChain)

        .authenticationManager(authenticationManager)

        .userDetailsService(userDetailsService);

}

As you can see, a token enhancement chain TokenEnhancerChain is created here for the token, and the tokenStore and jwtAccessTokenConverter objects created in JWTTokenStoreConfig are used. With this, we have completed the process of integrating JWT into the OAuth2 protocol, which means that the token we obtain when accessing the OAuth2 authorization server should now be a JWT token.

Let’s give it a try. We made a request through Postman and obtained the corresponding token:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJTcHJpbmcgU3lzdGVtIiwidXNlcl9uYW1lIjoic3ByaW5nX3VzZXIiLCJzY29wZSI6WyJ3ZWJjbGllbnQiXSwiZXhwIjoxNjE3NTYwODU0LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiY2UyYTgzZmYtMjMzMC00YmQ1LTk4MzUtOWIyYzE0N2Y2MTcyIiwiY2xpZW50X2lkIjoic3ByaW5nIn0.Cd_x3r-Fi9hudA2W80amLEga0utPiOJCgBxxLI4Lsb8",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJTcHJpbmcgU3lzdGVtIiwidXNlcl9uYW1lIjoic3ByaW5nX3VzZXIiLCJzY29wZSI6WyJ3ZWJjbGllbnQiXSwiYXRpIjoiY2UyYTgzZmYtMjMzMC00YmQ1LTk4MzUtOWIyYzE0N2Y2MTcyIiwiZXhwIjoxNjIwMTA5NjU0LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMDA0NjIxY2MtMmRmZi00ZDJiLWE0YWUtNTU5MzM5YzkyYmFhIiwiY2xpZW50X2lkIjoic3ByaW5nIn0.xDhGwhNTq7Iun9yLENaCvh8mrVHkabu3J8sP0NXENq0",
    "expires_in": 43199,
    "scope": "webclient",
    "system": "Spring System",
    "jti": "ce2a83ff-2330-4bd5-9835-9b2c147f6172"
}

Clearly, the access_token and refresh_token here are already Base64-encoded strings. Similarly, we can use online tools to parse the content of this JSON data format. The following is the original content of the access_token:

{
 alg: "HS256",
 typ: "JWT"
}.

{
 system: "Spring System",
 user_name: "spring_user",
 scope: [
  "webclient"
 ],
 exp: 1617560854,
 authorities: [
  "ROLE_USER"
 ],
 jti: "ce2a83ff-2330-4bd5-9835-9b2c147f6172",
 client_id: "spring"

}.

[signature]

How to use JWT in microservices? #

The first step in using JWT in microservices is configuration. We need to add a WTTokenStoreConfig configuration class to each microservice. The content of this class is to create a JwtTokenStore and build tokenServices, which has been introduced earlier and will not be discussed here.

After the configuration is completed, the remaining issue is how to propagate JWT in the service invocation chain. In the previous lesson, we provided the OAuth2RestTemplate utility class, which can propagate regular tokens. Unfortunately, it cannot propagate JWT-based tokens. From the implementation principle, OAuth2RestTemplate is also a wrapper based on RestTemplate, so our approach is to try adding support for JWT in RestTemplate requests.

  • As we know, HTTP requests pass the token by adding an “Authorization” message header in the header section. So the first step is to be able to get the JWT token from the HTTP request.
  • Then in the second step, we need to store this token in a thread-safe place for future use in the service chain.
  • The third step, which is the most critical one, is to automatically embed this token in every HTTP request made through RestTemplate.

The overall implementation approach is shown in the following diagram:

1.png Three implementation steps of propagating JWT token in the service invocation chain

Implementing this approach requires some understanding of the process and principles of HTTP requests, as well as some techniques in code implementation. Now I will explain them one by one.

First of all, in the process of an HTTP request, we can filter all requests through a filter. A filter is a core component in Servlet, which basically builds a filter chain and adds customized processing mechanisms to requests and responses that pass through this filter chain. The definition of the Filter interface is as follows:

public interface Filter {

    public void init(FilterConfig filterConfig) throws ServletException;

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    public void destroy();

}

Usually, we implement the doFilter method in the Filter interface. For more details about filters, you can review Lesson 08, “Pipeline Filtering: How to Extend Security with Spring Security Filters”. Based on filters, we can convert ServletRequest to an HttpServletRequest object and get the “Authorization” message header from this object. The sample code is as follows:

@Component
public class AuthorizationHeaderFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;

        AuthorizationHeaderHolder.getAuthorizationHeader().setAuthorizationHeader(httpServletRequest.getHeader(AuthorizationHeader.AUTHORIZATION_HEADER));

        filterChain.doFilter(httpServletRequest, servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}
}

Please note that here we save the “Authorization” message header obtained from the HTTP request in an AuthorizationHeaderHolder object. From the name, the AuthorizationHeader object represents the “Authorization” message header in HTTP, and AuthorizationHeaderHolder is the holder of this message header object. This naming convention is commonly seen in mainstream open-source frameworks like Spring.

Generally speaking, -Holder at the end of a name is often a class for encapsulation, used to add additional features such as thread safety to the original object. Here, AuthorizationHeaderHolder is such an encapsulation class, as shown below:

public class AuthorizationHeaderHolder {

    private static final ThreadLocal<AuthorizationHeader> authorizationHeaderContext = new ThreadLocal<AuthorizationHeader>();

    public static final AuthorizationHeader getAuthorizationHeader(){

        AuthorizationHeader header = authorizationHeaderContext.get();

        if (header == null) {

             header = new AuthorizationHeader();

            authorizationHeaderContext.set(header);

        }

        return authorizationHeaderContext.get();

    }
}
public static final void setAuthorizationHeader(AuthorizationHeader header) {

    authorizationHeaderContext.set(header);

}

As you can see, ThreadLocal is used here to ensure the thread safety of accessing the AuthorizationHeader object. The AuthorizationHeader is defined as follows and is used to store the JWT Token from the HTTP request header:

@Component
public class AuthorizationHeader {
    public static final String AUTHORIZATION_HEADER = "Authorization";
    private String authorizationHeader = new String();
 
    public String getAuthorizationHeader() {
        return authorizationHeader;
    }
 
    public void setAuthorizationHeader(String authorizationHeader) {
        this.authorizationHeader = authorizationHeader;
    }
}

Now, for each HTTP request, we can retrieve the Token from it and store it in the context object. The only remaining problem is how to pass this Token to the next service through RestTemplate, so that the next service can also retrieve the Token from the HTTP request and continue to pass it on, ensuring the continuous propagation of the Token throughout the call chain. To achieve this goal, we need to make some settings for RestTemplate, as shown below:

@Bean
public RestTemplate getCustomRestTemplate() {
    RestTemplate template = new RestTemplate();
    List<ClientHttpRequestInterceptor> interceptors = template.getInterceptors();
    if (interceptors == null) {
       template.setInterceptors(Collections.singletonList(new AuthorizationHeaderInterceptor()));
    } else {
       interceptors.add(new AuthorizationHeaderInterceptor());
       template.setInterceptors(interceptors);
    }
 
    return template;
}

RestTemplate allows developers to add custom interceptors, which are similar in functionality to filters and are used to customize the incoming HTTP requests. For example, the purpose of the AuthorizationHeaderInterceptor in the above code is to embed the JWT Token stored in AuthorizationHeaderHolder into the message header of the HTTP request, as shown below:

public class AuthorizationHeaderInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        HttpHeaders headers = request.getHeaders();
        headers.add(AuthorizationHeader.AUTHORIZATION_HEADER, AuthorizationHeaderHolder.getAuthorizationHeader().getAuthorizationHeader());

        return execution.execute(request, body);
    }
}

So far, we have finished explaining the method of using JWT in microservices. There is one more thing about JWT that we haven’t introduced, which is how to expand the data structure held in JWT. We will supplement this part of the content in the next case system, combined with specific business scenarios.

Summary and Preview

This is the last lesson in introducing the knowledge system of microservice security, focusing on authentication rather than authorization, and introducing the JWT mechanism for this purpose. JWT is essentially a type of Token, but it provides standardized specifications and can be integrated with the OAuth2 protocol. When using JWT, we can also add various information to this Token and propagate it in the microservice access chain.

Here’s a question for you to think about: How can we ensure that JWT is effectively propagated in each microservice?

With the introduction of JWT, the next lecture will introduce a new case. We will build a SpringOAuth2 case system based on Spring Security and Spring Cloud, and provide a detailed process for implementing secure access to services in a microservice architecture.