When a Simple Permission Check Became an Engineering Journey
Every engineering task starts with something that sounds deceptively simple.
When a Simple Permission Check Became an Engineering Journey
Every engineering task starts with something that sounds deceptively simple.
For us, it started with one question.
Can this subject perform this action on this resource?
At first glance, it feels straightforward.
A request comes in. The system checks permissions. It returns true or false.
Simple.
Or at least, that’s what we thought.
The moment we started integrating AuthZEN into ThunderID, that “simple question” quickly turned into something much bigger.
Who exactly is the subject?
Is it a user? An application? A service account? An agent?
What does the resource represent?
A booking? A document? A tenant?
What if the permission is inherited through a group instead of being assigned directly?
And if the client asks about a resource that doesn’t even exist… is that an error, or is the answer simply no?
That was the beginning of our AuthZEN journey.
And surprisingly, the hardest part was never writing the endpoints.
It was figuring out where AuthZEN should fit inside ThunderID.
Why We Even Needed This
To understand why this integration matters, imagine a company building a travel booking platform.
Thousands of users interact with the system every day.
Some users are customers booking tickets.
Some users are support agents managing reservations.
Some users are administrators updating bookings and managing pricing.
Now imagine a customer tries to access a booking.
The application suddenly needs to answer one important question.
Can this user read booking BK-1024?

Traditionally, every application handles this differently.
One service may check user roles directly.
Another service may query a permissions database.
A third service may implement its own custom authorization logic.
Over time, authorization logic becomes scattered everywhere.
This is where AuthZEN becomes valuable.
Instead of each application deciding authorization by itself, applications ask a centralized Policy Decision Point (PDP).
The request looks like this:
{
"subject": {
"type": "user",
"id": "alice"
},
"resource": {
"type": "booking",
"id": "BK-1024"
},
"action": {
"name": "booking:read"
}
}
The Policy Decision Point evaluates the request and responds:
{
"decision": true
}
The application does not need to understand internal permission logic.
It simply asks:
Should access be allowed?
That was exactly what we wanted ThunderID to support.
The First Big Architectural Decision
Very early in implementation, we faced two possible approaches.
Option 1
Build an entirely new authorization engine specifically for AuthZEN.
Option 2
Keep ThunderID’s existing authorization system and expose it through AuthZEN-compatible APIs.
We chose the second option.
And honestly, that single decision shaped everything that followed.
ThunderID already had a mature internal authorization system built around:
- RBAC
- Roles
- Permissions
- Resource Servers
- Users
- Groups
- Transitive group membership
That system was already working well.
Rebuilding it simply for protocol compatibility would have been unnecessary.
So we made one important decision.
We are not building a new authorization engine for AuthZEN.
We are exposing ThunderID’s existing authorization model through the AuthZEN API contract.
That decision became the foundation for the entire implementation.
Understanding The Core Challenge
AuthZEN provides a standard way for applications to ask a Policy Decision Point (PDP) for authorization decisions.
A request looks like this:
{
"subject": {
"type": "user",
"id": "user1"
},
"resource": {
"type": "booking",
"id": "booking123"
},
"action": {
"name": "booking:read"
}
}
And expects a simple response.
{
"decision": true
}
Simple from the outside.
But internally, we quickly discovered an important problem.
AuthZEN and ThunderID do not model authorization in the same way.
Designing An Adapter Instead Of A Rewrite
We implemented AuthZEN as an adapter layer inside ThunderID.
This layer acts as a translator between the external AuthZEN protocol and ThunderID’s internal authorization system.
We exposed four endpoints.
GET /.well-known/authzen-configuration
POST /access/v1/evaluation
POST /access/v1/evaluations
POST /access/v1/search/action
One thing I particularly liked about AuthZEN is the discovery endpoint.
Clients can discover PDP metadata automatically without manually configuring endpoints.
Internally, the request flow became:
AuthZEN Request
↓
AuthZEN Handler
↓
AuthZEN Service Adapter
↓
ThunderID Authorization Service
↓
RBAC Engine
↓
Authorization Decision
↓
AuthZEN Response
This separation gave us flexibility.
AuthZEN became purely the API contract.
ThunderID remained the actual decision maker.
If ThunderID changes authorization engines in the future, the AuthZEN API layer remains untouched.
That separation mattered.
The Translation Layer Was Where The Real Work Happened
AuthZEN describes authorization using three concepts.
subject + resource + action
ThunderID internally evaluates authorization differently.
It expects:
subject + resource server + permission
So every AuthZEN request had to be translated.
The adapter performs mappings like:
subject.id→ internal subject IDsubject.type→ entity validationresource.type→ resource server lookupaction.name→ permission validation
For example:
{
"resource": {
"type": "booking"
},
"action": {
"name": "booking:read"
}
}
Internally becomes:
Find resource server "booking"
Validate permission "booking:read"
Check whether subject has permission
That simple boolean response suddenly had a lot happening underneath.
Problem 1: Subject Validation Is Never Perfect
In theory, every client should provide complete subject information.
Reality is rarely perfect.
Sometimes integrations only know the subject ID.
Sometimes subject type information is missing.
Sometimes external systems do not provide enough metadata.
So we made a practical decision.
Validate strictly only when enough information exists.
The rule became:
- If
subject.typeexists → validate entity type - If
subject.typeis empty → skip validation
This gave us a balance between correctness and usability.
Problem 2: Permissions Usually Come Through Groups
Permissions are rarely assigned directly.
In ThunderID, users often receive permissions through:
- Roles
- Group assignments
- Nested groups
So before authorization evaluation begins, we resolve transitive group membership.

The internal flow looks like this.
Subject ID
↓
Entity Provider
↓
Resolve Groups
↓
Find Transitive Groups
↓
Authorization Evaluation
We also remove duplicate group IDs before evaluation.
It sounds like a small optimization.
Until you start debugging repeated lookups at scale.
Then it matters a lot.
Problem 3: What Happens If The Resource Does Not Exist?
This became one of our most interesting design questions.
Suppose a client sends:
{
"resource": {
"type": "unknown-resource"
},
"action": {
"name": "booking:read"
}
}
Or requests an action that does not exist.
Should the server return an HTTP error?
Or should authorization simply deny access?
We decided on a clear rule.
Malformed request structure:
400 Bad Request
Unknown resource or invalid action:
{
"decision": false,
"context": {
"error": {
"message": "Resource not found"
}
}
}
The reasoning was simple.
Asking whether someone can perform an unknown action is still a valid authorization question.
The answer is simply no.
Problem 4: Batch Evaluation Should Not Be A Loop
AuthZEN supports batch authorization.
POST /access/v1/evaluations
The easiest implementation would have been:
Loop through each request and call single evaluation repeatedly.
We intentionally avoided that.
Instead, we built an optimized batch path.
During batch processing, we cache repeated lookups.
We cache:
- Resource server lookups
- Subject validation results
- Action validation results
- Group resolution results
Then we call ThunderID’s internal batch authorization service.
Most importantly, we preserve response order.
Because response index 1 must always match request index 1.
A small detail.
A very important detail.
Problem 5: Sometimes You Need To Know What Is Allowed
Authorization is not always:
Can this user perform this action?
Sometimes applications need to ask:
What actions can this user perform on this resource?
This is where AuthZEN action search comes in.
Endpoint:
POST /access/v1/search/action
To answer this, the system:
- Resolves the resource server
- Collects all available actions
- Includes nested resource permissions
- Removes duplicate actions
- Runs batch authorization evaluation
- Returns only allowed actions
The important thing here was consistency.
We did not build shortcuts.
We reused the same authorization engine.
That meant authorization behavior stayed consistent across every endpoint.
Where Testing Became Critical
Interestingly, the biggest complexity was not RBAC itself.
It was the translation boundary.
So most of our tests focused there.
We tested both:
Handler Layer
- Request parsing
- Invalid JSON
- Metadata discovery
- Error handling
- API validation
Service Layer
- Subject validation
- Group resolution
- Invalid actions
- Batch evaluation caching
- Authorization failures
The AuthZEN adapter was doing much more than forwarding JSON.
It was:
- validating
- resolving
- transforming
- caching
- preserving ThunderID semantics
That boundary needed confidence.
What This Project Taught Me
The biggest lesson I learned is this.
Integrating a standard is rarely about implementing API endpoints.
The hard part is choosing the right architectural boundary.
Push AuthZEN too deep into the authorization engine, and internal logic becomes polluted by protocol-specific concerns.
Make it too thin, and you lose important behavior like validation, group resolution, and batch performance optimizations.
The adapter approach gave us the best balance.
- AuthZEN remains a clean public contract
- ThunderID remains the authorization decision engine
- Developers get a standard interface without learning internal implementation details
And honestly, that was the most satisfying part.
We did not replace ThunderID’s authorization system.
We made it interoperable.
Sometimes good engineering is not about building something entirely new.
It is about making what already exists work better with the rest of the world.
That, for me, was the real story behind adding AuthZEN support to ThunderID.
메타데이터
- post_id
- fb400c0d8adf
- slug
- when-a-simple-permission-check-became-an-engineering-journey-fb400c0d8adf
- url
- https://medium.com/@yathusigakirubananthan/when-a-simple-permission-check-became-an-engineering-journey-fb400c0d8adf
- canonical_url
- https://medium.com/@yathusigakirubananthan/when-a-simple-permission-check-became-an-engineering-journey-fb400c0d8adf
- author_url
- https://medium.com/@yathusigakirubananthan
- status
- ok
- fetched_at
- 2026-08-01 04:39:42