JSON Web Tokens (JWT) are widely used for securing APIs and managing identity and access. While their primary role is to authenticate users, JWTs can also support fine-grained authorization — making it possible to control access down to the resource, action, or field level. This blog explores how to implement permission granularity using JWT in a secure and scalable way.


What Is Fine-Grained Access Control?

Fine-grained access control (FGAC) goes beyond coarse rules like “admin vs user” roles. It enables you to define access at the level of:

  • Specific endpoints or API operations (e.g., /invoices/view)
  • Specific fields in a document or data model
  • Time, context, or tenant-specific restrictions

Instead of saying who can access the system, FGAC defines what each user can do within it.


JWT Basics Refresher

A JSON Web Token has three parts:

<Header>.<Payload>.<Signature>

The Payload typically contains claims like:

{
  "sub": "user123",
  "role": "editor",
  "permissions": ["read:articles", "edit:comments"],
  "iat": 1715751981,
  "exp": 1715755581
}

The permissions claim here illustrates a simple form of authorization. These claims can be validated server-side without additional database queries, supporting stateless authentication.


Using JWT for Permission Granularity

JWT can encode complex authorization models by including structured claims. For example:

{
  "sub": "alice",
  "department": "finance",
  "permissions": {
    "invoices": {
      "view": true,
      "edit": false
    },
    "reports": {
      "view": true,
      "export": true
    }
  }
}

This design allows you to:

  • Limit access by resource type (invoices, reports)
  • Restrict actions per resource (view, edit, export)
  • Apply policy logic directly in the backend using decoded JWT claims

Mermaid Flowchart: JWT Access Evaluation Logic

flowchart TD A[User sends request with JWT] --> B[API Gateway or Backend Decodes JWT] B --> C[Extract permissions claim] C --> D{Is resource and action allowed?} D -- Yes --> E[Allow Request] D -- No --> F[Return 403 Forbidden]

This logic is usually executed inside a middleware layer in Node.js, Python, Java, or Go APIs.


Case Study: Role + Permission-Based Claims

Scenario: A company has three roles: viewer, editor, and admin. Each role has different access levels.

JWT payload example for an editor:

{
  "sub": "editor42",
  "role": "editor",
  "permissions": ["articles:read", "comments:edit"]
}

On the backend:

function checkPermission(tokenPayload, resource, action) {
  const permissionKey = `${resource}:${action}`;
  return tokenPayload.permissions.includes(permissionKey);
}

This design avoids repeated lookups and supports microservice-friendly architectures.


Security Considerations 🔒

  • Never trust client-side JWTs blindly — always verify the signature.
  • Use short expiration times and rotate signing keys periodically.
  • Encrypt sensitive claims if your token contains confidential data (e.g., with JWE).
  • Avoid token bloat: excessively large tokens degrade performance and leak metadata.

Alternatives and Extensions

  • Policy-as-code: Tools like OPA (Open Policy Agent) allow decoupled policy evaluation.
  • Attribute-Based Access Control (ABAC): Add context like time, IP, or location into claims.
  • Scopes vs. Permissions: OAuth 2.0 uses scopes, but scopes are often too broad for fine-grained control.

Final Thoughts

JWTs are more than just authentication tokens. When used wisely, they become powerful vehicles for precise, low-latency access control. This makes them ideal for modern, distributed applications — especially in zero-trust environments.

➡️ What is the balance between token size and permission depth in your system? ➡️ Can dynamic permissions (e.g., project-based access) be encoded securely in JWTs, or is an external policy engine required?

Fine-grained access is no longer a luxury. It’s a necessity — and JWT can be your ally.