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:
- 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
}
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())
}
Routing rules come from a YAML config:
routes:
free: http://free-tier-service:9000
subscribed: http://subscribed-tier-service:9000
This is loaded in Go as:
type Config struct {
Routes map[string]string `yaml:"routes"`
}
🔐 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
}
Token is extracted like so:
func extractToken(r *http.Request) string {
bearer := r.Header.Get("Authorization")
return strings.TrimPrefix(bearer, "Bearer ")
}
🧭 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)
🛠️ 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)
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)
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