Securing microservices with JSON Web Tokens

duration 25 minutes

You’ll explore how to control user and role access to microservices with MicroProfile JSON Web Token (MicroProfile JWT).

What you’ll learn

You will add MicroProfile JWT to validate security tokens in the system and inventory microservices. You will use a token-based authentication mechanism to authenticate, authorize, and verify user identities based on a security token.

In addition, you will learn how to verify token claims through getters with MicroProfile JWT.

For microservices, a token-based authentication mechanism offers a lightweight way for security controls and security tokens to propagate user identities across different services. JSON Web Token (JWT) is becoming the most common token format because it follows well-defined and known standards.

MicroProfile JWT standards define the required format of JWT for authentication and authorization. The standards also map JWT token claims to various Java EE container APIs and make the set of claims available through getters.

The application that you will be working with is an inventory service, which stores the information about various JVMs that run on different systems. Whenever a request is made to the inventory service to retrieve the JVM system properties of a particular host, the inventory service communicates with the system service on that host to get these system properties. The JWT token gets propagated and verified during the comminication between two services.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-microprofile-jwt.git
cd guide-microprofile-jwt

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project, which is what you will build.

Try what you’ll build

The finish directory contains the finished JWT security implementation for the services in the application. You can try the finished application before you build your own.

To try out the application, first navigate to the finish directory. Next, execute the following Maven goals to build the application and run it inside of Open Liberty:

mvn clean install liberty:start-server

Note that you cannot directly visit a back end URL from a browser. You have to use the provided front end that generates valid JWT tokens to retrieve information from the back end.

Navigate your browser to the front-end web application endpoint: http://localhost:9090/application.jsf. From here, you can log in to the application with the form-based login. Log in with one of the following user information credentials:

Username

Password

Role

bob

bobpwd

admin, user

alice

alicepwd

user

carl

carlpwd

user

You are redirected to a page that displays the user who is logged in, the current OS of your localhost, and the security token that the front end uses when it sends the HTTP request to access the data from the back end. In addition, if you log in as an admin user, the current inventory size appears. If you log in as a user, you instead see the message, You are not authorized to access the inventory service., which means you cannot access the inventory service in the back end.

When you are done with the application, stop the server with the following command:

mvn liberty:stop-server

Securing back end services with MicroProfile JWT

Navigate to the start directory.

The MicroProfile JWT feature, mpJwt, has been added as a dependency in your start/backendServices/pom.xml file.

Creating the server configuration file

In the backendServices/src/main/liberty/config directory, create a configuration file named server.xml.

<server description="Backend server">
    <featureManager>
        <feature>mpJwt-1.1</feature>
        <feature>ssl-1.0</feature>
        <feature>jaxrs-2.1</feature>
        <feature>jsonp-1.1</feature>
        <feature>cdi-2.0</feature>
        <feature>appSecurity-3.0</feature>
    </featureManager>

    <!-- This is the keystore that will be used by SSL and by JWT.
         The keystore is built using the maven keytool plugin -->
    <keyStore id="defaultKeyStore" location="keystore.jceks" type="JCEKS" password="secret" />

    <!-- The HTTP ports that the application will use. -->
    <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="${http.port}" httpsPort="${https.port}"/>

    <!-- The application containing the user and login endpoints. -->
    <webApplication location="${app.name}" contextRoot="/" />

    <!-- The MP JWT configuration that injects the caller's JWT into a ResourceScoped bean for inspection. -->
    <mpJwt id="jwtUserConsumer" keyName="default" audiences="simpleapp" issuer="${jwt.issuer}"/>
</server>

The mpJwt feature adds the libraries that are required for MicroProfile JWT implementation.

The <mpJwt> element is required to configure the injection of the caller’s JWT.

issuer

Issues security tokens and must match the iss claim in the JWT.

audiences

Identifies the recipients and must match the aud claim in the JWT.

keyName

Specifies the key alias name to locate the public key for JWT signature validation and must be in the keystore in the SSL configuration.

Securing the system service

Open the backendServices/src/main/java/io/openliberty/guides/system/SystemResource.java file and add roles-based access control. Simply copy all the following code and replace the contents of the file:

package io.openliberty.guides.system;

import java.util.Properties;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.annotation.security.RolesAllowed;

@RequestScoped
@Path("properties")
public class SystemResource {
  @GET
  @RolesAllowed({ "admin", "user" })
  @Produces(MediaType.APPLICATION_JSON)
  public Properties getProperties() {
    return System.getProperties();
  }
}

Role names that are used in the @RolesAllowed annotation are mapped to group names in the groups claim of the JWT, which results in an authorization decision wherever the security constraint is applied.

The @RolesAllowed({"admin", "user"}) annotation allows only authenticated users with the role of admin or user to access the system service. Therefore, this service is properly secured.

Securing the inventory service

Open the backendServices/src/main/java/io/openliberty/guides/inventory/InventoryResource.java file and add roles-based access control. Simply copy all the following code and replace the contents of the file:

package io.openliberty.guides.inventory;

import java.util.Properties;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import io.openliberty.guides.inventory.model.InventoryList;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Context;

@RequestScoped
@Path("systems")
public class InventoryResource {

  @Inject
  InventoryManager manager;

  @GET
  @RolesAllowed({ "admin", "user" })
  @Path("{hostname}")
  @Produces(MediaType.APPLICATION_JSON)
  public Response getPropertiesForHost(@PathParam("hostname") String hostname,
      @Context HttpHeaders httpHeaders) {
    String authHeader = httpHeaders.getRequestHeaders()
                                   .getFirst(HttpHeaders.AUTHORIZATION);
    // Get properties
    Properties props = manager.get(hostname, authHeader);
    if (props == null) {
      return Response.status(Response.Status.NOT_FOUND)
                     .entity(
                         "ERROR: Unknown hostname or the resource may not be running on the host machine")
                     .build();
    }

    // Add to inventory
    manager.add(hostname, props);
    return Response.ok(props).build();
  }

  @GET
  @RolesAllowed({ "admin" })
  @Produces(MediaType.APPLICATION_JSON)
  public InventoryList listContents() {
    return manager.list();
  }
}

Similarly, for each specific service, the @RolesAllowed annotation sets which roles are allowed to access it. Therefore, the inventory service is properly secured, as well.

In addition, the getPropertiesForHost() method gets the HTTP Authorization header, which contains the corresponding security token, from the HTTP request caller. The method uses this authorization header to retrieve information from the system service.

The final addition of the manager.get(hostname, authHeader) code adds the authentication header to the client that sends the HTTP GET request to the system service.

Adding the SecureSystemClient class

In the beginning, the inventory service uses the normal SystemClient class to create a client and send HTTP GET requests to retrieve information from the system service. However, after you secure the system service with token-based authentication, you need to add security tokens when you send HTTP requests through the client.

Create the backendServices/src/main/java/io/openliberty/guides/inventory/client/SecureSystemClient.java file with the following code:

package io.openliberty.guides.inventory.client;

import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.core.HttpHeaders;
import java.util.Properties;
import io.openliberty.guides.inventory.client.SystemClient;

public class SecureSystemClient extends SystemClient {

  // Constants for building URI to the system service.
  private final int DEFAULT_SEC_PORT = Integer.valueOf(
      System.getProperty("https.port"));
  private final String SYSTEM_PROPERTIES = "/system/properties";
  private final String SECURED_PROTOCOL = "https";

  private String url;
  private Builder clientBuilder;

  // Overiding the parent method to set the attributes.
  public void init(String hostname, String authHeader) {
    this.url = this.buildUrl(SECURED_PROTOCOL, hostname, DEFAULT_SEC_PORT,
        SYSTEM_PROPERTIES);
    this.clientBuilder = this.buildClientBuilder(authHeader);
  }

  public String buildUrl(String protocol, String host, int port, String path) {
    return super.buildUrl(protocol, host, port, path);
  }

  public Builder buildClientBuilder(String authHeader) {
    Builder builder = super.buildClientBuilder(this.url);
    return builder.header(HttpHeaders.AUTHORIZATION, authHeader);
  }

  public Properties getProperties() {
    return super.getPropertiesHelper(this.clientBuilder);
  }
}

The SecureSystemClient class inherits methods from the SystemClient class. These methods hand the HTTP GET request to the system service to retrieve system properties. However, the SecureSystemClient class overrides the buildClientBuilder() method by adding the authorization header to the returned builder, return builder.header(HttpHeaders.AUTHORIZATION, authHeader). By adding the authorization header to the client request, the SecureSystemClient class can access services that are secured.

Trying more JAX-RS methods to validate tokens

To demonstrate how MicroProfile JWT retrieves confidential information and validates custom claims from the security token, add an additional service that uses JAX-RS security-related methods. The JWT service also illustrates how JAX-RS methods map to MicroProfile JWT features.

Create a backendServices/src/main/java/io/openliberty/guides/inventory/JwtResource.java file to enable a service that retrieves information from the token.

package io.openliberty.guides.inventory;

import java.util.Set;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Context;
import org.eclipse.microprofile.jwt.JsonWebToken;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.core.SecurityContext;
import java.security.Principal;

@RequestScoped
@Path("jwt")
public class JwtResource {
  // The JWT of the current caller. Since this is a request scoped resource, the
  // JWT will be injected for each JAX-RS request. The injection is performed by
  // the mpJwt-1.0 feature.
  @Inject
  private JsonWebToken jwtPrincipal;

  @GET
  @RolesAllowed({ "admin", "user" })
  @Path("/username")
  public Response getJwtUsername() {
    return Response.ok(this.jwtPrincipal.getName()).build();
  }

  @GET
  @RolesAllowed({ "admin", "user" })
  @Path("/groups")
  public Response getJwtGroups(@Context SecurityContext securityContext) {
    Set<String> groups = null;
    Principal user = securityContext.getUserPrincipal();
    if (user instanceof JsonWebToken) {
      JsonWebToken jwt = (JsonWebToken) user;
      groups = jwt.getGroups();
    }
    return Response.ok(groups.toString()).build();
  }

  @GET
  @RolesAllowed({ "admin", "user" })
  @Path("/customClaim")
  public Response getCustomClaim(@Context SecurityContext securityContext) {
    if (securityContext.isUserInRole("admin")) {
      String customClaim = jwtPrincipal.getClaim("customClaim");
      return Response.ok(customClaim).build();
    }
    return Response.status(Response.Status.FORBIDDEN).build();
  }
}

MicroProfile maps the JWT claims to JAX-RS security-related methods and makes the set of claims available through getters.

The getJwtUsername() function retrieves the user name from the injected JsonWebToken jwtPrincipal token by calling the getName() method.

The getJwtGroups() function retrieves the user groups information from the JSON Web Token. The SecurityContext.getUserPrincipal() method returns an object in the java.security.Principal type. This object is also an instance of the org.eclipse.microprofile.jwt.JsonWebToken type, which has the getGroups() method.

The getCustomClaim() function retrieves the custom claim from the token if the authenticated user has the admin role. Otherwise, it returns a Status.FORBIDDEN message. The SecurityContext.isUserInRole(String) method returns a true value for any role that appears in the MicroProfile JWT groups claim because role names are mapped to group names in the claim. The jwtPrincipal.getClaim("customClaim") method retrieves the value of the custom claim by its name. This method is useful when you want to validate information about a custom claim from the security token.

Building and running the application

To build the application, run the Maven install phase from the command line in the start directory:

mvn install

This command builds the application and creates a .war file in the target directory. It also configures and installs Open Liberty into the target/liberty/wlp directory.

Next, run the Maven liberty:start-server goal:

mvn liberty:start-server

This goal starts an Open Liberty server instance. Your Maven pom.xml is already configured to start the application in this server instance.

After the Open Liberty application server starts, you can log in to the application with the simple front end. The entire front end code is provided for you to test if you can retrieve information from the back end services by using a valid security token.

You cannot directly visit a back end URL from a browser because no valid tokens exist in the authorization header when you send HTTP requests through a browser. You have to use the provided front end to create a web client to send HTTP GET requests with valid security tokens.

Use one of the following credentials to log in:

Username

Password

Role

bob

bobpwd

admin, user

alice

alicepwd

user

carl

carlpwd

user

Use the following endpoint to log in to the application:

Once you log in, you can see some basic information retrieved from the back end services. Remember that if you log in as a user without the admin role, you cannot see the inventory size because you do not have permission.

If you make changes to the code, use the Maven compile goal to rebuild the application and have the running Open Liberty server pick them up automatically:

mvn compile

To stop the Open Liberty server, run the Maven liberty:stop-server goal:

mvn liberty:stop-server

Testing the application

Write SystemEndpointTest, InventoryEndpointTest, and JwtTest classes to test that you can access different services with valid tokens.

To begin, create a test class for the system service in the backendServices/src/test/java/it/io/openliberty/guides/jwt/SystemEndpointTest.java file:

package it.io.openliberty.guides.jwt;

import static org.junit.Assert.assertEquals;

import javax.json.JsonObject;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.junit.Before;
import org.junit.Test;
import it.io.openliberty.guides.jwt.util.TestUtils;
import it.io.openliberty.guides.jwt.util.JwtVerifier;

public class SystemEndpointTest {

  private final String SYSTEM_PROPERTIES = "/system/properties";
  private final String TESTNAME = "TESTUSER";

  String baseUrl = "https://" + System.getProperty("liberty.test.hostname") + ":"
      + System.getProperty("liberty.test.ssl.port");

  String authHeader;

  @Before
  public void setup() throws Exception {
    authHeader = "Bearer " + new JwtVerifier().createAdminJwt(TESTNAME);
  }

  @Test
  public void testSuite() {
    this.testGetPropertiesWithJwt();
  }

  public void testGetPropertiesWithJwt() {
    // Get system properties by using Jwt token
    String propUrl = baseUrl + SYSTEM_PROPERTIES;
    Response propResponse = TestUtils.processRequest(propUrl, "GET", null,
        authHeader);

    assertEquals(
        "HTTP response code should have been " + Status.OK.getStatusCode() + ".",
        Status.OK.getStatusCode(), propResponse.getStatus());

    JsonObject responseJson = TestUtils.toJsonObj(
        propResponse.readEntity(String.class));

    assertEquals("The system property for the local and remote JVM should match",
        System.getProperty("os.name"), responseJson.getString("os.name"));
  }
}

Next, create a test class for the inventory service in the backendServices/src/test/java/it/io/openliberty/guides/jwt/InventoryEndpointTest.java file:

package it.io.openliberty.guides.jwt;

import static org.junit.Assert.assertEquals;

import javax.json.JsonObject;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.junit.Before;
import org.junit.Test;
import it.io.openliberty.guides.jwt.util.TestUtils;
import it.io.openliberty.guides.jwt.util.JwtVerifier;

public class InventoryEndpointTest {

  private final String INVENTORY_HOSTS = "/inventory/systems";
  private final String TESTNAME = "TESTUSER";

  String baseUrl = "https://" + System.getProperty("liberty.test.hostname") + ":"
      + System.getProperty("liberty.test.ssl.port");

  String authHeader;

  @Before
  public void setup() throws Exception {
    authHeader = "Bearer " + new JwtVerifier().createAdminJwt(TESTNAME);
  }

  @Test
  public void testSuite() {
    this.testEmptyInventoryWithJwt();
    this.testHostRegistrationWithJwt();
  }

  public void testEmptyInventoryWithJwt() {
    String invUrl = baseUrl + INVENTORY_HOSTS;
    Response invResponse = TestUtils.processRequest(invUrl, "GET", null, authHeader);

    assertEquals(
        "HTTP response code should have been " + Status.OK.getStatusCode() + ".",
        Status.OK.getStatusCode(), invResponse.getStatus());

    JsonObject responseJson = TestUtils.toJsonObj(
        invResponse.readEntity(String.class));

    assertEquals("The inventory should be empty on application start", 0,
        responseJson.getInt("total"));
  }

  public void testHostRegistrationWithJwt() {
    String invUrl = baseUrl + INVENTORY_HOSTS + "/localhost";
    Response invResponse = TestUtils.processRequest(invUrl, "GET", null, authHeader);

    assertEquals(
        "HTTP response code should have been " + Status.OK.getStatusCode() + ".",
        Status.OK.getStatusCode(), invResponse.getStatus());

    JsonObject responseJson = TestUtils.toJsonObj(
        invResponse.readEntity(String.class));

    assertEquals("The inventory should get the os.name of localhost",
        System.getProperty("os.name"), responseJson.getString("os.name"));
  }

}

In the setup() step before test cases, an authorization header is created with the credentials of a test user who has the TESTUSER name and the admin role. This authorization header is added when the test client sends a HTTP GET request to a secure service to retrieve information.

Each test case tries to first assert that with a valid authorization header, it can get a Status.OK code from the response. The test fails if the response returns a Status.FORBIDDEN message, which means the authorization header is invalid.

After each test confirms that the response code is correct, each test gets the content from the designated endpoint and compares the result with its expected value.

Create a test class for the JWT service in the backendServices/src/test/java/it/io/openliberty/guides/jwt/JwtTest.java file:

package it.io.openliberty.guides.jwt;

import static org.junit.Assert.assertEquals;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.junit.Test;
import org.junit.Before;
import it.io.openliberty.guides.jwt.util.TestUtils;
import it.io.openliberty.guides.jwt.util.JwtVerifier;

public class JwtTest {

  private final String TESTNAME = "TESTUSER";
  private final String INV_JWT = "/inventory/jwt";

  String baseUrl = "https://" + System.getProperty("liberty.test.hostname") + ":"
      + System.getProperty("liberty.test.ssl.port");

  String authHeader;

  @Before
  public void setup() throws Exception {
    authHeader = "Bearer " + new JwtVerifier().createUserJwt(TESTNAME);
  }

  @Test
  public void testSuite() {
    this.testJwtGetName();
    this.testJwtGetCustomClaim();
  }

  public void testJwtGetName() {
    String jwtUrl = baseUrl + INV_JWT + "/username";
    Response jwtResponse = TestUtils.processRequest(jwtUrl, "GET", null, authHeader);

    assertEquals(
        "HTTP response code should have been " + Status.OK.getStatusCode() + ".",
        Status.OK.getStatusCode(), jwtResponse.getStatus());

    String responseName = jwtResponse.readEntity(String.class);

    assertEquals("The test name and jwt token name should match", TESTNAME,
        responseName);
  }

  public void testJwtGetCustomClaim() {
    String jwtUrl = baseUrl + INV_JWT + "/customClaim";
    Response jwtResponse = TestUtils.processRequest(jwtUrl, "GET", null, authHeader);

    assertEquals("HTTP response code should have been "
        + Status.FORBIDDEN.getStatusCode() + ".", Status.FORBIDDEN.getStatusCode(),
        jwtResponse.getStatus());
  }

}

In the setup() step of the JwtTest test, an authorization header is created with the credentials of a test user who has the TESTUSER name and the user role. The authorization header is added when the test client sends a HTTP GET request to a secure JWT service.

  • The testJWTGetName() test accesses the https://localhost:5051/inventory/jwt/username endpoint to get the username from the JWT token and to verify whether the user name is the same as the TESTUSER user name.

  • The testJWTGetCustomClaim() test accesses the https://localhost:5051/inventory/jwt/customClaim endpoint. Because this service is secured and only accessible to users who have the admin role, the test asserts that the current test user with the user role is forbidden from accessing it.

Running the tests

If the server is still running from the previous steps, stop it using the Maven liberty:stop-server goal from command line in the start directory:

mvn liberty:stop-server

Then, verify that the tests pass using the Maven verify goal:

mvn verify

It may take some time before build is complete. If the tests pass, you will see a similar output to the following:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.jwt.InventoryEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.434 sec - in it.io.openliberty.guides.jwt.InventoryEndpointTest
Running it.io.openliberty.guides.jwt.JwtTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.117 sec - in it.io.openliberty.guides.jwt.JwtTest
Running it.io.openliberty.guides.jwt.SystemEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.069 sec - in it.io.openliberty.guides.jwt.SystemEndpointTest

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

To see whether the tests detect a failure, remove the authorization header generation in the setup() method of the InventoryEndpointTest.java file. Rerun the Maven build. You see that a test failure occurs.

Great work! You’re done!

You learned how to use MicroProfile JWT to validate JWT and authorize users to secure your microservices.

Next, you can try one of the related MicroProfile guides. They demonstrate technologies that you can learn and expand on what you built here.

Contribute to this guide

Is something missing or broken? Raise an issue, or send us a pull request.