WEB API's Lesson 18 – Authorization | Dataplexa
Web APIs · Lesson 18

Authorization

Build systems that decide exactly what authenticated users can and cannot do with your API endpoints.

Slack's API handles millions of requests daily across thousands of workspaces. Every message sent, every file uploaded, every channel created triggers authorization checks happening in milliseconds. The user might be authenticated through OAuth, but authorization determines whether they can delete messages in a specific channel, invite users to that workspace, or access private conversations.

Authentication answers "who are you?" Authorization answers "what can you do?" These two concepts work together but solve completely different problems.

Think of authentication as your employee badge getting you through the front door of a building. Authorization is the different key cards that let you into specific floors, meeting rooms, or restricted areas once you are inside.

The APIForge Security team just finished building their authentication system using JWT tokens. Now they need authorization to control what different user roles can do with their developer platform APIs. Some users should read documentation, others should create API keys, and admin users should manage billing across multiple teams.

The Authorization Problem

Authorization becomes critical the moment your API serves more than one type of user. A social media API needs different permissions for regular users posting content versus moderators deleting posts versus administrators managing the entire platform.

Without proper authorization, your API becomes an all-or-nothing system. Users either have complete access to everything or no access at all. This creates massive security risks and makes it impossible to build applications with different user roles.

GitHub's API demonstrates sophisticated authorization in action. Repository owners can push code and manage settings. Collaborators might have write access but cannot delete the entire repository. Public repositories allow anyone to read code but not modify it. Each permission maps to specific API endpoints and HTTP methods.

Authorization Without Authentication

Some APIs use authorization without traditional authentication. Public APIs might authorize based on request origin, rate limiting by IP address, or allowing certain operations for anonymous users while requiring authentication for others.

Core Authorization Concepts

Authorization systems revolve around three fundamental concepts that determine access control. Understanding these concepts helps you design APIs that scale from simple personal projects to enterprise platforms serving millions of users.
Subject
Action
Resource
Policy Engine
Decision

The subject represents who or what is making the request. This could be a user account, an API client, a service account, or even an anonymous visitor. Subjects carry attributes like user ID, role, team membership, or subscription level.

The action describes what the subject wants to do. In HTTP APIs, actions often map to HTTP methods like GET, POST, PUT, or DELETE. But actions can be more granular than HTTP methods - like "invite user," "export data," or "change billing settings."

The resource identifies what the subject wants to access or modify. Resources can be specific entities like "user account 12345" or "document abc-def" or broader categories like "all team members" or "billing information."

Concept What it controls APIForge example
Roles Collections of permissions assigned to users Developer, Team Lead, Admin roles
Permissions Specific actions users can perform create_api_key, view_analytics, manage_billing
Scopes Boundaries that limit access to specific resources team:frontend, project:mobile-app
Policies Rules that determine when permissions apply Only during business hours, if user owns resource

Role-Based Access Control (RBAC)

Most APIs start with role-based access control because it matches how organizations naturally think about permissions. Instead of assigning individual permissions to each user, you create roles that bundle related permissions together.

Stripe's API uses RBAC extensively. A "Developer" role might include permissions to create test charges and view API logs. A "Finance" role could access real transaction data and generate reports. An "Administrator" role would have permissions to manage account settings and invite team members.

RBAC works well when you can predict the types of users your API will serve. But it starts breaking down when you need more granular control or when roles overlap in complex ways.

// APIForge RBAC permission check before creating API keys
const user = await getCurrentUser(token);
const requiredRole = 'developer';
const requiredPermission = 'create_api_key';

if (user.role === requiredRole || user.permissions.includes(requiredPermission)) {
  // Allow API key creation
  const newApiKey = await generateApiKey(user.id);
  return { success: true, apiKey: newApiKey };
} else {
  return { error: 'Insufficient permissions', code: 'FORBIDDEN' };
}
HTTP/1.1 403 Forbidden Content-Type: application/json { "error": "Insufficient permissions", "code": "FORBIDDEN", "details": { "required_permission": "create_api_key", "user_role": "viewer", "user_permissions": ["view_documentation", "view_analytics"] } }

What just happened?

The API checked whether the authenticated user has the necessary role or permission to create API keys. Since the user only has "viewer" level access, the request was denied with a 403 Forbidden status.

Try this: Design three roles for a blogging API - Reader, Author, and Editor - and list what permissions each role should have.

Attribute-Based Access Control (ABAC)

When role-based systems become too rigid, attribute-based access control provides fine-grained authorization using multiple attributes about the user, resource, and context.

ABAC policies can consider time of day, user location, device type, resource sensitivity level, and countless other attributes. A policy might allow file deletion only if the user created the file, it is during business hours, and the user is connecting from a corporate network.

Google Workspace APIs use sophisticated attribute-based authorization. Whether you can access a document depends on your organization role, the document's sharing settings, your relationship to the document owner, and sometimes even your physical location.

RBAC Approach

User has "Manager" role, so they can approve expenses. Simple rule based on assigned role.

ABAC Approach

User can approve expenses if they manage the department, expense is under $5000, and request was submitted during business hours.

Resource-Level Authorization

Resource-level authorization controls access to specific entities rather than broad categories. This becomes essential when users should only access resources they own or have been explicitly granted access to.

Consider a project management API where users can belong to multiple teams and work on different projects. A user might have "read" access to Project A, "write" access to Project B, and "admin" access to Project C. Their role alone cannot determine what they can do - you need to check permissions for each specific resource.

Notion's API exemplifies resource-level authorization. Users can have different permissions for different pages, databases, and workspaces. Some pages might be shared with read-only access while others allow full editing capabilities.

// APIForge resource-level check for team analytics access
async function checkAnalyticsAccess(userId, teamId) {
  const membership = await TeamMembership.findOne({
    user_id: userId,
    team_id: teamId
  });
  
  if (!membership) {
    return { allowed: false, reason: 'NOT_MEMBER' };
  }
  
  if (membership.role === 'admin' || membership.permissions.includes('view_analytics')) {
    return { allowed: true, level: membership.role };
  }
  
  return { allowed: false, reason: 'INSUFFICIENT_PERMISSIONS' };
}
{ "allowed": true, "level": "team_lead", "permissions": [ "view_analytics", "export_reports", "manage_projects" ], "team_name": "Backend Engineering", "access_expires": null }

What just happened?

The API verified the user's membership in a specific team and checked their permissions within that team context. The user has team lead privileges, which includes analytics access.

Try this: Design a permission system where users can have different roles in different projects within the same organization.

HTTP Status Codes for Authorization

Authorization failures need clear HTTP status codes so clients understand exactly what went wrong and how to fix it. The difference between 401 and 403 matters enormously for API usability.

Status code 401 Unauthorized means authentication is required or has failed. The client needs to provide valid credentials or refresh their token. Status code 403 Forbidden means the client is authenticated but lacks permission for the requested action.

Many APIs incorrectly return 401 for authorization failures, which confuses client developers. If a user is logged in but cannot access admin endpoints, that is a 403 Forbidden, not 401 Unauthorized.

Common Status Code Confusion

Many developers use 401 for all access denials, but this creates poor client experiences. A mobile app receiving 401 might prompt for login credentials when the user just needs higher privileges.

Status Code When to use Client action
401 Unauthorized Missing or invalid authentication Provide credentials or refresh token
403 Forbidden Authenticated but insufficient permissions Request higher privileges or different resource
404 Not Found Resource does not exist or user cannot see it Check resource ID and permissions
429 Too Many Requests Rate limit exceeded for this user/resource Wait and retry, possibly with higher rate limit

Authorization Headers and Tokens

Authorization information travels through HTTP headers, usually alongside authentication tokens that carry permission data. JWT tokens can embed role and permission claims, while OAuth tokens reference permissions stored server-side.

The Authorization header serves double duty - it provides authentication credentials and often contains authorization context. A JWT might include both user identity and their granted permissions in a single token.

Some APIs use separate headers for fine-grained authorization context. A user might authenticate with one token but specify which organization or project context applies to their current request using additional headers.

# APIForge API call with authorization context headers
GET /api/v1/teams/backend/analytics HTTP/1.1
Host: api.apiforge.dev
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Team-Context: backend-engineering
X-Permission-Scope: analytics:read
Content-Type: application/json
HTTP/1.1 200 OK Content-Type: application/json X-Permission-Used: analytics:read X-Rate-Limit-Remaining: 450 { "team_id": "team_backend_eng", "analytics_period": "last_30_days", "api_calls": 125000, "error_rate": 0.02, "top_endpoints": [ "/api/v1/auth/login", "/api/v1/projects", "/api/v1/users/profile" ] }

What just happened?

The API validated the user's JWT token and checked their permissions against the specific team context. The X-Permission-Used header confirms which permission was applied for this request.

Try this: Design headers that would let a user specify they want to act on behalf of a specific client account they manage.

Implementing Authorization Middleware

Authorization middleware sits between your API endpoints and business logic, making permission decisions before any sensitive operations execute. Well-designed middleware makes authorization transparent to your main application code.

Express.js applications commonly implement authorization as middleware functions that can be composed and reused across different routes. The middleware extracts user information from tokens, loads their permissions, and either allows the request to proceed or returns an authorization error.

Modern authorization middleware often integrates with external services like Auth0, AWS IAM, or Google Cloud IAM to avoid building complex permission systems from scratch. These services handle the hard parts while your middleware focuses on enforcing decisions.

// APIForge authorization middleware with role and resource checking
function requirePermission(permission, resourceType) {
  return async (req, res, next) => {
    try {
      const user = req.user; // Set by authentication middleware
      const resourceId = req.params.id;
      
      // Check role-based permission
      if (user.permissions.includes(permission)) {
        return next();
      }
      
      // Check resource-specific permission
      if (resourceType && resourceId) {
        const hasResourceAccess = await checkResourcePermission(
          user.id, resourceType, resourceId, permission
        );
        if (hasResourceAccess) {
          return next();
        }
      }
      
      return res.status(403).json({
        error: 'Insufficient permissions',
        required: permission
      });
    } catch (error) {
      return res.status(500).json({ error: 'Authorization check failed' });
    }
  };
}
// Usage in route definitions: app.delete('/api/v1/projects/:id', authenticate, requirePermission('delete_project', 'project'), async (req, res) => { // Project deletion logic here res.json({ success: true }); } );

What just happened?

The middleware creates reusable authorization logic that checks both general permissions and resource-specific access. Routes can easily require specific permissions without repeating authorization code.

Try this: Create middleware that checks if a user can only modify resources they created, with exceptions for admin users.

Authorization Testing Strategies

Authorization logic contains some of the most critical security code in your application, making thorough testing essential. Authorization bugs can expose sensitive data or allow unauthorized actions that damage your users and business.

Authorization tests should cover positive cases where access is granted, negative cases where access is denied, and edge cases around permission boundaries. Test with different user roles, resource ownership scenarios, and permission combinations.

Automated testing becomes crucial because manual testing cannot cover all permission combinations reliably. A single API endpoint might need different authorization behavior for administrators, resource owners, team members, and anonymous users.

Security Testing Focus

Authorization testing should include attempts to access resources without permission, privilege escalation attempts, and boundary testing around role transitions. Security researchers often find authorization bugs in edge cases that normal usage never triggers.

The APIForge Security team built comprehensive authorization across their developer platform. Users authenticate once but have different permissions for different teams and projects. Developers can create API keys for their projects, team leads can view analytics for their teams, and administrators can manage billing across the entire organization. Each API endpoint validates not just who the user is, but exactly what they are allowed to do in the current context.

Quiz

1. An APIForge developer tries to access team billing information but receives an error. They are logged in with a valid JWT token but have a "Developer" role instead of "Admin". What HTTP status code should the API return?

2. What type of authorization check should APIForge implement when a user requests analytics data for a specific team?

3. What is the main difference between Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC) in API authorization?

Up Next
API Keys
The APIForge team implements API key authentication for service-to-service communication and third-party integrations.