{{announcement.body}}
{{announcement.title}}

Securing Spring Boot Microservices with JSON Web Tokens (JWT)

DZone 's Guide to

Securing Spring Boot Microservices with JSON Web Tokens (JWT)

This article demonstrates how JWTs can be used for securing access to Java microservices built with Spring Boot.

· Microservices Zone ·
Free Resource

Abstract

I have become a fan of JSON web tokens (JWTs) ever since I have found out that these can offer nifty solutions for complex distributed access control requirements.

Over the past five years, I used JWTs, either independently or in association with other security solutions like SAML, or OpenID Connect to secure many distributed web applications.

In this article, I would like to demonstrate how JWTs can be used for securing access to Java microservices built with Spring Boot.

Table of Contents

  1. Real-world analogy of token-based authorization
  2. Technical overview of JWT based authorization
  3. Workshop: Single Sign-On for Spring Boot Microservices with JWT
  4. Study of source code
  5. Conclusion
  6. References

Real-World Analogy of Token-Based Authorization


Real-world analogy to describe JSON Web Token



I would like to cite my favorite public transport system, London's, to describe JSON Web Tokens! The system provides a simple, seamless, and user-friendly journey authorization experience for millions of people who use multiple modes of transport – train, tram, bus, ferry (not shown in the picture, to avoid overflowing)!

Customers can use all these modes with just one travel card. A travel card is purchased or recharged at the vending machine. The card is always carried by the customer, who just taps the card at the entry gate to use the ride.

JWT-based authorization for digital applications works similarly. A JWT is like a travel card. Applications (service providers) are like the different modes of transport. The vending machine that sells travel cards is like the JWT Token Issuer.

You may also enjoy: JWT Token: Lightweight, Token-Based Authentication

Technical Overview of JWT-Based Authorization

Like the real-world analogy explained above, JWT authorization involves three entities and four steps, as described in the sequence diagram below:

JWT based authorization flow

Entities in JWT Authorization Flow

  1. Token Issuer  The issuer of the token (Identity and Access management system)
  2. User Interface  The user’s browser or mobile app
  3. Service Provider – The web site or app that the user wants to access 

Steps in JWT Authorization

Step 1: Token Issuer Gives a Signed & Encrypted Token to User Interface

The user authenticates to Token Issuer using some login method and asks the Token Issuer to grant a token. Upon success authentication, the Token Issuer creates a JSON Web token (JWT) which has the following structure:

Header.Payload. Signature

More information on JWT structure can be found at https://jwt.io/

The payload part of the token contains key-value pairs called claims that provide information on who the user is and what the user is allowed to access. The Token Issuer creates a token with some claims and sends the token to user. Once the token is created and given to the user, the responsibility of using the token to gain access to service lies entirely with the user. The user is required to submit the token to the service provider, in order to obtain access.

As the responsibility of carrying the token is being given to the user, the following security concerns must be addressed:

  • Concern #1: What if the sensitive data in the token gets tapped by an eavesdropper?
  • Concern #2: What if the token is tampered to request services meant for other users?

The well-known RSA public key cryptography comes in handy to tackle these issues. The Token Issuer applies signature and encryption as follows:

  1. Token Issuer signs the token with its private key and creates JWS (JWT – Signed).
  2. Token Issuer then encrypts the JWS with the public key of the Service Provider. The ncrypted JWS is called JWE (JWT – encrypted).

Only the signed and encrypted token (JWE) is passed over to the user interface.

The user can only carry the JWE but cannot decrypt it. Only the Service Provider can decrypt the token and see the claims contained in it. Any tampering of token will also be detected by the Service Provider, as the token is signed by Token Issuer.

JWE is embedded as an Authorization header in the HTTP response sent to the client.

The authorization header appears as follows:

HTTP
 




xxxxxxxxxx
1


 
1
Authorization: Bearer encrypted-json-web-token-text


 

Step 2: User Interface Sends Token Along With Request to Service Provider

The user interface attaches the JWE as an Authorization Header to the HTTP request that it submits to the Service Provider.

Step 3: Service Provider Validates the Token

On receiving a request from user, the Service provider performs the following sequence of validations:

  1. Does the request have a token?
  2. Can the token be decrypted?
  3. Was the token content tampered with?
  4. Is the token valid?
  5. What are the claims inside the token?

At the end of validation, the Service Provider extracts the payload from the JWT and finds out the claims.

Here is an example of a JWT payload that the Service provider extracts from the JWE.

JSON
 




xxxxxxxxxx
1


 
1
{
2
  "iss": "token-provider-name",
3
  "aud": "service-provider-name",
4
  "iat": 1516227022,
5
  "exp": 1516239022,
6
  "jti": "unique-id-or-nonce",
7
  "username": "John Doe",
8
  "account": "123-456-789"
9
}


Claims found in the above payload:

Claim

Description

iss

The issuer of token (Token Issuer)

aud

Audience (The Service Provider that the token is meant for)

iat

Issued-at (Time at which the token was issued)

exp

Expiry (Time at which the token expires)

jti

JWT Token ID (A unique ID or randomly generated nonce)

username

User name

account

Account number of the user


Step 4: Service Provider Responds to User Interface

Service Provider gives the appropriate response to the user interface, based on the token and the claims.

Workshop: Single Sign-on for Spring Boot Microservices With JWT

Let us now use JSON web tokens to implement single sign-on for Spring Boot microservices. The full source code for this workshop is at https://github.com/deargopinath/jwt-spring-boot

Technologies used: Java, Spring Boot, JWT, Nimbus JOSE, JavaScript, CSS, HTML

The single sign-on solution has two parts:

  1. token-issuer – Code for creating signed and encrypted JWT 
  2. service-provider – Code for decrypting token and authorizing user with valid token

Steps to Run the code

Step 1: Compile and Run service-provider

Shell
 




xxxxxxxxxx
1


 
1
$ cd service-provider
2
$ mvn clean install
3
$ java -jar target/service-provider-1.0.0.jar


Step 2: Open Service Provider and verify that an error message (Invalid token) is displayed


Invalid token

Step 3: Compile & Run token-issuer in A New Command Window

Shell
 




xxxxxxxxxx
1


 
1
$ cd token-issuer
2
$ mvn clean install
3
$ java -jar target/token-issuer-1.0.0.jar


 

Step 4: Open the Token Issuer and get a token to access the Service Provider

Token issuer

Fill in the token form with relevant details (Service provider URL, User name, Account number) and click "Get a token" button to get a signed and encrypted token.

Step 5: Login to Service Provider using the token. Verify that the Service Provider allows the user with a valid token

Service provider login

Clicking on "Service Provider Login with Token" button sends token to the Service Provider.

The token will be embedded in the "Authorization Header" of the HTTP request.

Response from Service Provider appears in a new Tab.

Service provider

Service Provider decrypts the token, verifies the signature and then shows the welcome page for the user with valid token. Name and Account number shown by Service Provider are extracted from the encrypted token.

Study of The Source Code

Now. Let us study the source code to see what is happening under the hood.

Token Issuer

Project is structured as shown in the picture below:

token-issuer project structure

Let us study the code from these two files to understand the functionality of the application.

1. TokenService.java

This program creates the JSON Web Token, signs it with the Private key of the Token Issuer and then encrypts it with the Public Key of Service Provider.

Signing with Issuer’s Private key protects Data Integrity.

Encrypting with Service Provider’s Public key protects confidentiality.

Java
 




xxxxxxxxxx
1
51


 
1
public String getToken(RequestData requestData) {
2
  String token = "unknown";
3
  try {
4
      String subject = requestData.getSubject();
5
      String user = requestData.getUser();
6
      String account = requestData.getAccount();
7
      LOG.info("user = " + user + ", account = " + account + ", subject = " + subject);
8
      RSAKey serverJWK = getJSONWebKey(serverPKCS);
9
 
          
10
    // Set the token header
11
      JWSHeader jwtHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).jwk(serverJWK).build();
12
      Calendar now = Calendar.getInstance();
13
      Date issueTime = now.getTime();
14
      now.add(Calendar.MINUTE, 10);
15
      Date expiryTime = now.getTime();
16
      String jti = String.valueOf(issueTime.getTime());
17
   
18
 
          
19
    // Set the token payload
20
      JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
21
        .issuer(issuer)
22
        .subject(subject)
23
        .issueTime(issueTime)
24
        .expirationTime(expiryTime)
25
        .claim("user", user)
26
        .claim("account", account)
27
        .jwtID(jti)
28
        .build();
29
      LOG.info("JWT claims = " + jwtClaims.toString());
30
 
          
31
 
          
32
    // Sign the token with Issuer’s Private Key
33
      SignedJWT jws = new SignedJWT(jwtHeader, jwtClaims);
34
      RSASSASigner signer = new RSASSASigner(serverJWK);
35
      jws.sign(signer);      
36
      JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, 
37
         EncryptionMethod.A256GCM).contentType("JWT").build();
38
      JWEObject jwe = new JWEObject(jweHeader, new Payload(jws));
39
 
          
40
 
          
41
    // Encrypt signed token with Service Provider’s public key
42
      RSAKey clientPublicKey = getPublicKey(clientCertificate);
43
      jwe.encrypt(new RSAEncrypter(clientPublicKey));
44
      token = jwe.serialize();
45
      LOG.info("Token = " + token);      
46
    } catch (final JOSEException e) {
47
      LOG.error(e.toString());
48
    }
49
  return token;
50
  }
51
}


2. TokenController.java

This program sends the signed and encrypted token to User.

The token is embedded as the "Authorization Header" of the HTTP response sent to the user. The user will then pick the token from the Authorization Header and send it to Service Provider. 

Java
 




x
23


 
1
@PostMapping("/api/jwe")
2
public ResponseEntity<?> getToken(@Valid @RequestBody final RequestData requestData, final Errors errors) {
3
  
4
  if (errors.hasErrors()) {
5
    String errorMessage = errors
6
                            .getAllErrors()
7
                            .stream()
8
                            .map(x -> x.getDefaultMessage())
9
                            .collect(Collectors.joining(","));
10
    LOG.error("Error = " + errorMessage);
11
    return ResponseEntity.badRequest().body(errorMessage);
12
  }
13
 
          
14
  String subject = requestData.getSubject();
15
  String jwe = tokenService.getToken(requestData);
16
  String json = ("{\"subject\":\"" + subject 
17
              + "\",\"token\":\"" + jwe + "\"}");
18
  LOG.info("Token generated for " + subject);
19
  final HttpHeaders headers = new HttpHeaders();
20
  headers.add("Authorization", "Bearer " + jwe);
21
  LOG.info("Authorization Header set with token");
22
  return (new ResponseEntity<>(json, headers, HttpStatus.OK));
23
}


Service Provider

The project is structured as shown in the picture below:

service-provider project structure

Let us study these two files to understand the functionality of the application.

1. WebsiteConfiguration.java

This program configures cross-origin access control to allow users from Token Issuer’s domain to submit requests to Service Provider domain.

Java
 




xxxxxxxxxx
1
10


 
1
public class WebsiteConfiguration implements WebMvcConfigurer {
2
  
3
    @Value("${token.issuer.url}")
4
    private String tokenIssuer;
5
   
6
    @Override
7
    public void addCorsMappings(CorsRegistry registry) {
8
        registry.addMapping("/**").allowedOrigins(tokenIssuer);
9
    }
10
}



2. TokenValidator.java

This program extracts the token from the Authorization Header, decrypts it and validates it.

Java
 




xxxxxxxxxx
1
56


 
1
public boolean isValid(HttpServletRequest request) {
2
  
3
  Enumeration<String> headers = request.getHeaderNames();
4
 
          
5
  // Extract the encrypted token (JWE) form the Authorization Header
6
  while(headers.hasMoreElements()) {
7
        String key = headers.nextElement();
8
        if(key.trim().equalsIgnoreCase("Authorization")) {
9
            String authorizationHeader = request.getHeader(key);
10
            if(!authorizationHeader.isBlank()) {
11
                String[] tokenData = authorizationHeader.split(" ");
12
                if(tokenData.length == 2 && 
13
                   tokenData[0].trim().equalsIgnoreCase("Bearer")) {
14
                    token = tokenData[1];
15
                    LOG.info("Received token: " + token);
16
                    break;
17
                }
18
            }
19
        }
20
    }
21
 
          
22
 
          
23
    try {
24
        JWT jwt = JWTParser.parse(token);
25
      
26
      // Decrypt JWE into Signed JWT (JWS)
27
      if(jwt instanceof EncryptedJWT) {
28
        EncryptedJWT jwe = (EncryptedJWT) jwt;
29
        RSAKey clientJWK = getJSONWebKey(clientPKCS);
30
        JWEDecrypter decrypter = new RSADecrypter(clientJWK);
31
        jwe.decrypt(decrypter);
32
        SignedJWT jws = jwe.getPayload().toSignedJWT();
33
 
34
 
          
35
        // Verify the signature of JWS
36
            RSAKey serverJWK = getPublicKey(serverCertificate);
37
            RSASSAVerifier signVerifier = new RSASSAVerifier(serverJWK);
38
            if(jws.verify(signVerifier)) {
39
              // Extract the payload (claims) of JWT
40
               JWTClaimsSet claims = jws.getJWTClaimsSet();
41
               Date expiryTime = claims.getExpirationTime();
42
               LOG.info("Expiry time = " + expiryTime.toString());
43
               if(expiryTime.after(new Date())) {
44
                    user = claims.getStringClaim("user");
45
                    account = claims.getStringClaim("account");
46
                    LOG.info("Token validated for user = " + user 
47
                             + ", account = " + account);
48
                    return true;
49
                }
50
            }
51
        }
52
    } catch(ParseException | JOSEException ex) {
53
        LOG.error(ex.toString());
54
    }
55
    return false;
56
}


Conclusion

JSON Web Token (JWT) is used in modern Internet-scale authentication solutions like OpenID Connect and several commercial Identity and access management tools. This article explains a simple, scalable and secure method for authorizing microservices with JSON Web Tokens.

References

  1. JSON Web Token (JWT)
  2. Nimbus JOSE for secure JWTs
  3. Spring Boot
  4. NetBeans IDE for developing Java Microservices

Further Reading

Cookies vs. Tokens: The Definitive Guide

Four Most Used REST API Authentication Methods

Topics:
authorization ,cors ,java 13 ,jwt ,microservices ,nimbus jose ,openid connect ,spring boot ,spring boot 2.2 ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}