DEV Community

Cover image for Zero Trust at the Edge (part 1)
Tom Mount
Tom Mount

Posted on

Zero Trust at the Edge (part 1)

Part 1: Using JSON Web Tokens to Validate Requests

Filter your traffic as far away from your origin as possible.

JSON Web Tokens (JWTs) have exploded in popularity over the last few years, and with good reason: implemented correctly, a JWT system can ensure that requests are authorized at every point in the journey from a user's browser to your origin server. A JWT is simply a JSON payload transmitted across the internet along with an encrypted signature that allows the receiver to verify that the payload has not been tampered with.

In Part 1 of this series, we'll review what a JSON Web Token is; how tokens are generated and signed; and how, when, and by whom the tokens should be verified. In Part 2, we'll look at a simple Express server that both signs and validates JWTs. In Part 3, we'll take JWTs one step further and explore JSON Web Key Sets (JWKS).

Defining the Terms

The JWT is made up of three components: the payload is a cleartext JSON object transmitted with a signature (a cryptographically generated representation of the payload) and a header that identifies the type of signing or encryption used to generate the signature. These three components are sent in a single bundle (the token) to the receiving server; the server must then validate the signature from the token before it can trust any claims (the JSON keys) in the payload. Once verified, the server can treat the payload as authoritative information. At any point in the transmission process, any system with simple cryptographic capabilities can validate the signature of the payload and halt the request if the signature does not match.

JWT Structure
Source: https://supertokens.com/blog/what-is-jwt

Generating the Token

A token is generally created first at the server. This may happen after a user successfully logs into a service, completes an authentication challenge, etc. The server creates the payload based on the business requirements. Generally, the payload will at minimum include the following claims:

  • iat: the timestamp of the token creation;
  • exp: the timestamp when the token will expire.

Other common claims include the sub (the subject of the claim, usually a username or user ID), iss (the issuer of the token), and aud (the audience for whom the token is meant). Any other information can be included as long as it can be serialized into JSON. Note, however, that the payload is sent in clear-text and should not contain any secret information like passwords, PII, etc.

The server will choose an appropriate header. The header should always include an alg claim with a specific value representing the algorithm used to generate the signature. Some algorithms (like HMAC SHA) only require a secret key on the server to generate; others, like RSA SHA, require a signed public/private key pair rather than a simple secret value. Regardless of the algorithm used, the signing data must be kept secret, as a third party with knowledge of the secret key or a copy of the private key could easily generate valid JWTs and spoof authentication.

The signature is generated based on the algorithm chosen. The header and payload are both base64-encoded (separately) and joined with a dot ([encoded_header].[encoded_payload]). This joined text is sent through a hashing function that calculates the hashed and encrypted value of the header and payload; this signature is then appended with a dot. The final format of the token is [encoded_header].[encoded_payload].[signature].

Sending the Token

Generally, the token is returned as a set-cookie header so that the token can be sent with every subsequent request to the server. The token must be revalidated on each request to ensure that the individual request is authorized. One of the most common use cases for JWTs is around authentication: once a user has successfully verified their credentials, the server sends a JWT back with the response that is then used to continually verify that the user is authorized to make those requests. This allows for a more stateless application.

Validating the Token

JWTs should ALWAYS be validated by the origin server; this prevents any sort of man-in-the-middle attack that might occur between a token being validated at the outer edge of the application perimeter and the token being received by the origin server. But there is still value in validating JWTs at the outermost edge of the application. Validation before the origin can help prevent a supply-chain attack against the token. Tokens that are known to be bad can be rejected before going very far into the application stack, sometimes at the very first node that sees the token. And, depending on the application architecture, some requests can be handled without going to the origin if the token has been validated at the edge. In the final two cases, the end result is less traffic going back to the origin server and potentially a more performant application.

Tokens signed with RSA (an asymmetric algorithm) can be validated merely with a public key, which means that the private key used to sign the payload can remain on the signing server and does not need to be stored at any other point where the token would be validated. Tokens using HMAC SHA signing (a symmetric algorithm) will require the secret value in order to generate a comparison hash. In either case, the process is fundamentally the same: the verifying party splits the token into its components (header, payload, and signature), and passes the joined header and payload through a verification algorithm, with either the public key or the shared secret key as an additional argument. The verification algorithm will check to make sure that the header and payload given to it match the signature. If so, the claims in the payload can be trusted; if not, the claims have likely been tampered with and should not be trusted.

Simplified Real World Use Case

I worked on a project a few years ago for a company that wanted to spin up "companion" websites for live-streamed conferences. Attendees at these conferences would have limited access to certain content throughout the site based on the tier ticket they had purchased, for example, gold, silver, or bronze packages. They were expecting tens of thousands of visitors to a WordPress site and wanted to optimize the cache hit capability for as many requests as possible. Two problems immediately leaped out:

  1. Asking WordPress to handle ten thousand simultaneous authorization requests is asking WordPress to crash.
  2. Authenticated content is typically not cached by any CDN to prevent one user's experience on a given page from being cached for all users who visit that page.

JWTs provided an elegant solution to both. Rather than have WordPress handle the user logins, we created a custom authentication page in Google Firebase and redirected new users there. Once they logged in, Firebase returned a JWT with only one critical custom claim: the user's membership level. The WordPress server would be responsible for validating the token, and as long as it was valid, it would impersonate a generic user at that ticket tier, generate the appropriate content for the given URL and membership level, and send the response with a custom response header with that membership level. The caching server would use the custom header as part of a custom cache key for that URL, enabling gold, silver, and bronze versions of all URLs to be cached simultaneously. For subsequent requests, the CDN would first validate the JWT, and if it was valid, it would add the ticket tier into the cache key to look up the unique combination of URL and tier and return the appropriate version of that URL. Only if that combination of URL and tier did not exist would it dispatch the request to WordPress.

Because users could not generate their own JWT to give them access to the content (a signed JWT could only come from Firebase) nor could they change their stored JWT to give them a higher level of access (because the signature would be invalid), this allowed us to serve authenticated content via the CDN and keep the amount of traffic going back to WordPress to a minimum, once the cache had been populated. We recorded a 99% cache hit ratio (the ratio of the number of requests served from the CDN's cache vs. the total number of requests received by the CDN) through the duration of the first event where we implemented this stack, and NewRelic showed that the WordPress instance barely received any traffic at all once the cache had been sufficiently warmed.

Further Reading

  • https://jwt.io/, a tool created by Auth0/Okta which generates a variety of "dummy" JWTs and allows you to change all three parts of the token and see the effect of those changes. It's an invaluable tool if you're building your own JWT signing service and want to experiment or quickly validate your own tokens. You can also inspect tokens from any other service, even if you can't validate them within this tool.
  • https://auth0.com/docs/secure/tokens/json-web-tokens, Auth0's official documentation on JWTs.

Top comments (0)