OAuth2 JWT Verification Best Practices

October 15, 2019 | Posted in certificates,OAuth2,security

OAuth2 is very rapidly becoming the de-facto standard for securing APIs.
An OAuth2 JWT token is a signed JSON snippet containing fields (claims) that are needed to make a decision about granting access.

It is important to understand the inherent risks of OAuth2/JWT and make sure that the right mechanisms are in place to mitigate them.

A JWT token is similar to an X509 certificate. If a certificate is signed by a CA we trust (and if it is not expired, the signature is valid, etc.), we will trust the TLS client (or our browser will trust the server using this certificate). A JWT token is signed by an authorization server as opposed to a CA, so we have to trust the authorization server in order to authorize the client.

Another important difference is that a JWT token is short-lived, whereas an X509 cert could be issued for a year or more. This makes JWT tokens less risky from the security standpoint. But the issue of trust between the resource server and the authorization server still remains.

An access token is only as secure as the authorization server that issued it. If a private key that the server used to sign the token is compromised, all of its tokens will be compromised as well.

In order to verify a token's signature, we need to have access to the authorization server's public key. Public keys are made available in JWK format, usually from a well-known URL, e.g., "oauth2/keys".

Here's how a JWK's public key looks like:

{
    "kty": "RSA",
    "alg": "RS256",
    "kid": "oViynWdKmd9m43BihjrQH9bHlp22fto0Nu-zwaBzUAs",
    "use": "sig",
    "e": "AQAB",
    "n": "q8BD_0q9JQRnpZ5vLnBMEA03nUWmxE56nGvKFY8K0fOAHojFPExI0Il67NEv6TCPZaXiifT5p9N9DIQl-JaWNaQmDCvd5Hbeugqn05QGJ14E_ghTXA6iXsONnavri5qlgc5rPmAS9zkm755ID7mHnuskEMXJy929LlxFKHzDRTkN8Lf1hSVXG8Mdy0f1QW-01VNRE8ZW0Ar5vLLuGrDb8bg9fCZXA6CK7oVJHXzo6ajIgzpa86kpdvWOhhtYPCL9P9wNjt4kfX3LBb6_sl9s8lI0C0OWtoMyNtAbE4wFc08o0ZsW1UGQin5eFFBuH_zbaPwc7wvYw40bBw35U_V9Sw"
}

"n" and "e" are the modulus and the exponent of the RS256 public key.

JWK supports X509 certs in a sense that the public key could be taken from the cert and then the cert itself can be referenced using optional "x5u" or "x5c" claims of the JWK. However, many/most authorization servers today do not provide x509 certs along with the key. E.g., the public keys for Google APIs are straight RS256 keys.

Avoiding creating the x509 cert for each key allows for easier update/more frequent rotation of the keys and usually, resource servers rotate keys on a frequent basis, e.g., every three months.

The downside is that there is no chain of trust and no standardized revocation mechanism such as OCSP. We have to fully trust the authorization server provider and assume that it has appropriate safeguards in place to revoke/replace compromised keys.

With all this in mind, the following are the recommendations for securing OAuth2 verification for resource servers/API providers:

  • Make sure that the keys are frequently refreshed/rotated by the authorization server.
  • If an authorization server provides X509 certificates as part of its JWT, validate the public key using a regular PKIX mechanism.
  • Limit caching of public keys in the resource server. Re-download public keys as often as possible from a performance standpoint. As an aside, the authorization server reliability should be taken into account -- we don't want to introduce a single point of failure in case if the authorization server is down.
  • Obviously, all the communications with an authorization server have to be performed over TLS. The certificate of that resource server has to be verified and ideally checked using OCSP/OCSP stapling. If the TLS with the authorization server is properly secured, there is no need to using encrypted JWTs; this is actually the common practice today.
  • Always check that the "aud" field of the JWT matches the expected value, usually the domain or the URL of your APIs.
  • If possible, check the "sub" (client ID) -- make sure that this is a known client. This may not be feasible however in a public API situation (e.g., we trust all clients authorized by Google).
  • Use different scopes (roles) for different APIs, at the very least read-only and read-write scopes would be necessary. The authorization server will check if a client can be granted access to the requested scope. The client must specify the scope as part of its authorization/authentication request to the authorization server. It can't hurt, however, to also verify the scope in the resource server, to prevent any kind of misconfiguration mistakes in the authorization server. This could be limited to checking only for "elevated access" scopes, e.g., the read-write scopes.
  • Validate the issuer's URL ("iss") of the token. It must match your authorization server.
  • Ideally, the keys should be specific to your APIs or to a set of APIs provided by your organization. This is more realistic if you control both authentication and authorization but mode difficult when authentication/OpenID is performed by a different/public authorization server (Google, Facebook, etc.). There is a nascent specification for token exchange that will make this process easier but it is not yet widely supported.
  • This goes without saying, don't use unsigned tokens.
  • Use a predefined signature verification mechanism to avoid various algorithm-based attacks as explained here. Just ignore the "alg" claim of a JWT header and use an agreed-upon algorithm supported by your authorization server, such as RS256.