How to secure microservice applications with role-based access control? (4/7)

April 17, 2023

Foto Source: Nataliya Vaitkevich (www. pexels.com)

Option: JWT

In the previous blog part (part 3) we have used  BasicAuthentication for transferring credentials which are then enforced in the requested service. This approach has several disadvantages. First and foremost, each service has access to user credentials. Thus, if one service is hacked, all services are impacted.

For this reason, we introduce in this blog (part 4) a new concept called a JsonWebToken (JWT).

In Part 1, we’ve provided the context for the whole blog series. We recommend you read it first because otherwise, you miss out on the context.

You find below an overview of the content of these blog series. Just click on the link to jump directly to the respective blog part:

Blog PartImplementation OptionDescription
(2/7)HTTP Query ParamThis is the most basic module where the “role” is transferred as a HTTP Query Parameter. The server validates the role programmatically.
(3/7)Basic AuthenticationA user agent uses Basic Authentication to transfer credentials.
This blog
JWT
A JSON Web Token (JWT) codifies claims that are granted and can be objectively validated by the receiver.
(5/7)
OpenID and Keycloak

For further standardization, OpenID Connect is used as an identity layer. Keycloak acts as an intermediary to issue a JWT token.
(6/7)
Proxied API Gateway (3Scale)ServiceB uses a proxied gateway (3Scale) which is responsible for enforcing RBAC. This is useful for legacy applications that can’t be enabled for OIDC.
(7/7)
Service Mesh

All services are managed by a Service Mesh. The JWT is created outside and enforced by the Service Mesh.

What do we want to achieve in this blog part?

In this blog (part 4) we are going to introduce JSON Web Token to further improve on the security and ease-of-use for application developers. See below an architectural overview of the implementation that we want to establish.

What is a JSON Web Token?

JSON Web Token is an open industry standard (RFC 7519) used to share information between two or more entities. The token so-to-say codiefies all the associated rights (“claims”) which allows fine-granular access management. Moreover, as the JWT is signed by the producer, it generates explicit trust between the producing service, the bearer and the requested service.

We will use this token to embed the information about the role of the user and also (optionally) which service can be accessed with the token.

Prerequisites

We are using the following tools:

Code Base:

You can either:

  • continue from the previous blog and:
    • delete all extensions that are related to BasicAuthentication, e.g. “quarkus-elytron-security-properties-file” or “quarkus-security-jpa”
    • delete the corresponding entries in the application.properties file:

      quarkus.hibernate-orm.database.generation=drop-and-create
      quarkus.hibernate-orm.enabled=true
      quarkus.datasource.db-kind=postgresql
      quarkus.datasource.username=quarkus
      quarkus.datasource.password=quarkus
      quarkus.datasource.jdbc.url=jdbc:postgresql:identity_store

  • or clone the code base from here to have a clean start

Implementation

We will explain step-by-step how you can achieve multi-service RBAC with JWT. If you are only interested in the end result, you can clone this from git here.

Token Generation

  1. First, we need to add a quarkus extension (quarkus-smallrye-jwt) to both services. This is required for generation and validation of the JWT.
    • Add the following stanza to the pom-file:

      <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-smallrye-jwt</artifactId>
      </dependency>

    • or execute the command:

      mvn quarkus:add-extension -Dextensions=smallrye-jwt

  2. Now, we need to generate a JWT token. Again, Quarkus is very convenient to use for this task as it provides extensions to accomplish this task. But first, we need to have a key-pair (private/public) that we will use to sign the token.

    The easiest way to generate this key-pair is to use the openssl utility which will generate a public and private key file.

    openssl genrsa -out rsaPrivateKey.pem 2048 &&
    openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem &&
    openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem


  3. Place the privateKey.pem and publicKey.pem in the resources folder of the ServiceA project.



  4. Now, add a Java class that will generate the token programmatically. (Remark: You could also use a JWT generator like jwt.io). This class is only needed for token generation and not for actual communication flow.
package org.acme;

import java.util.Arrays;
import java.util.HashSet;

import org.eclipse.microprofile.jwt.Claims;

import io.smallrye.jwt.build.Jwt;

public class GenerateToken {
    /**
     * Generate JWT token
     */
    public static void main(String[] args) {
        String token =
           Jwt.issuer("https://example.com/issuer") 
             .upn("[email protected]") 
             .groups(new HashSet<>(Arrays.asList("users", "admins"))) 
             .claim(Claims.birthdate.name(), "2001-07-13")
             .expiresIn(3600)
           .sign();
        System.out.println(token);
    }
}Code language: JavaScript (javascript)

As you can see the JWT token will be created with some properties:

  • UserPrincipalName (“upn”): This is the user for which the token is generated

  • Groups: These are the groups that the user belongs to. (Please note that we will use groups in our example as a synonym for roles.)

  • Birthdate: This is just an example “claim” that is added to further describe the user. In fact you could add any kind of attributes to the JWT that could be then used by receivers of the JWT.

  • ExpiresIn: A token is usually associated with an expiration date. This is an important security feature to invalidate the token after a certain time-frame.

Testing the JWT with ServiceB

  1. Add for all the endpoints of ServiceA and ServiceB the appropriate RBAC annotations, e.g.

    @RolesAllowed("users")

  2. Call the main method to get a token generated and outputed:

    mvn exec:java -Dexec.mainClass=org.acme.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem

    This should output an alphanumeric string which is a JWT token with the specified properties. For ease of use, export the value of the token to an environment variable.



  3. (optional) Verify that the token has been properly generated and contains the correct values by decoding the token easily (e.g. jwt.io).



  4. Start both services in developer mode:

    mvn quarkus:dev

  5. Now, let’s test to call the end-point of ServiceB (port:9000):
    • without Token:

      curl http://localhost:9000/serviceB/userEP -v

      You should get a http 401 (“Unauthorized)

    • with the Token:

      curl -H "Authorization: Bearer $JWT_TOKEN" http://localhost:9000/serviceB/userEP -v

      You still get a HTTP 401 (“Unauthorized”)

      Why is this?

      Well, ServiceB can’t verify the validity of the token. We need to specify the public key that belongs to the key-pair which has been used to generate the JWT.

  6. Thus, we need to:
    • copy the public key to the resources folder of ServiceB
    • and add the following to the application.properties file:

      mp.jwt.verify.publickey.location=publicKey.pem
      mp.jwt.verify.issuer=https://example.com/issuer


      Let’s try to test the end-point again!

      Now, it should work!!

  7. Test the same with admin end-point. This should work exactly the same way, as we have included both roles (“users” and “admins”) in the JWT.

Congratulations! You have successfully generated and used a JSON Web Token to secure your application.

Testing the JWT with ServiceA

This is great, but does this also work with multiple services?

  1. Let’s test against the ServiceA endpoint. And it obviously will fail again, because also ServiceA needs to verify the validity of the JWT.

    curl -H "Authorization: Bearer $JWT_TOKEN" http://localhost:8000/serviceA/adminEP -v

  2. If we also add the same settings to the application.properties file then the request should work from ServiceA and ServiceB.

    mp.jwt.verify.publickey.location=publicKey.pem
    mp.jwt.verify.issuer=https://example.com/issuer

How to limit the audience of the JWT

Per default, the JWT can be used to access all services. But this might pose additional security threats. Thus, it is common to explicity specify the audience of the token. How can this be accomplished?

  1. For the services, specify an additional attribute in application.properties to verify the audience of the token:

    mp.jwt.verify.audiences=myservices

  2. Now, test again. It should fail as the JWT doesn’t contain yet this audience claim.

  3. Thus, we need to adapt our token generator class to add an audience claim (in bold):

    .claim(Claims.birthdate.name(), "2001-07-13") .claim(Claims.aud.name(),"myservices")
    .expiresIn(3600)


  4. Generate a new JWT as specified above and check that the additional claim is included there.

  5. Now, test again.
    • If the two values from the JWT token and the application.properties setting (“mp.jwt.verify.audiences”) match:

      HTTP 200

    • Now try to change the audience to something different (e.g. anyService)

      You should get a http 403 (“Unauthorized”)

How to access attributes of the JWT

In order to validate that the receiver easily can de-codify the attributes from the JWT, we just need to add some statements to the quarkus code:

  1. Inject a jwt object in the GreetingResource class:

    @Inject
    JsonWebToken jwt;


  2. Now, you can just get some values from the jwt object, e.g. the name of the user or even custom claims that we have included in the JWT like the birthdate:

    return "Your name is " + jwt.getName() + " and you are born on " +jwt.getClaim("birthdate").toString();

  3. Test it. It should return:

    Your name is [email protected] and you are born on 2001-07-13


Congratulations! You have mastered the JWT concept and have applied it successfully to multi-service applications.

Conclusion

JWT is a convenient and secure way to codify the access rights, particularly in multi-component architectures. The beauty of this approach is that the services don’t have to care about credentials, the JWT can be specified with short expiration time to minimize the impact of and it is a standard that many programming languages support.

But there are still downsides:

  • The generation of the JWT is rather complicated
  • All consuming services need to have access to the Public Key with which the JWT was generated

Thus, a 3rd party component that acts as a trusted source and mitigates these downsides will be introduced in the next blog (part 5).

Common pitfalls:

  • Check whether the JWT has been properly generated and contains all the attributes (“claims”)
  • Make sure that the private and public Key are available in the right directory
  • Make sure that the token is still valid (in the code the expiration is set to 3600 sec = 1 hour)

Leave a Reply

close

Subscribe to our newsletter.

Please select all the ways you would like to hear from Open Sourcerers:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our newsletter platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.