DEV Community

Savinda
Savinda

Posted on

Building a JWT-Aware Reverse Proxy in Go for Tiered API Access

Introduction

In this blog post, I’ll walk you through a small proof-of-concept project I built: a reverse proxy in Go that routes requests to different backend services based on the tier claim in a JWT token.

The proxy uses RSA public/private key cryptography to validate the JWT, then forwards the request to either a free-tier or subscribed-tier backend. The whole setup runs in containers on Minikube. It’s a great way to understand how reverse proxies and JWT authentication can work together

Motivation

I wanted to understand how reverse proxies work at a lower level and how JWTs could be used to manage access control between service tiers. I also wanted something I could deploy easily with Docker and Minikube, so it’s very infrastructure- and developer-friendly.


Architecture Overview

Here's what the system looks like:

Architecture

  • The client sends a request with a JWT in the Authorization header
  • The reverse proxy verifies the JWT using a public RSA key
  • It extracts the tier claim (e.g., "free" or "subscribed")
  • Based on that, it routes the request to the appropriate backend service

All services are containerized and deployed in Minikube.

Key Components with Code Examples

🔁 Reverse Proxy (Go)

Here's how the reverse proxy is created:

target, _ := url.Parse(targetURL)
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.ModifyResponse = func(resp *http.Response) error {
    log.Printf("Response from backend: %d", resp.StatusCode)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Custom Director logic modifies the request:

proxy.Director = func(req *http.Request) {
    req.Header.Set("X-Forwarded-For", req.RemoteAddr)
    req.URL.Scheme = target.Scheme
    req.URL.Host = target.Host
    log.Printf("Forwarding request to: %s", req.URL.String())
}
Enter fullscreen mode Exit fullscreen mode

Routing rules come from a YAML config:

routes:
  free: http://free-tier-service:9000
  subscribed: http://subscribed-tier-service:9000
Enter fullscreen mode Exit fullscreen mode

This is loaded in Go as:

type Config struct {
    Routes map[string]string `yaml:"routes"`
}
Enter fullscreen mode Exit fullscreen mode

🔐 JWT Auth

JWT tokens are RSA-signed and validated using the public key:

func validateJWT(r *http.Request) (jwt.MapClaims, error) {
    tokenString := extractToken(r)
    pubKeyData, _ := ioutil.ReadFile("public.pem")
    pubKey, _ := jwt.ParseRSAPublicKeyFromPEM(pubKeyData)

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return pubKey, nil
    })
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return claims, nil
    }
    return nil, err
}
Enter fullscreen mode Exit fullscreen mode

Token is extracted like so:

func extractToken(r *http.Request) string {
    bearer := r.Header.Get("Authorization")
    return strings.TrimPrefix(bearer, "Bearer ")
}
Enter fullscreen mode Exit fullscreen mode

🧭 Routing Logic

Once the tier is extracted, routing is handled like this:

claims, _ := validateJWT(r)
tier := claims["tier"].(string)
targetURL := routeConfig.Routes[tier]

proxy := reverseProxy(targetURL)
proxy.ServeHTTP(w, r)
Enter fullscreen mode Exit fullscreen mode

🛠️ Backends

Two simple backend services:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def free_home():
    return {"msg": "Hello Free-tier User 🌱"}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=9000)
Enter fullscreen mode Exit fullscreen mode
from flask import Flask
app = Flask(__name__)

@app.route("/")
def subscribed_home():
    return {"msg": "Hello Subscribed User 🚀"}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=9000)
Enter fullscreen mode Exit fullscreen mode

Each backend is a basic HTTP server with a unique response to verify routing.


🧪 Common Real-World Use Cases

  • SaaS platforms with "free" vs. "premium" tiers controlling access to APIs
  • Content streaming services that serve different quality or features based on the user's subscription
  • E-learning portals gating content behind subscriptions or course enrollments
  • Mobile backend systems offering tier-based feature access
  • Enterprise APIs that segment functionality by internal department or external partner level

🌐 Real-World Considerations

Here’s where things get a bit more nuanced 🔍:

In a real-world system, user identity and tier access should be validated and persisted both at the authentication system and on the backend services.

JWT Signature & PKI

Since JWTs are signed using PKI (public/private key cryptography), any attempt by a user to modify the token’s claims (like the tier) will break the signature verification. This means:

The backend can (and should) verify the JWT signature itself to ensure the token is genuine and untampered.

If the signature check fails, the request must be rejected.

This prevents users from forging or altering their JWTs to escalate privileges.

Why Backend Validation Matters

Even though the proxy validates the JWT, backends should never blindly trust the proxy. The proxy can be bypassed if someone calls the backend API directly.

Backends should perform their own JWT validation or check with a centralized auth system.

Token Freshness and Revocation

JWTs are stateless, meaning once issued, they carry claims until expiry.

If user privileges change (e.g., downgrade from subscribed to free), the backend needs a way to invalidate old tokens or verify claims against a database.

This can be done with short token lifetimes, token revocation lists, or real-time claim checking.

Proxy as a Single Point of Failure

The proxy could become a bottleneck or single point of failure.

For production, consider dedicated API gateways (e.g., Kong, Ambassador, Istio) that offer robust features like rate limiting, observability, and retries.

✋ Limitations

  • No HTTPS (for now)
  • Static public key loading (no hot reload or key rotation)
  • Basic error handling
  • No rate limiting, observability, or distributed tracing
  • No token revocation or refresh support

Still, it’s a clean and focused implementation that illustrates the core ideas beautifully.


🚧 Source Code & Deployment

You can find the full source code on my Github

To deploy it in Minikube:

  • Build the Docker images for the proxy and backends
  • Apply the Kubernetes manifests
  • Test with curl and different JWTs

💭 Final Thoughts

This was a fun project to build and learn from. If you’re new to Go or want to understand JWT-based routing and reverse proxy logic, I highly recommend trying something like this. Yes, it’s basic — but it’s real, working code, and you’ll walk away with actual insights.

Let me know what you think! Feedback is welcome — just keep in mind that this was built for educational purposes, not production 🚀

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more