JSON Web Tokens
There are many excellent introductions to JWTs, so for the purposes of this discussion we will focus on the structure.
JWTs are typically transmitted as base-64 encoded strings, and are composed of three parts separated by periods:
- A header containing metadata about the token itself
- The payload, a JSON-formatted set of claims
- A signature that can be used to verify the contents of the payload
For example, this JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSBTbGFzaElEIiwiaWF0IjoxNTE2MjM5MDIyfQ.4cL42NsNCXLPEvmvNGxHN3wLuarpp98wwezHnSt2fqg
comprises the following parts
Part | Encoded value | Decoded value | Description |
---|---|---|---|
Header | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | { “alg”: “HS256”, “typ”: “JWT”} | Indicates that this is a JWT and that it was hashed with the HS256 algorithm (HMAC using SHA-256) |
Payload | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSBTbGFzaElEIiwiaWF0IjoxNTE2MjM5MDIyfQ | {“sub”: “1234567890”, “name”: “SlashID User”, “iat”: 1516239022} | Payload with claims about a user and the token |
Signature | 4cL42NsNCXLPEvmvNGxHN3wLuarpp98wwezHnSt2fqg | N/A | The signature generated using the HS256 algorithm that verifies the payload |
The important aspect of this is that the JWT is signed, meaning that the claims in the payload can be verified, if one has access to the appropriate cryptographic key.
The algorithm used for the signature is stored in the Header (alg
) and as we’ll see later in the article, this becomes the source of a lot of issues with JWTs.
The signature of a JWT token is calculated as follows:
signAndhash(base64UrlEncode(header) + '.' + base64UrlEncode(payload))
Where signAndhash
is the signing and hashing algorithms specified in the alg
header field. The JOSE IANA page contains the list of supported algorithms.
In the example above HS256 stands for HMAC using SHA-256 and the secret
is a 256-bit key.
Signing is not the same as encryption - even without the cryptographic key for verifying, anybody can decode the token payload and inspect the contents.
Naming convention
There are many standards associated with JWTs, it is useful to clarify a few different formats as we’ll use them throughout the article.
JWT (JSON Web Token): JSON-based claims format using JOSE for protection
JOSE (Javascript Object Signing and Encryption): set of open standards, including:
- JWS (JSON Web Signature): JOSE standard for cryptographic authentication
- JWE (JSON Web Encryption): JOSE standard for encryption
- JWA (JSON Web Algorithms): cryptographic algorithms for use in JWS/JWE
- JWK (JSON Web Keys): JSON-based format to represent JOSE keys
The JWT specification is relatively limited as it only defines the format for representing information (“claims”) as a JSON object that can be transferred between two parties. In practice, the JWT spec is extended by both the JSON Web Signature (JWS) and JSON Web Encryption (JWE) specifications, which define concrete ways of actually implementing JWTs.
Anatomy of a JWT-related bug
A key aspect of JWTs, and one of the reasons why they have become so popular, is that they can be used in a stateless manner. In other words, the server doesn’t store a copy of the JWTs it mints. As a result, to check the validity of the token both the client and the server need to verify their signature. The validity of the signature is how we prove the integrity of the token.
Generally vulnerabilities in JWT implementations rely on either a failure to validate a token signature, a signature bypass, or a weak/insecure secret used to encrypt or sign a token.
Fundamentally three design choices make JWT implementations prone to issues:
- Deciding the decryption/validation algorithm based on untrusted ciphertext
- Allowing broken algorithms (RSA PKCS#1 v1.5 encryption) and “none”
- Allowing for very complex signing options. For instance, supporting X.509 Certificate Chain.
Let’s see some of the most common issues with JWTs.
The “none” Algorithm
The none
algorithm is intended to be used for situations where the integrity of the token has already been verified. Unfortunately, some libraries treat tokens signed with the none
algorithm as a valid token with a verified signature. This would allow an attacker to bypass signature checks and mint valid JWT tokens.
Solution: Always sanitize the
alg
field and reject tokens signed with thenone
algorithm.
“Billion hashes attack”
Tervoort recently disclosed at Black Hat a new attack pattern.
JWT tokens support various families of PBES2
as signing/encryption algorithms. The p2c
header parameter is required when using PBES2
and it is used to specify the PBKDF2 iteration count.
An unauthenticated attacker could use the parameter to DoS a server by specifing a very large p2c
value resulting in billions of hashing function iterations per verification attempt.
Solution: Always sanitize the
p2c
parameter.
Brute-forcing or stealing secret keys
Some signing algorithms, such as HS256 (HMAC + SHA-256), use an arbitrary string as the secret key. It’s crucial that this secret can’t be easily guessed, brute-forced by an attacker or stolen.
An attacker with the secret key would be able to create JWTs with any header and payload values they like, then use the key to re-sign the token with a valid signature.
Solution: Avoid weak secret keys, implement frequent key rotation.
Algorithm confusion
As discussed, JWTs support a variety of different algorithms (including some broken ones) with significantly different verification processes. Many libraries provide a single, algorithm-agnostic method for verifying signatures. These methods rely on the alg
parameter in the token’s header to determine the type of verification they should perform.
Problems arise when developers use a generic signature method and assume that it will exclusively handle JWTs signed using an asymmetric algorithm like RS256
. Due to this flawed assumption, they may end up in a “type confusion” type of scenario. Specifically, a scenario where the public key of a keypair is used as an HMAC secret for a symmetric cypher instead.
An attacker in this case can send a token signed using a symmetric algorithm like HS256
instead of an asymmetric one. This means that an attacker could sign the token using HS256 and the static public key used by the server to verify signatures, and the server will use the same public key to verify the symmetric signature thus completely bypassing the signature verification process.
Solution: Always verify the alg parameter and ensure that the key passed to the verification function matches the type of algorithm used for the signature.
Key injection/self-signed JWT
Although only the alg
parameter is mandatory for a token, JWT headers often contain several other parameters. Some of the more common ones are:
-
jwk (JSON Web Key) - Provides an embedded JSON object representing the key.
-
jku (JSON Web Key Set URL) - Provides a URL from which servers can fetch a set of keys to verify signatures.
-
kid (Key ID) - Provides an ID that servers can use to identify the correct key in cases where there are multiple keys to choose from.
These user-controllable parameters each tell the recipient server which key to use when verifying the signature.
Injecting self-signed JWTs via the jwk parameter
The jwk
header parameter allows an attacker to specify an arbitrary key to verify the signature of a token. Servers should only use a limited allow-list of public keys to verify JWT signatures. However, default implementations of JWT verification libraries allow for arbitrary signatures to be used hence the developer has to allow-list specific keys or otherwise an attacker could bypass the signature verification process.
Solution: Disallow the usage of
jwk
or have an allow-list of valid keys.
Injecting self-signed JWTs via the jku parameter
Similar to the example below it is crucial that the keys passed to the verification function via the jku
parameters are part of an allow-list. Further the implementer should also
have an allow-list of domains and valid TLS certificates for those domains.
In fact, JWK Sets like this are often exposed publicly via a standard endpoint, such as /.well-known/jwks.json
- if a domain is subject to a watering-hole attack or the verification function doesn’t verify the domain an attacker is able to bypass signature verification.
Solution: Disallow the usage of `jku“ or have an allow-list of valid keys, trusted domains, and valid TLS certificates for those domains.
Injecting self-signed JWTs via the kid parameter
Verification keys are often stored as a JWK Set. In this case, the server may simply look for the JWK with the same kid
as the token. However, the kid
is an arbitrary string and it’s up to the developer to decide how to use it to find the correct key in the JWK Set.
The kid
parameter could be used for a command injection attack without proper sanitization. For example, an attacker might be able to use it to force a directory traversal attack pointing the verification function to a static, well-known file like /dev/null
which would result in an empty string used for verification and often result in a signature bypass.
Another example is if the server stores its verification keys in a database, the kid
parameter is also a potential vector for SQL injection attacks.
Solution: Always sanitize the
kid
parameter.
The SlashID approach
The issues above are just some of the common ones found in libraries, but many others keep coming up due to the design flaws described above. At SlashID, we strive to help customers secure their identities so we took a principled approach to the problem by abstracting it away for developers.
In a previous blogpost we’ve discussed some of the implementation details of our we mint and verify JWT tokens.
But we’ve gone further and added a JWT verification plugin to Gate.
The verification plugin works both with tokens issued by SlashID as well as tokens issued by a third-party.
To address the issues described above, we employ several countermeasures:
- Only support pre-defined signing algorithms
- Rotate signing keys frequently and adopt a vaulting solution to store the private key
- Verify and pin TLS certificates for JWKS
- Maintain an allow-list of valid domains
- Disallow unsafe header parameters
By deploying Gate with the JWT verification plugin, developers can offload the complexity and risk of verifying JWT tokens to SlashID.
Let’s see an example in action.
Gate configuration
First, let’s look at an example using SlashID as the issuer:
...
- id: validator
type: validate-jwt
enabled: false
parameters:
<<: *slashid_config
jwks_url: https://api.slashid.com/.well-known/jwks.json
jwks_refresh_interval: 15m
jwt_expected_issuer: https://api.slashid.com
urls:
- pattern: "*/api/echo"
target: http://backend:8000
plugins:
validator:
enabled: true
- pattern: "*/api/admin_abac"
target: http://backend:8000
plugins:
validator:
enabled: true
parameters:
token_schema: |
patternProperties:
user_roles:
contains:
const: admin
required:
- user_roles
In the configuration, we tell the plugin that the JWKS URL is https://api.slashid.com/.well-known/jwks.json
, and the keys should be refreshed every 15 minutes.
We also specify that the issuer has to be https://api.slashid.com
.
In the URLs, for the */api/echo
path we simply allow requests that have a valid JWT in the Authorization header.
For the */api/admin_abac
, we also specify that the plugin should check for the presence of a user_roles
claim in the JWT and that claim should contain the string admin
.
This is an easy way to enforce ABAC on an endpoint.
If we were to use a third-party issuers instead of SlashID, the plugin instance configuration instance would change slightly:
- id: validator
type: validate-jwt
enabled: false
parameters:
jwks_url: https://issuer.example.com/.well-known/jwks.json
jwks_refresh_interval: 15m
jwt_expected_issuer: https://issuer.example.com
jwt_valid_cert: <CERT signature>
jwt_allowed_algorithms:
- 'ES256'
- 'ES512'
The difference is that for third-party issuers we need to specify the jwt_allowed_algorithms
and a valid jwt_valid_cert
URL signature.
Conclusion
In this brief post we’ve shown how JWT tokens while ubiquitous and simple-looking at first, are fraught with risk. Gate is an effective and easy way to offload that effort from application developers.
We’d love to hear any feedback you may have! Try out Gate with a free account.