This is a two-part article where we are going to analyze an option to secure RESTful web services through the JWT standard, also analyzing the option to use public keys that can be rotated to increase the security model.
In this first part we will analyze the standard for JWT and JWK from a theoretical standpoint. Our part two of the article will focus on a simple practical example about how to implement this security standard to protect your own microservices.
In the contemporary environment of software development, securing microservices has become increasingly important. Since RESTful microservices run over HTTP protocol, anyone having access to the web API exposed by a microservices application can invoke its services simply using a web browser, or even more sophisticated invocations can be done using tools like Postman. This is mainly the reason to have a robust security mechanism protecting and controlling the access to any service within an organization infrastructure.
Securing RESTful microservices involves not only the implementation of security mechanisms at the server side, but also web clients must be able to adjust its own requests to meet the security requirements configured by such microservice application.
Introduction to JWT, JWS and JWK
In general, JSON Web Tokens (JWT) consist on a security mechanism to transfer information between two parties by using JSON-format tokens that are self-signed. As such, a consumer of JWT can perform JWT signature verification, which is a cryptographic process to verify the authenticity of the information contained in the token, as well as validations of the information within the token payload.
JWT consists of three JSON format parts:
When working with JWKS, JOSE header will specify the following attributes:
- encryption algorithm – alg Declared with value “RS256” (asymmetric encryption algorithm, described at this specification)
- Key id – kid This attribute contains the identifier for the public key that must be used to perform signature verification.
2. Token payload. It consists on a set of key-value attributes commonly referred to as “Claims”, where the attribute name and value are called “Claim Name” and “Claim value”. Claims are ultimately the lowest level of information contained in the JWT, and can be used by the resource owners to determine any kind of information about the user who owns the token (access level, identity, authorization, etc.).
3. Token signature – JWS. From the security standpoint, this is the most important part of a JWT, as it contains the token signature that must be used to perform the verification of the token. Token signature is the result of taking the token payload and apply RS256 encryption using the private key of the RSA key pair.
All three parts of the JWT are BASE64 encoded, which makes the JWT URL-encoded, this means it can be shared between two or more parts using HTTP.
As mentioned in the first bullet above, JWKS is based on the premise of using asymmetric encryption, which consists on having a private key utilized to encrypt the token payload, and the capability to use the corresponding public key to perform the decryption of the payload. Private and public keys are referred to as “Key pair”. This way, an Authorization server can generate a signed token and the trusted part can perform token verification using the corresponding public key. This provides an additional layer of security, as only the owner of the key pair can generate tokens signed with the appropriate private key.
Additionally, JWKS also provides the capability of performing “Key rotation”, this means the Authorization server can constantly update its own set of Key Pairs. This is a second layer of security, as the RSA Key Pair do not remain static, and in an extreme case of someone stealing the private key, such Key will be expired eventually by the Authorization Server, on a reasonable frequency of time.
In our second part of this post, we will dive into the implementation of JWKS (specification available here), that can be used to secure the communication between two or more systems, like internal microservices running on an organization’s infrastructure, or two systems that belong to different organizations.
JSON Web Key & JSON Web Key Set
JWK consists on a JSON data structure that represents a cryptographic key. The members of the object represent properties of the key, including its value. A JWK Set is simply JSON data structure that represents a set of JWKs.
There are attributes that are mandatory within JWK, regardless on the signing algorithm (formally known as JWA). Such attributes are:
- kid (Key ID) Parameter is used to identify a key, with the purpose to choose among a set of keys within a JWK Set during key rollover. kid parameter is utilized to lookup the appropriate public key, as it is also included in the JWT JOSE header.
- kty (Key Type) Parameter. This attribute identifies the cryptographic algorithm family used with the key, such as “RSA” or “EC”. This attribute is required. As mentioned before, this document is based on the premise that the readers are interested in working with RSA algorithm.
- use (Public Key Use) Parameter. This parameter identifies the intended use of the public key. The “use” parameter is employed to indicate whether a public key is used for encrypting data or verifying the signature on data. This attribute is optional, although encouraged.
- Values defined by this specification are:
- sig (signature)
- enc (encryption)
- Values defined by this specification are:
- exp (Expiration) Parameter. Although “exp” is not mentioned in the JWK specification, it is widely used in the same way as described in the JWT specification, which can be found here. Its purpose is to define the expiration time for the given JWK. “exp” MUST be a number containing a NumericDate value.
There are more optional parameters that can be found at the JWK specification.
In addition to the previous common parameters, JWK allows the definition of algorithm specific parameters. When working specifically with RSA public keys, the following attributes can be used to describe such type of key:
- n Parameter is used to define the modulus for both the public and private keys. Its length, usually expressed in bits, is the key length.
- e Parameter is used to define the RSA public exponent.
Both attributes are required to perform RSA algorithm during JWS verification. Details about the RSA encryption algorithm can be found at this specification.
General Flow to Implement JWK Set
The following diagram depicts the flow to work with JWK set URL when it is available in an Authorization server.
The sequence of steps happen in the following way:
1. End user performs Authentication to the user-facing application (usually and Identity provider is involved in this process).
2. Once authenticated, end user can request access to certain resource(s), it can be any kind of information or rich content.
3. User-facing application can request an access token that will serve to request the external resource in representation of the end user, so the resource server can determine who is requesting the information (authentication) and the access level of that user (authorization). On this step an Oauth2 Access token request is performed. Oauth2 Authorization Framework is described at this specification.
4. Authorization server will validate the request for an access token. The end-user application can provide client credentials to prove identity of the invoker, as well as a hint to determine the user who triggers the process. Other possibility is to perform an Oauth 2 token exchange, where a user-facing application would provide an oauth 2 token that encapsulates information about the user who authenticated to the application. Such flow is described at this specification.
In addition to validate the user and the request, Authorization server can optionally gather more user details like internal roles, assignments or even user profile, anything that could be helpful for resource servers to determine if a user can or cannot access the requested resource. Since we are working with JSON, these attributes can be nested in this format, as required by the implementers.
Another important flow in this step is the ability of the Authorization server to generate a new RSA key pair, whose private key will be utilized to create the JWT signature, while the public key will be stored and published so any resource server who receives the JWT can look up the public key and perform signature verification.
5. User facing application receives the JWT access token, attaches it to a new HTTP request as an Authorization header, as a bearer token, and invokes the resource server to attempt retrieval of the external resource.
6. Resource server detects that a Bearer token was included in the new request received, decodes the JOSE header in order to find out the JWK key id, then connects to the authorization server JWK set URL in order to retrieve the list of available public keys, then filters the key with id indicated in the JOSE header. Once the JWK is found, Resource server just must perform signature verification to determine if the JWT received is valid. There are a few additional validations that must be performed, but those can be analyzed (aud, exp) in the JWT specification.
7. Once JWT is verified, the payload is considered as valid, and any information within the payload can be used by the resource server to determine whether the requested resource can be delivered in the request received or not.
There are several use cases that can be identified in the flow described. The next sections will dive into them and a provide brief guidelines about how they can be implemented in a Java-based platform. However, since this document focuses on JWK set, we will avoid details about user authentication and the process to validate user identity when a JWT access token is requested to the Authorization server.
On to part II, where we will analyze a practical example to implement the flow described before, using the standards described in the specifications.
In the first part of this article, we analyzed the standards and concepts for JWT and JWK that can be leveraged to protect RESTful microservices. Now we will use the concepts described to implement a practical example based on the flow also described before. The following sections describe the stages to implement such flow.
Authorization Server: Generate RSA key pair and store public JWK
Authorization server must contain a logic component to provide RSA key pairs that can be used to perform JWS, as well as storing the public JWK, so it can be retrieved in further use cases to expose such JWK through an open endpoint.
Let us define such services and the guidelines to implement them.
1. Service: JwkLocalService. Implementation of this service will manage a cache of KeyPair objects, which will be empty at the beginning and will be populated as soon as new key pairs are requested. Implementers can decide how many Key Pairs can be kept in memory at runtime. In the current example we are using 2 key pairs so there can be diversity of RSA keys used to sign different JWT. Cache mechanism can be based on JSR 107 specification. In this case a custom caching mechanism is used. This service will contain the following methods:+ Optional<KeyPair> getKeyPair(). This method will populate the local cache of KeyPairs, then pick any of the available Key pairs and return it. A simple implementation can be:
+ List<JwkExposedModel> findAll(). This method will use the repository dependency to retrieve all the JWKExposedModel In this example, the repository and storage mechanism are able to manage expiration of objects internally, so the service doesn´t have to filter out the public JWK objects returned by the repository (which are already expired). However, an additional layer could be implemented to filter out any JWK considered expired, according to the expiration time of each KeyPair. In the current example, our repository stores the objects in Redis, using Spring data Redis and TimeToLive annotation, described at this technical reference. Please notice that exposed model includes only the public key, while private key is kept on the application memory, described in the following bullet.
2. Wrapper class: KeyPairContainer. As noticed in bullet 1.a. a wrapper class is used to contain the KeyPair objects. This container could be replaced by a JSR 107 implementation, but since this example uses this custom caching mechanism, the wrapper class is described now. The purpose of this wrapper is to determine when a local KeyPair object has expired, based on the parameter received when creating the object that includes the private RSA key. Its only purpose is to allow the JwkLocalService to determine when a keypair should be removed from the local cache and replaced with a fresh key pair object. The wrapper class is necessary to hold the private key within the application memory, as it is not necessary to store it anywhere, since it will rotate frequently.
+ Boolean isExpired(). This method uses the parameter received in the constructor of the class to determine if at the current timestamp, the key pair is expired. The following code shows an implementation of this behavior
The following code shows the implementation of this behavior:
Authorization Server: generate JWT
One Authorization server can generate, cache and expire (rotate) its own set of key pairs. The next step is to actually generate the JWT for a given client. As mentioned before, retrieving a JWT can be part of several security flows. Regular Oauth2 access token request (working with JWT as Oauth2 tokens), performing Oauth2 token exchange, working with Open ID Connect flow, or even a custom flow where security is based on JWT between systems who have a previous handshake mechanism. This part of the flow is kept out of the scope of this document.
The following code snippet shows the code to generate a JWT after the HTTP request for a JWT access token is validated. This method is part of the “JwkLocalService” class described before.
The code snippet above shows the process to generate the RSA algorithm that is used to sign the new JWT token. return statement shows the process to generate the token. Any custom claims can be added to the token, like user roles or permissions, user profile, and any other information required.
Additionally, please notice how RSA Key pair is retrieved from the same service using the method described before, called getKeyPair, which internally manages available keypairs in memory and stores corresponding public keys to Redis using the repository object dependency as described before. Such repository and storage mechanism will be referenced in the next section.
Authorization Server: expose public JWK through open endpoint
Now that JWT is provided to resource servers, our authorization server must expose the public key through an open endpoint, we will achieve this by using a Spring MVC Controller called JwkSetController, the following is a simple implementation for such controller class.
As you can notice, the controller class utilizes the dependency jwkLocalService, whose implementation was analyzed previously, specifically it uses its method findAll to retrieve all the Jwk objects that contain the public JWK model that will be available for external consumers to retrieve a specific JWK and perform signature verification. Something to notice in this part of the integration is, the response format must follow the standard defined at the specification for JWT set URL that can be found here additionally, an example of public JWK can be found at the appendix A of the same specification.
Resource Server: decode JOSE header
After a system retrieves a JWT issued by the authorization server, such server can use JWT as a Bearer token, since it would be a valid token issued by the trusted IDP participant. Target resource server should be able to verify the signature of the token, read the payload within the token and use the information to perform any custom applicable validation process in order to determine if the request for a given resource can be satisfied according to the user who is requesting the resource, whose information is contained within the JWT payload.
In order to be verify the JWT received, a resource server must read the JOSE header, looking for the JWK identifier that was used to sign the token, and then lookup such JWK form the authorization server known endpoint. The following code snippet shows a simple implementation of this flow, using the library java-jwt.
The method above returns an instance of “com.auth0.jwt.JWTVerifier”, which can be used to perform a JWT verification. The first line of this method decodes the token JOSE header and retrieves the attribute “kid”, that corresponds to the JWK key identifier. After this, the JWK set URL is defined as a static string, but it could be stored within the application environment properties or retrieved from database, depending on the complexity of your own implementation. Once we have the JWK set URL, the code us using another dependency called “jwkService”, which will be analyzed in the next section.
“jwkService” will basically return the JWK public model and it is finally used to create the algorithm and verifier objects that can be leveraged later to perform the signature verification.
Resource Server: lookup public JWK
This step within the resource server is required, as explained in the previous section, to provide access to a given JWK by specific key identifier (kid attribute). Its basic implementation is shown in the next code snippet.
The service uses java-jwt class: com.auth0.jwk.UrlJwkProvider.UrlJwkProvider(URL) to perform JWK retrieval, but it also includes an additional layer to cache the JWK models. This helps the resource server application to improve performance by omitting several retrievals of public JWKs, as the public key is not supposed to change once it is issued by the Authorization server. This custom caching mechanism can be enhanced by using any caching implementation mechanism based on JSR 107 specification. exp attribute within the JWK model is optional, but if present, it defines the time in seconds when the key must be considered expired and shouldn´t be required by any participant to perform further JWT signature verification. Authorization servers could issue a new JWK with a repeated identifier after the first JWK has expired.
Resource Server: perform JWT signature verification
Finally, the last part of the flow is to perform the signature verification using the public JWK model that was retrieved form the Authorization server and the JWT that was received as Bearer token.
Let´s look at the following code snippet to analyze how we can achieve this.
One more time, this method is using the java-jwt API to perform the JWS verification. After the invocation to verifier.verify(jwtStr); we can obtain one out of two possible results:
- If the token signature is verified successfully using the public JWK encapsulated within the verifier object, the following line will continue its execution and no exception is thrown.
- If the token signature verification fails, an exception is thrown by this line and we can decide how to proceed, but basically this would mean that the received JWT is not valid.
JWT not being valid can be caused by several factors, like token is now expired, or the token was created using an invalid RSA private key or it could not be a valid JWT at all! (it could have been modified by the external party.
JWK set URL can be leveraged in several security flows. Personally, I have used it to improve secure communication between microservices.
The main advantage of using JWT is that token verification can be performed by the application itself, and only needs the public key model, which can also be cached to enhance the performance of this verification process.
JWT allows us to include any additional information within the token, although we must be careful with adding sensitive information, as JWT is not encrypted, it is only Base64 encoded, and anyone can decode the token payload and read the information within the token, so avoid including identifiers or sensitive information.
JWK set URL also allows us to implement “key rotation“, by expiring RSA key pairs and issuing new ones when needed. This is an additional security layer, as it would make harder to generate fake JWT using a stolen private key, since they are frequently rotated.
The examples provided in this document can be used to implement an actual flow of JWK set url and perform JWS verification in any application that received the JWT.