Quantcast
Channel: Blog – Stormpath User Identity API
Viewing all articles
Browse latest Browse all 278

Tutorial: Establish Trust Between Microservices with JWT and Spring Boot

$
0
0

If you’ve never heard of JWTs (JSON Web Tokens), well then you didn’t read my last post on CSRF Protection with JWTs. To briefly recap: JWTs can be used wherever you need a stand-in to represent a “user” of some kind (in quotes, because the user could be another microservice). And, they’re used where you want to carry additional information beyond the value of the token itself and have that information cryptographically verifiable as security against corruption or tampering. In this post, we’ll talk about using JWTs to establish trust between microservices.

For more information on the background and structure of JWTs, here’s the IETF specification.

The code that backs this post can be found on GitHub. The example application makes use of the JJWT library. This Java JWT library has over 1000 stars on Github. As you’ll see further on in this post, the library has a very readable and easy to use fluent interface, which contributes to its popularity.

Let’s begin at the beginning.

What are Microservices?

If you ask 10 different people the question, you might get 10 different answers. One of my favorite treatments on the subject is from Martin Fowler. In short, it’s “componentization via services” (direct quote from the article). By that, we mean identifying discrete business capabilities and exposing them as standalone services that can be scaled independently.

In the distant past of 3 – 5 years ago, we had monolithic service oriented architectures:

monolithic-soa-2

If, say, your AuthenticationService started to get bogged down, it would make your GroupService unresponsive as well. Sure, you could deploy a copy of your big beefy server and put a load balancer in front of them, but that’s a lot of horsepower to throw at one overloaded service.

What if we bust up our big beefy server into a number of smaller ones, each responsible for a piece of the architecture, like this:

microservices-2

Now if the AuthenticationService bogs down you can scale it independently. Maybe you’ll need 10 small instances of the AuthenticationService and only 2 instances of all the other services. Even better: if I’ve already authenticated (in this example), I can go right to the GroupService.

But, with all this awesomesauce, we’ve introduced a new problem: all of these independently running microservices need to communicate with each other and they need to do so in a secure manner.

We can use JWTs to not only carry information between microservices, but by the very nature of JWTs we can cryptographically verify the signature, proving that they have not been tampered with.

JWTs and Microservices in Action

Let’s fire up some microservices and see communication between them in action. This example exposes an API to demonstrate communication between microservices.

Execute the following to build the example:

git clone https://github.com/stormpath/JavaRoadStorm2016
cd JavaRoadStorm2016
cd roadstorm-jwt-microservices-tutorial
mvn clean install

Now, let’s run two instances of the example. This will serve as our simulated microservices environment:

target/*.jar --server.port=8080 &
target/*.jar --server.port=8081 &

Let’s see the “happy path” in action – we’ll exercise some endpoints to show that a microservice trusts itself. We can do this using a command-line http tool. My personal favorite is HTTPie:

http localhost:8080/test-build


HTTP/1.1 200
...


{
    "jwt": "eyJraWQiOiI4YjA3NzhkOC01MGJiLTQ1ZjAtYmYyMC1lYzg0ZDU3NTQ4NWYiLCJhbGciOiJSUzI1NiJ9...",
    "status": "SUCCESS"
}

http localhost:8080/test-parse?jwt=eyJraWQiOiI4YjA3NzhkOC01MGJiLTQ1ZjAtYmYyMC1lYzg0ZDU3NTQ4NWYiLCJhbGciOiJSUzI1NiJ9...


HTTP/1.1 200
...


{
    "jwsClaims": {
        "body": {
            "exp": 4622470422,
            "hasMotorcycle": true,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "sub": "msilverman"
        },
        "header": {
            "alg": "RS256",
            "kid": "8b0778d8-50bb-45f0-bf20-ec84d575485f"
        },
        "signature": "..."
    },
    "status": "SUCCESS"
}

The first command spits out a JWT. The second command parses the JWT passed in. The build operation uses the microservice’s auto-generated private key to sign the JWT. And, the parse operation uses the matching public key to verify the signature.

Now, let’s repeat the parse command, but this time, against our second microservice – the one running on port 8081:

http localhost:8081/test-parse?jwt=eyJraWQiOiI4YjA3NzhkOC01MGJiLTQ1ZjAtYmYyMC1lYzg0ZDU3NTQ4NWYiLCJhbGciOiJSUzI1NiJ9...


HTTP/1.1 400
...


{
    "exceptionType": "io.jsonwebtoken.JwtException",
    "message": "No public key registered for kid: 8b0778d8-50bb-45f0-bf20-ec84d575485f. JWT claims: {iss=Stormpath, sub=msilverman, name=Micah Silverman, hasMotorcycle=true, iat=1466796822, exp=4622470422}",
    "status": "ERROR"
}

Here we see that our 8081 microservice can’t parse the JWT. Trust has not been established between the two microservices.

Public Key Infrastructure and JWT

Now’s a good time to take a step back and look at how these JWTs are built and parsed in this example.

When the Spring Boot application is first started, the microservice creates a key-pair for itself. That is, it creates a private key and a public key. Every JWT that’s created from the example API is signed using the microservice’s private key. The public key is then used to verify the signature. This uses the RSA crypto libraries provided by java and supported by the JJWT library. Here’s a great article on the inner workings of RSA crypto.

Here’s the code that creates the key pair when the microservice starts:

public PublicCreds refreshMyCreds() {
    myKeyPair = RsaProvider.generateKeyPair(1024);
    kid = UUID.randomUUID().toString();


    PublicCreds publicCreds = getMyPublicCreds();


    // this microservice will trust itself
    addPublicCreds(publicCreds);


    return publicCreds;
}

Notice that in addition to the key pair, we are creating a unique key ID. This will become important later.

Build and Sign a JWT

The /test-build endpoint is defined in the SecretServiceController. It’s job is to create a JWT with some hard-coded custom and registered claims. It then signs the JWT using the microservice’s secret key. Let’s jump into the code:

@RequestMapping("/test-build")
public JWTResponse testBuild() {
    String jws = Jwts.builder()
        .setHeaderParam("kid", secretService.getMyPublicCreds().getKid())
        .setIssuer("Stormpath")
        .setSubject("msilverman")
        .claim("name", "Micah Silverman")
        .claim("hasMotorcycle", true)
        .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))   // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
        .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L))) // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
        .signWith(
            SignatureAlgorithm.RS256,
            secretService.getMyPrivateKey()
        )
        .compact();
    return new JWTResponse(jws);
}

The JJWT library uses a modern fluent interface along with the builder pattern and method chaining. Line 3 kicks us off with a static method call that returns a JWT Builder object to us. Each successive method call adds to our JWT configuration until finally the compact method is called, which returns the resultant signed JWT in its string form.

On line 4, we set the public key id as a header param on the JWT (that will become important in just a bit).
On line 11, we sign the JWT with this microservice’s private key.

If you look at the resulting JWT in a handy tool like (shameless plug) jsonwebtoken.io, you’ll see:

{
 "typ": "JWT",
 "alg": "RS256",
 "kid": "cb5beb41-440d-4d14-9c6b-66199029ce19"
}

and

{
 "iss": "Stormpath",
 "sub": "msilverman",
 "name": "Micah Silverman",
 "hasMotorcycle": true,
 "iat": 1466796822,
 "exp": 4622470422
}

Parse and Verify a JWT

Next, let’s take a look at the code that backs the /test-parse endpoint. There’s some real JJWT magic happening here:

@RequestMapping("/test-parse")
public JWTResponse testParse(@RequestParam String jwt) {
    Jws jwsClaims = Jwts.parser()
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JWTResponse(jwsClaims);
}

Notice that this entire method is basically 3 lines.

Line 3 is a call to a static method to get us a JWT Parser Builder object.

Line 5 actually parses the incoming JWT string. Per the JWT spec, if the JWT is a JWS (signed JWT), the parser must verify the signature.

That’s where little old line 4 comes in. It looks so small and unassuming. But, it packs a wallop into one line of code.

It may not seem obvious, but there’s a chicken-and-egg problem here. We need a key in order to parse the JWT. In order to lookup the key, we need to examine the information in the header. But, we don’t yet know if we can trust that this JWT hasn’t been tampered with. You see? Chicken and egg.

The JJWT addresses this by using a SigningKeyResolver interface. This enables us to choose (resolve) a key in-flight as it were while we are parsing. Let’s look at the code from the SecretService and see what’s going on.

private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
    @Override
    public Key resolveSigningKey(JwsHeader header, Claims claims) {
        String kid = header.getKeyId();
        if (!Strings.hasText(kid)) {
            throw new JwtException("Missing required 'kid' header param in JWT with claims: " + claims);
        }
        Key key = publicKeys.get(kid);
        if (key == null) {
            throw new JwtException("No public key registered for kid: " + kid + ". JWT claims: " + claims);
        }
        return key;
    }
};

public SigningKeyResolver getSigningKeyResolver() {
    return signingKeyResolver;
}

On line 1, we are using a SigningKeyResolveAdapter. This is the common pattern of using an adapter that has empty implementations for all the methods in the interface so that we can implement only those methods we care about. In this case, the one method we are overriding is the public Key resolveSigningKey(JwsHeader header, Claims claims) method. Notice that the method gets the JwsHeader and the Claims objects passed in. And, the method will return a Key, which could be null if it’s unable to be resolved. This allows us to safely examine the header and possibly the claims while we are in the middle of parsing the JWT.

Now, I promised you I’d explain the use of the kid in the header and we’ve arrived at that moment! In this “poor man’s” key manager, each registered public key is stored in a Map identified by the unique kid. This enables the microservice to establish trust with itself and other microservices by adding public keys to the collection. As we touched on before, when the microservice starts up and generates its keypair, it immediately registers its own public key so as to trust itself when parsing JWTs signed with its own provate key.

Line 4 attempts to get the kid header parameter. If it there is no kid an exception is thrown. Earlier, when we tried to have our second microservice parse a JWT from the first microservice, we made it past this hurdle as there was a kid in the header.

Line 8 attempts to retrieve the public key that matches the private key used to sign the JWT based on the kid value it found. If it’s not able to find the public key, then an exception is thrown. It’s here that our attempt to parse the JWT failed earlier. In the next section, we’ll see how we can register the public key from one microservice with another, thereby establishing trust.

Assuming a key was found in the collection, it is returned from the method which in turn allows the JWT Parser to verify the signature and complete parsing the JWT.

Establish Trust Between Microservices

Now that we’ve seen the mechanism by which JWTs are verified and parsed, let’s look at how we can establish trust between microservices.

The SecretServiceController exposes two additional endpoints: /get-my-public-creds and /add-public-creds. The first endpoint outputs a base64-urlencoded version of the microservice’s public key. This is safe to do as this type of key is meant to be distributed publicly. You could tweet it and include it in your email signatures and that would be just fine.

http localhost:8080/get-my-public-creds

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 03:06:36 GMT
Transfer-Encoding: chunked

{
    "b64UrlPublicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC-...",
    "kid": "06adff6d-1351-4e27-8ab5-7a9fc837ad34"
}

Note: the b64UrlPublicKey in the output is NOT a JWT. It is simply a text based version of the binary public key.

The b64UrlPublicKey and kid can be sent to the other microservice after which that microservice will be able to verify JWTs from the first microservice. This is what I mean by establishing trust. Before, the second microservice very literally could not verify the signature of a JWT from the first microservice as it didn’t have it’s public key on record. Here’s what this looks like:

http POST localhost:8081/add-public-creds \
b64UrlPublicKey=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC-... \
kid=06adff6d-1351-4e27-8ab5-7a9fc837ad34

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 03:12:21 GMT
Transfer-Encoding: chunked

{
    "b64UrlPublicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC-...",
    "kid": "06adff6d-1351-4e27-8ab5-7a9fc837ad34"
}

The method that backs the /add-public-creds endpoint responds with a 200 status and spits back the encoded public key and kid to indicate that the public key was successfully added to its internal collection.

Now, let’s try to take the JWT from our first microservice and use the second microservice to parse it once again.

http localhost:8081/test-parse?jwt=eyJraWQiOiIwNmFkZmY2ZC0xMzUxLTRlMjctOGFiNS03YTlmYzgzN2FkMzQiLCJhbGciOiJSUzI1NiJ9...

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 03:19:53 GMT
Transfer-Encoding: chunked

{
    "jwsClaims": {
        "body": {
            "exp": 4622470422,
            "hasMotorcycle": true,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "sub": "msilverman"
        },
        "header": {
            "alg": "RS256",
            "kid": "06adff6d-1351-4e27-8ab5-7a9fc837ad34"
        },
        "signature": "QzR95gK9ly3Cr6hB-5OK-YHDUL2WbP1geG2m5oGH0IfSH8Z-..."
    },
    "status": "SUCCESS"
}

Now that trust has been established (by way of registering the first microservice’s public key), the second microservice is able to properly parse the JWT from the first microservice.

JWTs for Trust and Data

So far, we’ve been looking at some test endpoints that build and parse JWT’s. The application in this example exposes a couple of other endpoints that simulate more realistic microservices communication.

The /account-request endpoint of one microservice takes some search parameters and generates a JWT that can be passed to another microservice. Here’s where the real value of JWTs comes into play. The JWT is used both as a token to prove identity and carries additional information encoded into it that the receiving microservice can use to perform some action.

The AccountService has some hardcoded dummy account information to simulate a service that might query against a database for accounts. It also expects that there’s a JWT set in the standard Authorization header using the Bearer scheme.

Let’s take a look at this in action:

http localhost:8080/account-request userName=anna

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 04:26:23 GMT
Transfer-Encoding: chunked

{
    "jwt": "eyJraWQiOiIwNmFkZmY2ZC0xMzUxLTRlMjctOGFiNS03YTlmYzgzN2FkMzQiLCJhbGciOiJSUzI1NiJ9...",
    "status": "SUCCESS"
}

The jwt value can now be passed over to our second microservice which is acting as our account service:

http localhost:8081/restricted \
Authorization:"Bearer eyJraWQiOiIwNmFkZmY2ZC0xMzUxLTRlMjctOGFiNS03YTlmYzgzN2FkMzQiLCJhbGciOiJSUzI1NiJ9..."

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 04:30:37 GMT
Transfer-Encoding: chunked

{
    "account": {
        "firstName": "Anna",
        "lastName": "Apple",
        "userName": "anna"
    },
    "message": "Found Account",
    "status": "SUCCESS"
}

This indicates that, first, the microservice was able to validate the incoming JWT and second, the microservice was able to use the information contained in the JWT to lookup an account.

If we don’t include the Authorization header at all, the response looks like this:

http localhost:8081/restricted

HTTP/1.1 401
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 04:29:36 GMT
Transfer-Encoding: chunked

{
    "exceptionType": "com.stormpath.tutorial.exception.UnauthorizedException",
    "message": "Missing or invalid Authorization header with Bearer type.",
    "status": "ERROR"
}

And, by the way, the JWT expires in one minute. A short expiration is good practice for microservice-to-microservice communication. Here’s what happens if we try to do the same search with an expired JWT:

http localhost:8081/restricted \
Authorization:"Bearer eyJraWQiOiIwNmFkZmY2ZC0xMzUxLTRlMjctOGFiNS03YTlmYzgzN2FkMzQiLCJhbGciOiJSUzI1NiJ9..."

HTTP/1.1 400
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Wed, 07 Dec 2016 04:29:21 GMT
Transfer-Encoding: chunked

{
    "exceptionType": "io.jsonwebtoken.ExpiredJwtException",
    "message": "JWT expired at 2016-12-06T23:27:23-0500. Current time: 2016-12-06T23:29:21-0500",
    "status": "ERROR"
}

Note: There is no additional code for you to write to enable this behavior. This is one of the great things about the JWT spec and the JJWT library: a compliant parser must fail parsing a JWT that has an exp claim and that exp claim has a value in the past.

Bonus: Ditch HTTP/1.x for More Scalable Microservices

We’ve come a long way on our JWT and microservices communication journey! This last section is a bonus that’s more about modern microservice architecture than about JWT specifically.

So far, all of our examples have been interactions with HTTP. The example project is a Spring Boot application that naturally and easily “speaks” HTTP. However, modern microservices demand a more performant architecture. Why is HTTP less performant? It comes down to the synchronous nature of HTTP/1.x. That is, you make a request and wait around for a response. While it’s great for our web browsers, it does not scale well as a microservices architecture.

A better architecture that does scale very well is one based on asynchronous messaging. One of the more popular messaging servers is Apache Kafka. While it’s written in Java, interacting with it in Java is not a requirement. This is a pub/sub system. That is, publish/subscribe. Producer clients can publish messages to Kafka and Consumer clients can read messages from Kafka. The produce and consume operations are completely independent of each other.

The example project is set up such that one microservice can behave as a producer and a second microservice can behave as a consumer. This is accomplished first by running Kafka and second by including the appropriate properties to the microservices at startup.

An extensive dive into configuring kafka is outside the scope of this post. However, all you need to get the sample code working with Kafka is to follow their quickstart. Once you’ve downloaded Kafka, you’ll start Zookeeper and Kafka like so:

~/local/kafka_2.11-0.10.0.1/bin/zookeeper-server-start.sh ~/local/kafka_2.11-0.10.0.1/config/zookeeper.properties

~/local/kafka_2.11-0.10.0.1/bin/kafka-server-start.sh ~/local/kafka_2.11-0.10.0.1/config/server.properties

Now, we can fire up our example as before. This time, however, we will enable the first microservice to work as a message producer and the second microservice to work as a message consumer.

target/*.jar --server.port=8080 --kafka.enabled=true

target/*.jar --server.port=8081 --kafka.enabled=true --kafka.consumer.enabled=true

Note: You’ll need to establish trust between these microservices as before using the /get-my-public-creds and /add-public-cred endpoints.

This time, we’ll perform our account lookup using the /msg-account-request endpoint. This will:

    1. Respond with a JWT over HTTP
    2. Produce a message with the same JWT
    3. Publish it to Kafka

We should see our consumer microservice automatically read and parse the incoming JWT message and log some output. Let’s check it out:

http http://localhost:8080/msg-account-request userName=anna

This results in the same HTTP response as before. However, if we flip over to our second microservice, we should see this in the log output:

2016-12-07 00:20:25.631  INFO 12373 --- : record offset: 0, record value: eyJraWQiOiIwMjQwYWEyMy0xMjZlLTQ3MDctOWZjYy0zODE2YzBhZGEyMmYiLCJhbGciOiJSUzI1NiJ9...
2016-12-07 00:20:25.657  INFO 12373 --- : Account name extracted from JWT: Anna Apple

It may not look like much, but behind the scenes, a JWT was created with our search terms and sent to Kafka as a message. Here’s the code that made that happen:

@RestController
public class MessagingMicroServiceController extends BaseController {

    @Autowired(required = false)
    SpringBootKafkaProducer springBootKafkaProducer;

    private static final Logger log = LoggerFactory.getLogger(MessagingMicroServiceController.class);

    @RequestMapping("/msg-account-request")
    public JWTResponse authBuilder(@RequestBody Map claims) throws ExecutionException, InterruptedException {
        String jwt = createJwt(claims);

        if (springBootKafkaProducer != null) {
            springBootKafkaProducer.send(jwt);
        } else {
            log.warn("Kafka is disabled.");
        }

        return new JWTResponse(jwt);
    }
}

The main method of our Spring Boot application sets up the microservice as a Kafka consumer is the properties are set properly.

public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(JJWTMicroservicesTutorial.class, args);

    boolean shouldConsume = context
        .getEnvironment()
        .getProperty("kafka.consumer.enabled", Boolean.class, Boolean.FALSE);

    if (shouldConsume && context.containsBean("springBootKafkaConsumer")) {
        SpringBootKafkaConsumer springBootKafkaConsumer =
            context.getBean("springBootKafkaConsumer", SpringBootKafkaConsumer.class);

        springBootKafkaConsumer.consume();
    }
}

The consumer code has some more setup to it. You can see it in the JJWTMicroservicesTutorial.java file in the example project.

JWTs for Fun and Profit

In this post, we’ve seen the value of JWTs in establishing trust between microservices. The primary benefits are:

  • Verifiability — You have a high degree of confidence that a JWT has not been tampered with when the signature can be verified.
  • Automatic time stamp checks — When you have certain claims set in your JWT, such as exp, a spec compliant parser must fail if the relevant time-test is not met.
  • Additional encoded information — Aside from registered claims, you can include custom claims in your JWT. Unlike dumb tokens, this allows for meaningful data to be passed within the JWT.

While there are no panaceas in tech, JWT goes a long way to solving multiple challenges at once: Securing communication between microservices and passing data between microservices all at once.

Learn More About JWTs in Java

Interested in what else you can do with JWTs in Java? Well, we’ve got some awesome resources to salve yoru curiosity:

And as always, if you have any questions hit me up in the comments or on Twitter @afitnerd.

The post Tutorial: Establish Trust Between Microservices with JWT and Spring Boot appeared first on Stormpath User Identity API.


Viewing all articles
Browse latest Browse all 278

Trending Articles