a theoretical implementation of the HTTP 402 status code

I have always been interested in the semantics of the web, HTTP status codes are such an elegantly simple solution to the challenging problem of how to communicate all the various states of a service to the client.

One code has stuck out to me for years, and that is 402. While all other status codes have well defined request and response expectations, in nearly all the lists and guides you'll find, 402 is always coupled with a note that mentions "this is not yet implemented" or "this is undefined and experimental".

So what is HTTP 402, and why is it so elusive, that in the nearly 10 years since RFC7231 was published defining HTTP status codes, it still has not been well defined and implemented? Surely it must be a convoluted and complex specification full of technical jargon. Let's read Section 6.5.2 of RFC7231:

6.5.2. 402 Payment Required

The 402 (Payment Required) status code is reserved for future use.


That's it. That's the full specification for HTTP 402. No jargon, no complex standards to adhere to - simply "Payment Required".

It is in this simplicity we find the confusion - even with the web being over three decades old, the problem of transferring real-world assets over the web is still a real challenge.

To send money from one web user to another still requires a third party service such as a bank, PayPal, or Stripe. While in one sense these services are innovating to provide an integration service in the current state of the web, it never sat well with me that there was not a way for two users to exchange value on the web without involving a third party.

Currently, the most popular way to transfer value between two users on the web without involving a third party is cryptocurrency.

Crypto is a relatively new technology, and while it certainly has its caveats and limitations, it is the most currently applicable technology to the problem.

The Challenge

Since browsers currently do not implement the HTTP 402 status code, we will have to get creative to implement it.

We need a way for web resources to specify the need for web payments - provide a way to receive those payments - and then handle user authentication and authorization once payment is sent to then access the specified resources.

Additionally, we need to be able to present a unified payment interface to clients which handles the full payment flow, issues the proper access token, and pass the user through to the desired content once payment is received.

Oh, and all of this must be done with zero code / development work on the web resource owner side, since it should all be semantically just a status code and specification.

The Solution

As most modern web applications are running behind some form of gateway, I chose to implement the HTTP 402 status code as a gateway / middleware layer, with the idea that if / when it were to be implemented as a native gateway/IAP standard, the middleware service could be removed with little-to-no refactor of dependent services required.

By tying into the gateway layer, requests can be intercepted and responded to with a payment form when required, and authorization tokens can be validated in-flight to ensure that the user is authorized to access the resource.

While the various gateway technologies - NGINX, Istio, Cloudfront, Cloudflare, GCP IAP, etc - are all different, the underlying concepts are the same and therefore a generic interface can be designed to provide a gateway-agnostic integration service.

The Implementation

The implementation is fairly straightforward. We have a small authorizing middleware service which checks the upstream resource for a "payment required" flag, and if found, presents the user with a payment form. Once the middleware confirms the payment is received, it issues the user a token which can be used to access the resource.

When the user accesses the resource, the middleware service validates the token and if valid, passes the user through to the resource.

Since the traffic is flowing through the gateway and not the middleware - the middleware is simply acting as a binary authorizer service for the traffic - it does not present a scaling limitation or significant performance impact to the application.

Of course the lower-level technical implementation ended up being slightly more complex. Let's take a look at the high level architecture:

architecture

You'll note the two 402 services (indicated by the Golang Gophers) - for a deeper dive into the reasoning for the separation of these services, check out my other post on securing internal upstreams in a multitenant environment.

So what's going on here? When a user makes a request to a web resource, the gateway first checks with 402 to determine if that resource requires payment or not. If not, it passes the request through to the resource.

If the resource requires payment, 402 checks the payment request header returned by the resource and generates a new crypto address for the user to send the payment to, and then presents the user with a payment form on the specified crypto network, for the specified amount, and with the unique receive address.

402 then begins to monitor the address for the payment, and once the payment is received, it issues the user a token which can be used to access the resource and automatically redirects the user to the resource.

The user, now with the authorization token in their cookies, can access the resource without having to pay again, until the token expires.

As tokens are JWTs, they enable additive claims-based authentication and authorization against multiple resources specifying different scopes.

And as 402 relies on any EVM-compatible cryptocurrency network, it can be entirely self-hosted and run without any third party payment provider.

Let's check out an example payment request that would be presented as a response header (or HTML HEAD tag for static content) to require payment for that resource.

{
    "claims": {
        "aud": "example.com",
        "resource": "/hello-world",
        "userLevel": "silver",
        "moreCustomClaims": "my-custom-data"
    },
    "exp": 6000000000000,
    "payment": {
        "requests": [
            {
                "network": "polygon",
                "address": "",
                "amount": 10
            }
        ]
    },
    "customization": {
        "css": "https://example.com/payment-style.css",
        "js": "https://example.com/payment-scripts.js",
        "image": "https://example.com/payment-image.png"
    }
}


The schema is intentionally simple. You have a set of claims which is simply a map[string]string of claims that will be included and validated in the JWT.

There is an optional exp defining an expiration time from the time of issuance, in milliseconds.

There is a payment section which contains a list of payment requests on various supported networks. The address field will be auto-filled with a unique per-user receipt address.

The customization section contains optional fields for CSS, JavaScript, and an image to be displayed on the payment form, so that it can be customized to match the site branding while still providing a unified payment experience across 402-enabled sites on the web.

Where to go next

While fully functional, of course this is still a very early-stage concept, and ultimately browsers will be implementing some form of payment system in the future - this is just an example of what could be done today with the technologies we currently have available.

Feel free to play around with the code and try out 402 on your next project. We are still in the very early stages of zero-trust web payments, and it is exciting to see what is to come.

last updated 2024-03-18