Team we45
September 10, 2016

JWT AND THE STATE OF STATELESS AUTHENTICATION

Over the last several months, we find that several of our customers and prospects are trying to go “stateless”. One of the serious painpoints for developers in a Web Application world is to maintain state. This was essentially achieved with Sessions and Session Tokens. We have all known and used sessions as de-facto for a long time, especially with traditional browser-oriented Web Applications. However, with the rise of the Web Service as the primary vehicle that caters to a wide variety of client-side technologies, there is a definite move/preference to go “stateless”. Stateless Applications are more suited to the “functional” approach that drives the adoption of Web Services. Stateless also lends itself better to multi-threading, concurrency and so on. In addition, maintaining state, especially at scale adds massive overhead to the infrastructure. More than ever, in today’s age of multi-client “API” style applications, going stateless seems to be the preference for most developers and application development houses.

One of the most popular authentication frameworks for stateless web services is the JWT or the JSON Web Token. The JWT replaces the need to issue and maintain a “Session Token”. Let's quickly explain how this works:

  1. The user authenticates with credentials to the application.
  2. Instead of Generating a Session and a Session Token associated with it in-memory, the application generates a Base64 Encoded token with JSON as the payload. The token is an Access Token that asserts certain claims (will get to this later) indicating that the user is now authenticated.
  3. The user now uses that access token (until it expires) to gain authenticated access to the application.

Now you must be thinking, “How is this different from Sessions? It sounds exactly the same”. In some ways, yes. However, with JWT there is an important distinction that makes it stateless. The JWT essentially consists of three sections:

  1. The Header
  2. The Payload
  3. The Signature

The Token essentially is a signed token with certain attributes of assertion. The Base64(JSON Header) and the Base64(JSON Payload) is signed with a secret key when issued (typically with HMAC-SHA256) and the signature is stored in the signature section of the token. The token is signed and transmitted to the client. The server does not store any “session” like object in memory. Whenever the token is resubmitted by the client (typically in the HTTP Request Header), the server attempts to decode the Token using the same secret key and verifies (or not) that the token has indeed not been tampered with and the user is considered authentic. Provided that the other assertion conditions are met, the user is authenticated to the application. Since there is no in-memory session store and other persistent information other than a “static” key, the JWT provides a powerful, stateless authentication framework that can be used extensively by traditional apps and web services alike.

This article is accompanied with a test API which demonstrates the concepts expounded by this article. This application is a simple Flask Web Service with a SQLite DB with some users and passwords. We will also provide relevant gists during the article.

The app can be found here => https://github.com/abhaybhargav/jwt_demo

What are these “Assertions”?

Every JWT can have a bunch of attributes that can be used to assert the validity and authenticity of the token. These are called “Claims”. They are:

  1. Issuer (iss) - Used to denote the issuer of the token. Can be an organization’s name or some kind of String or URI value (Optional)
  2. Issued at (iat) - NumericDate Value of the time the JWT was issued by the Issuer. (Optional)
  3. Expiration (exp) - NumericDate value of the time the JWT would expire (Optional)
  4. Audience (aud) - Denotes the recipients that the JWT is intended for. Could be String or URI (Optional)
  5. Subject (sub) - Referes to the subject or the user this token represents (Optional)
  6. Not Before Time (nbf) - The time before which this token should not be accepted by the application (Optional)

As you can see from the ‘Optional’ flag on each of these Claims, (a.k.a. Registered Claims), none of these are mandatory, but are useful and highly recommended when the application receives a token that it needs to validate. For instance - If an attacker submits a token that is expired, but the application still accepts it, would be cause for a potential security breach.

Aside from the registered claims, you can also include custom, additional claims in your JWT that you can validate upon receiving the token. This makes it useful in scenarios where you need to add additional attributes to your authentication token for certain functions and validate it in other functions or state change events.

Are there known exploits against JWT?

Yes, there are. The two most common exploits against JWT date back to March 2015. They are:

The “none” algo flaw - In a JWT implementation, it is possible for you to sign your token with HS256 (HMAC-SHA256) or with certain PKI algos like RSA and ECDSA, apart from HS384 and HS512. However, you also have the ability to specify the “none” algo. Some libraries, treate tokens signed with the “none” algo as a valid token. Therefore, an attacker can “sign” supposedly valid tokens with the “none” algo and pass it to a vulnerable app (due to a vulnerable library or vulnerable implementation of a library) and authenticate to the app. The library I am using is the PyJWT, which is patched, however, setting it to a verify=False mode, we can pass “none” signed tokens to the application that accepts it as valid tokens.

token ='eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTQ3ODcwOTYyOSwiZXhwIjoxNDc4NzEzMjI5LCJpYXQiOjE0Nzg3MDk2MjksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.'jwt_manage = jwt.decode(token, verify=False)#Remember, certain libs might not be patched or require explicit verification params to be set

view raw none_token.py hosted with ❤ by GitHub

In our test application, if you pass a “none” signed token in the “Authorization” header to the /insecure_auth URI with a HTTP GET request, you will find that your token is accepted as legitimate.

The Public Key Flag - Another implementation flaw that is common with JWTs is when developers sign the token with a public key. Public Keys are by nature “public”. If the attacker gets hold of the public key of the signing server, then forging signed tokens with said public key is a trivial task. Several libraries accept a token signed with a public key, but some of the more updated and security conscious developers of libraries (like the one we have used, PyJWT) for instance, have deemed any tokens signed with Public Keys to be invalid and a security exception, which is great!

To simulate this with our test application, copy the contents of the “public.pem” file and generate an HS256 signed JWT on http://kjur.github.io/jsjws/tool_jwt.html and use that to pass the JWT to the ‘/rsa_auth’ URI on our web service, you’ll find in your console that it has produced an InvalidKeyError which disallows the use of the RSA Public key to sign the token.

What are Security Best Practices around JWT?

There are some good practices you can follow to secure your implementation of JWT. They are:

  1. Validate! Validate! Validate!
  2. Protect your secret key
  3. No Sensitive Data in the JWT
  4. Explicit algo and key validation
  5. What about CSRF?

Validate! Validate! Validate!

Attacks can happen in conventional and unconventional ways with JWT. The simple attacks could be attackers sending forged tokens with “none” algos or with expired tokens. Apart from verifying the signature, its a good practice to validate all claims of the JWT like the iss claim, the aud claim, the exp claim, the nbf claim and so on. In addition, you would need to ensure that the signature is verified. If an application does not verify the signature, attackers may be able to pump the Token with malicious payloads, if you’re really unlucky, could end up with injection attacks, authorization attacks or other client side attacks as well.

def verify_jwt(token):try:decoded = jwt.decode(token, app.config['SECRET_KEY_HMAC'], verify=True, issuer = 'we45', leeway=10, algorithms=['HS256'])print("JWT Token from API: {0}".format(decoded))return Trueexcept DecodeError:print("Error in decoding token")return Falseexcept MissingRequiredClaimError as e:print('Claim required is missing: {0}'.format(e))return False

view raw jwt_validation.py hosted with ❤ by GitHub

This source code snippet of our test app displays validation with signature verification, issuer verification, and expiration verification. The expiration has a leeway of 10 seconds even after the token expires, which can be used if you don't want to be too strict with expiration values.

Protect your secret key

By now, you must have understood that the Key used to sign the JWT needs to be protected. This is akin to protecting any other secret in your application, right from passwords to encryption keys. We would highly recommend the use of a secret management solution like Vault or Keywhiz to protect the secret key. It goes without saying that the secret key should not be easily guessable. The secret we have used for the test application is terrible! It's just for purposes of demonstrating the concepts. Do not use something like this in your application!

No Sensitive Data in the JWT

A lot of devs we have met think that the JWT is encrypted. This is not true. The JWT is merely encoded with Base64URL Encoding. The contents of the token can easily be converted to the JSON plaintext values. Therefore, DO NOT put in any sensitive data like passwords or PII in the JWT.

Explicit Algo and Key Validation

A lot of libraries do not require you to specify the algo that you are using to verify the token. We would highly recommend creating  a whitelist of algos that you will use to verify the token, especially in the event that you are using multiple algos and keys for signing tokens. It is a layer of defense against vulnerable JWT libraries, that accept public keys as valid keys for signing tokens.

def verify_jwt(token):try:decoded = jwt.decode(token, app.config['SECRET_KEY_HMAC'], verify=True, issuer = 'we45', leeway=10, algorithms=['HS256'])print("JWT Token from API: {0}".format(decoded))return Trueexcept DecodeError:print("Error in decoding token")return Falseexcept MissingRequiredClaimError as e:print('Claim required is missing: {0}'.format(e))return False

view raw jwt_validation.py hosted with ❤ by GitHub

What about CSRF?

This is a source of slight conflict with JWT. JWT could be with traditional browser-based apps, as a substitute to Cookies. This begs the question “Is this protected against CSRF?” The answer is “maybe”. The very point of using Token based Authentication, is the fact that you negate the use of Stateful Cookies, hence you are protected against CSRF by default as the attacker doesnt know the Token value. With JWTs used in browser based applications, you could store the Token in the localStorage or sessionStorage. However, if your application is vulnerable to XSS, then attackers could possibly extract the values from localStorage and steal tokens.

There is another school of thought that states that you could use a CSRF Token as a claim in the Token that would need to be asserted. However, this would negate the very statelessness of the JWT.

Thanks for reading. Do reach out to us if you have any questions on JWT or if you need any help with securing your applications.