Distributed system authorization using JWT
A very common challenge for application developers is knowing which users are currently using the system or logged in on multiple devices to prevent unauthorized access based on permissions the user has.
JWT stands for JSON Web Tokens and is an open, industry-standard RFC 7519 method for representing claims securely between two parties. It is widely used for authorization since a token is generated as output in the authentication process. There is a great article that helps to distinguish them.
Although JWTs can be encrypted to also provide secrecy between parties, in this article we will focus on signed tokens. If you want to know more about encrypted tokens, you can read more about in this article RFC 7516.
Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
JWT’s can be used to build a stateless authorization system since all session information is inside the token and thus, we don’t need to rely on an application state in order to validate sessions or permissions, that’s the main difference between JWT and browser session hashes.
Token Structure
Tokens are broken down into three parts which are separated with a dot (“.”): header, payload, and signature.
The header specifies how the payload is encrypted, that is which is the algorithm that needs to be used to decrypt it.
Signed tokens encode the payload using base64 and the header should be encoded with base64 for both: signed and encrypted tokens.
The payload is the data that the token holds, each field on the JSON payload is called a claim. We can add as many claims as we want, bearing in mind that anyone could potentially decrypt or decode the tokens, so we should not include sensitive data, for instance, passwords, credit card numbers, and so on.
As the private key is not shared with anyone, only a token issuer that poses the public key can create new tokens. Nobody else is able to modify a token because the signature won’t be valid without the private key. Thus, the signature is a way to ensure that the payload did not change in transit.
Consider the following example token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
After decoding it, we can read its contents:
HEADER:
{
"alg": "HS256",
"typ": "JWT"
}PAYLOAD:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“iat”: 1516239022
}SIGNATURE:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
"<your-256-bit-secret>"
)
Claims in depth
Remember that claims are JSON fields in the payload contained inside the token. We can use the JWT Debugger to decode a token and inspect the payload.
The specification document defines seven reserved claims that are not required but are recommended to allow interoperability with third-party applications. These are:
- iss (issuer): Issuer of the JWT
- sub (subject): Subject of the JWT (the user)
- aud (audience): Recipient for which the JWT is intended
- exp (expiration time): Time after which the JWT expires
- nbf (not before time): Time before which the JWT must not be accepted for processing
- iat (issued at time): Time at which the JWT was issued; can be used to determine age of the JWT
- jti (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once)
IANA has a list of pre-defined claims, which is a good practice to use. If you need more, you can also create custom claims.
How should we use a JWT?
From a client, we must send the token on each request so the session can be validated. RFC 2616 specifies how authorization should be performed. A good standard practice for sending the token to a back-end service is making use of the Authorization header for this purpose. Besides the token itself, we need to specify the token type as well. So the header will look like this in our case:
Authorization: Bearer <token>
How to validate a JWT in a service?
The first thing we should do is validate the token structure. The token needs to have three parts separated by a dot “.”. Remember: header, the payload, and signature. So we should check we have three strings concatenated by two dots.
The next step is to check the signature, which is the last field in the token. If the signature is valid, we can trust that it was generated by our systems and it wasn’t modified. In order to do this, we need to get the JWKS located by default in:
https://<your_domain>/.well-known/jwks.json
For more details on the “well-known” prefix, please see RFC 5785.
Inside the jwks.json file, we can find the JWK, or JSON Web Key, a public key we use to verify the signature.
For example, if we are creating a signature for a token using the HMAC SHA256 algorithm, we can validate it with the following code:
HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
We will continue the validation process by checking standard claims, one of the most important claims is “exp”; a timestamp that represents the time until that token will be valid. To do this, first, we need to decode the payload using base64 and parse its result as a JSON.
Additional checks
As all applications are different this probably varies from app to app. Usually, we want to check permissions as well to detect if the currently logged user is able to get a specific resource, in this case, that validation could occur against a “perms” claim, in which we can define a map with all permissions and booleans that indicate if the user has permission or not. We can think of this even as a distributed service, so in this example, we have a “users” service and a “contents” service, in each service we have a model “admin” and “post” respectably, and lastly, we set a CRUD operation for each model with a boolean value which enables or disables the permission.
“perms”: {
“users:admin:read”: false,
“users:admin:create”: false,
“users:admin:edit”: false,
“users:admin:delete”: false,
“contents:post:read”: true,
“contents:post:create”: true,
“contents:post:edit”:true,
“contents:post:delete”:false
}
Conclusion
So far we covered the basics of JWT and how it works internally, and a few possibilities we can implement with them. We also got into how we should send tokens and how to validate them in backend services.
In the next article, we will see how to authorize incoming requests from a serverless service and how to authorize resources from a specific client without relying on a third party service.