← Back to list

When a Simple Permission Check Became an Engineering Journey

Every engineering task starts with something that sounds deceptively simple.

Yathusiga Kirubananthan · 2026-06-12 08:28 · 61 claps · 6.0 min read
#authzen #thunderid #pdp #authorization #software-engineering
Open on Medium ↗
Wiki topics: LIT · Literature & Writing 🧘 · Spirituality

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 ID
  • subject.type → entity validation
  • resource.type → resource server lookup
  • action.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.type exists → validate entity type
  • If subject.type is 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:

  1. Resolves the resource server
  2. Collects all available actions
  3. Includes nested resource permissions
  4. Removes duplicate actions
  5. Runs batch authorization evaluation
  6. 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