Skip to main content

Building Strict Authorization for Internal APIs with Spring Boot

00:03:28:19

Knowing who you are isn't knowing what you can do

Our internal APIs handled things you don't want leaking — customer records, pricing, inventory positions. Authentication was solved and had been for years. Every request arrived with a verified identity attached.

Authorization was the part nobody owned. Each service had grown its own permission checks, at whatever moment its team first needed one. Some read a role off the token. Some called a shared service that had since been deprecated. At least one checked a hardcoded list of usernames. None of them agreed on what a permission was, and because the logic lived in a dozen places, fixing a flaw in one didn't fix it anywhere else.

I was asked to build something central that all of them could use.

What I decided before writing code

Deny by default. A service gets no access unless something explicitly grants it. This is easy to say and genuinely annoying to live with during migration, which is the point — it surfaces every implicit permission that was quietly being relied on.

Finer grain than roles. Role-based access falls apart the moment a rule depends on the thing being accessed rather than the person accessing it. We needed attribute-based rules for the cases where "who you are" wasn't a sufficient answer.

One annotation. If protecting an endpoint takes more than a line, people will skip it under deadline, and a security control people skip isn't a control. The bar was: a developer should be able to secure an endpoint without reading the auth service's documentation.

Fast, because it's on every path. This check runs on every call to every protected API. Latency here isn't a feature cost, it's a tax on the entire platform.

How it's put together

Developers annotate an endpoint and the engine takes over. Permissions are evaluated against a hierarchical model that supports wildcards and conditions, so a rule can be written broadly and narrowed where a specific case demands it.

The hierarchy is the piece I'd defend hardest. Flat permission lists look simpler right up until you have a few thousand of them and no way to reason about what a given service can actually reach.

Making it fast

Three layers. Caffeine in-memory first, with a five-minute TTL. Redis behind it at fifteen minutes. The database only when both miss.

That gets 99% of authorization checks under 5ms, at about a 90% cache hit rate.

TTLs on a permission cache are an uncomfortable trade, and it's worth being honest about what it means: revoking a permission isn't instant. It propagates within the TTL window. For our threat model that was acceptable, and it was a deliberate decision rather than an oversight — but it's the first thing I'd revisit if the requirements changed.

What it turned into

Fifty-plus internal services adopted it inside six months. Sub-5ms at the 99th percentile. No authorization-related security incidents in production since. Compliance reporting got a single audit trail to read instead of a dozen log formats to correlate.

The adoption number is the one I'm actually proud of, because nobody was forced. Teams migrated because deleting their own half-maintained permission code was more appealing than keeping it.

Things that aren't the annotation

New principals start with nothing and have to ask for what they need. Permissions can carry expiry dates, which turned out to matter more than expected — most temporary access is granted during an incident and then never revoked, and having it lapse on its own closes a gap that no amount of process discipline reliably closes.

And this service is one layer, not the layer. Network isolation, mutual TLS between services, and per-principal rate limiting all still apply. A centralized auth service is a very attractive single point of failure, and it shouldn't be the only thing standing between an attacker and the data.

The thing I took from this: security and developer experience get framed as a trade-off, and mostly they aren't. The insecure shortcuts in the old system existed because the secure path was tedious. Make the secure path the shortest one and the trade-off mostly dissolves.