Custom JWT Generation for Cloud Solutions
This blog post was published on PurpleBox website on Jan 5th, 2022.
What are JWTs?
JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted since it is digitally signed. JWTs can be signed using a secret key or a public/private key pair. Signed tokens consist of three parts separated by dots: header, payload, and signature. They 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 proves that only the intended party accessed the contents.
JWTs are useful in several scenarios. Authorization is the common usage for JWTs. Once the user is logged in, each following request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign-On is a feature that widely uses JWT because of its small overhead and its ability to be easily used across different domains. Secure information exchange is another scenario where JWTs are useful. Since JWTs can be signed, the party who has signed it can be verified. Additionally, as the signature is calculated using the header and the payload, it can be understood if the data in it has been changed or not.
Identity Providers that generate JWTs
There are several identity providers that use JWTs as part of their schemes. Some of the examples are Cognito, Auth0, and Okta:
Cognito lets us add user sign-up, sign-in, and access control to web and mobile apps quickly and easily. Amazon Cognito scales to millions of users and supports sign-in with social identity providers, such as Apple, Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0 and OpenID Connect. Cognito also supports the use of other providers.
Auth0 issues JWTs as a result of the authentication process. When the user logs in using Auth0, a JWT is created, signed, and sent to the user. Auth0 supports signing JWT with both HMAC and RSA algorithms. This token will be then used to authenticate and authorize with APIs which will grant access to their protected routes and resources. JWTs are also used to perform authentication and authorization in Auth0’s API v2, replacing the traditional usage of regular opaque API keys.
Okta is a customizable, secure, and drop-in solution to add authentication and authorization services to applications. It gets rid of the development overhead, security risks, and maintenance that comes from building it in-house.
Why Custom JWTs are Needed?
Custom JWTs may be needed and useful for several purposes.
- During development, they help us understand JWT basics.
- They may be needed if the functionalities provided by identity providers are not compliant with our system.
- They allow users to authenticate with an authentication system that is independent of any provider.
- They allow us to avoid third parties being involved with our applications.
Important Note: One downside of opting for custom JWTs is the overhead. Using custom JWTs requires extra layers of implementation, maintenance, and cost, so we should decide this process is a mandatory requirement for architectural design or not. If it’s not, we should always think about using identity providers for JWT. The following architectural diagram shows the necessary steps to efficiently implement custom JWTs.
To efficiently use custom JWTs, they should be connected to authorizers and services associated with their usage. In this blog post, we explain how to perform the first step on AWS Cloud.
JWT Generation on AWS Cloud
Firstly, assess which claims are needed in the custom JWT. Some options are User ID, username, roles, expiration time. These claims will be used in the JWT script. Then, generate the necessary keys using Ubuntu. First, install OpenSSL, and cryptography libraries. After installing the libraries, generate keys using the following commands:
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
These commands output two keys in the form jwt.io require. Then, copy the keys into the local machine. Save the private key into a file named “jwt-key” and copy the public key into a file named “jwt-key-public”. After generating the keys, the private key needs to be recorded in AWS Secrets Manager. To do this, log in to your AWS console, then open the AWS Secrets Manager dashboard. Select ‘Store a new secret’.
Then, choose ‘Other type of secret’, and ‘Plaintext’ as the storage option. Paste the generated private key in the box, do not forget to clear the existing text.
Lastly, give a name to the stored secret. Adding a description or tags is optional but may be preferred for better identification. After finalizing, note down the ARN of the secret. We need this ARN to update our code later.
For AWS Lambda, a ‘.zip’ file containing the necessary libraries and main code needs to be uploaded. An alternative way is to directly upload the compressed file into an S3 bucket via console. To prepare the necessary file, download authlib and cryptography libraries into a folder. Next, we will prepare the main code file and add it to this folder before compressing it. First, copy the following code and update the issuer field.
from authlib.jose import jwt
from time import time
import base64 //Secrets manager code goes here.def lambda_handler(event, context):
device_id = event['device_id']
user_id = event['user_id']
header = {'alg': 'RS256'}
payload = {'iss': '<your-name>', 'sub': user_id, 'user_id': user_id, 'device_id': device_id, 'exp': time() + 3600}
private_key = get_secret()
s = jwt.encode(header, payload, private_key)
data = s.decode("utf-8")
print("success")
return data
To fill in the secrets manager part of this code, head to the secrets manager console and click on the private key we previously stored. Head to the bottom of the page, then copy the python code generated by AWS. Update the generated code to fit your needs, then paste it into the corresponding part of the code. Do not forget to name the code file ‘lambda_function’! After adding the generated code, we are ready to upload our compressed file to lambda.
Carry the ‘.zip’ file into your main machine, then check if the compressed file has extra parent directories. If so, all files which need to be included in the function need to be selected and compressed. Lambda will raise an error if there are extra parent directories, so only compress the necessary files. After preparing the ‘.zip’ file, head into the lambda console. Select ‘Create function’.
Name your function, select ‘Python 3.9’ from the runtime options, leave the rest as default then hit ‘Create function’. Now, for our function to work correctly, we need to give it enough permissions to access Secrets Manager. Find the newly created function from the lambda dashboard, then click on it. From here, select ‘Configuration’, then ‘Permissions’.
Click on the assigned execution role, this will lead you into the IAM console. From here, select ‘Attach policies’.
In this menu, search for ‘SecretsManagerReadWrite’, then check the box next to it and click ‘Attach policy’. Then, head back to the Lambda console, and click ‘Test’.
Set up the test event with device_id and user_id fields. To avoid errors, check if the handler name is lambda_function.lambda_handler. Then, hit test to obtain the generated JWT. To test the obtained JWT, paste it into jwt.io.
The values are correct, but the signature is not yet verified. For verification, paste the generated public key into the corresponding box.
Supplying the JWT and the public key into jwt.io, it can be observed that the signature is now valid.
Attacking JWTs
We introduce two sample tools to attack our generated JWT: jwt_tool and jwt_hack.
1. jwt_tool can be used to validate, forge, scan, and tamper JWTs. Its functionality includes:
- Checking the validity of a token,
- Testing for known exploits,
- Scanning for misconfigurations or known weaknesses,
- Fuzzing claim values to provoke unexpected behaviors,
- Testing the validity of a secret/key file/Public Key/JWKS key,
- Identifying weak keys via a High-speed Dictionary Attack,
- Forging new token header and payload contents and creating a new signature with the key or via another attack method,
- Timestamp tampering,
- RSA and ECDSA key generation, and reconstruction (from JWKS files).
The sample screenshot from the tool shows the functionalities it features.
2. jwt_hack can be used to crack, decode, and encode JWTs, and create attack payloads. For example, using the ‘payload’ command and supplying a JWT outputs several different JWTs tampered with known exploits. These JWTs can be used to check the correctness and validity of the original via jwt.io or be used in some tests to see if a system is running correctly.
Conclusion
In this blog post, we’ve talked about JWT (JSON Web Token) with different points of view. We provided information about what JWT is, JWT use cases, why we need custom JWT. We give you details about custom JWT generation for your AWS Cloud solutions. Also, we’ve introduced different JWT attacking tools that could be useful for your pentest processes. We hope you enjoyed it! Stay safe in the cloud!
If you want to read more on this topic, feel free to check out the PurpleBox Blog Section.