The third time a customer reported “I’m logged in but your API says I’m not,” we finally stopped patching and asked the question we should have asked after the first report: why does this keep happening in a different service every time?
The answer was uncomfortable. We had three services — the public API, the partner integrations service, and an internal admin tool — and each one had its own copy of the JWT validation logic. Not a shared library with three call sites. Three separate implementations, written by three people, at three different points in the company’s history, each one a slightly different interpretation of “check the token.” One validated the exp claim with a 30-second leeway for clock skew. One didn’t allow any leeway at all. One checked token revocation against a cache that refreshed every five minutes; the other two checked it on every request. None of this was documented anywhere — you only found out the behavior differed by hitting the edge case.
The bug itself was almost mundane: a token issued right at the boundary of a permission change would pass validation in one service and fail in another, because the revocation check timing didn’t line up. A customer’s session would look valid to the endpoint they’d used a minute ago and invalid to the one they hit next. We’d “fixed” this twice already, each time by patching the specific service that was reported broken. Both fixes were correct and both fixes were incomplete, because we were treating three symptoms of a shared design problem as three unrelated bugs.
Centralizing was the obvious answer, and the one we’d been avoiding
Centralizing auth was the obvious answer and also the part we’d been avoiding, because it touched every service and none of us wanted to own that migration. We ended up building it as a gateway-level concern rather than a shared library, for a reason that mattered more in practice than in theory: a shared library still gets forked. Someone vendors an old version, someone patches their copy under deadline pressure, and eighteen months later you have three implementations again, just with more shared git history. Pulling validation out to the edge — one gateway service that terminates auth before a request ever reaches application code — meant there was structurally only one place it could happen.
Shadow mode caught the disagreements before they were incidents
The migration took about five weeks, most of which was not writing the gateway itself but making the cutover safe. We ran the gateway in shadow mode first: it validated every request and logged what it would have decided, without actually blocking anything, while the three existing services kept doing their own checks. That surfaced real disagreements — cases where the gateway would have rejected a token one of the old services was still accepting — before any of them affected a real request. We fixed those discrepancies one at a time, in the gateway’s logic, until shadow mode ran clean for a full week. Only then did we start cutting services over, one at a time, oldest and lowest-traffic first, with the option to fail open back to the service’s own check if the gateway had an outage.
Consolidating the decision also consolidates the failure mode
The part that surprised us was the operational side, not the security side. Centralizing auth meant centralizing its failure mode too — a bug or outage in the gateway now affects every service at once instead of one. We treated that as a cost worth paying deliberately: the gateway runs on its own scaling policy, gets paged on its own error budget separate from any downstream service, and has a circuit breaker that fails open to a short-lived cached decision rather than blocking everything if its own dependency (the revocation store) is slow. Consolidating a decision doesn’t remove the risk of getting it wrong; it just moves the risk somewhere you can actually watch it.
None of this is exotic — “auth belongs at the edge, not reimplemented per service” is close to conventional wisdom. What’s easy to miss is that you don’t usually get there by architecture review. You get there because the same bug keeps showing up wearing a different service’s name, and eventually someone notices the pattern instead of just filing the third ticket.
We went through a similar consolidation on our own platform at Edilec after the same pattern showed up in our services, and it’s since become the default we reach for whenever auth logic starts spreading across more than one place — more on how we think about that kind of infrastructure work at edilec.com.
답글 남기기