API Design | Nov 17, 2025 | 20 min read | By Savan Kharod

Savan Kharod works on demand generation and content at Treblle, where he focuses on SEO, content strategy, and developer-focused marketing. With a background in engineering and a passion for digital marketing, he combines technical understanding with skills in paid advertising, email marketing, and CRM workflows to drive audience growth and engagement. He actively participates in industry webinars and community sessions to stay current with marketing trends and best practices.
When people talk about API security, they usually lump everything under “auth,” but there are really two separate jobs: authentication and API authorization. Authentication is about proving who is calling your API; authorization is about deciding what that caller is allowed to do once they’re in.
A simple way to think about it is a secure office building:
Authentication is the front desk checking your badge to confirm you’re an employee.
Authorization is what happens at the turnstiles and doors: this badge opens the 3rd floor, but not the finance vault.
Your APIs work the same way. Authentication verifies the identity behind the request, API keys, JWTs, OAuth tokens, etc. Authorization then decides whether that identity can read this record, update that resource, or call a specific admin endpoint.
In this article, we’ll focus on understanding the key difference between API authorization vs authentication, and how to design access control in REST APIs for each.
We’ll walk through the main models you’ll actually use in production, roles (RBAC), attributes (ABAC), and OAuth scopes, then show how to implement simple checks in code, handle failures cleanly, and keep authentication (authN) and authorization (authZ) properly separated in your architecture.
At a practical level, API authorization is the step where your backend decides whether an authenticated caller is allowed to perform a specific action on a specific resource. After authentication has verified who the caller is, API authorization evaluates their roles, permissions, scopes, and other attributes to grant or deny access to routes, operations, and data.
You can think of this as the access control layer in REST APIs: given a request like DELETE /users/123, the authorization answers questions such as:
Is this user allowed to delete any account?
If not, are they at least allowed to delete their own account?
Are there extra constraints (e.g., feature flags, plan limits, tenant boundaries)?
Where authentication is binary (“is this token valid?”), API authorization is about granularity: “what operations, on which resources, under which conditions, are permitted for this identity?” Modern APIs usually implement this with a combination of user roles and permissions, RBAC (role-based access control), OAuth scopes, or more expressive policy engines.
Concretely, API authorization shows up in everyday use cases like:
Admin-only operations
admin role can hit destructive endpoints like DELETE /users/:id or change global configuration.Per-user data isolation
GET /users/:id only when :id matches their own user ID, or they hold a higher-privileged role.Plan- or tier-based features
GET /reports/summary, but not GET /reports/detailed; premium plans get extra endpoints or higher rate limits.Tenant or organization boundaries
org_id, even if they know another organization’s IDs.Field- and action-level controls
Under the hood, every authorization decision is typically based on four ingredients:
Subject: who is calling (user, service, API client) and what attributes they have (ID, roles, groups, scopes, tenant, risk level).
Resource: what is being accessed (endpoint, object, field), such as a specific user record or invoice.
Action: what operation is requested (read, write, delete, approve, export).
Context: extra factors like time of day, IP range, region, or device type.
API authorization evaluates these pieces against your access control rules or policies and then either allows the call to proceed or blocks it with an error (usually 403 Forbidden). When this layer is missing or misconfigured, you get broken access control, where users can act outside their intended permissions, which OWASP highlights as one of the most critical classes of web and API vulnerabilities.
Once authentication has identified who is calling, API authorization requires a consistent method to determine what that caller can do. In practice, most teams lean on three core models:
Role-Based Access Control (RBAC): permissions tied to roles
Attribute-Based Access Control (ABAC): permissions driven by attributes and policies
Scope-based access (OAuth scopes): permissions encoded into tokens
You can use them individually or combine them to build robust access control in REST APIs.
Role-Based Access Control (RBAC) is the “classic” API authorization model: instead of assigning permissions directly to each user, you define roles, associate permissions with those roles, and then assign roles to users.
RBAC implements least privilege: each role gets only the access it needs to perform its job, reducing the blast radius of compromised accounts and simplifying audits.
Clear user roles and permissions (matches org structure and job titles).
Straightforward to understand, debug, and enforce in REST APIs.
Works well for coarse-grained access: “admins vs normal users.”
Complex enterprises with many edge cases (“admin in region X, read-only in Y”).
Multi-tenant or B2B systems where rules depend on who owns what and context.
Growing sets of roles that explode into “role-per-permutation” (sales-eu-manager, sales-us-manager, etc.).
ABAC takes API authorization one step further by using attributes and policies rather than just roles. So instead of asking “Does this user have the admin role?”, ABAC asks:
“Given the user, the resource, the action, and the environment, do the attributes satisfy the policy?”
NIST defines ABAC as a logical access control model where authorization to perform operations is determined by evaluating attributes of the subject, object, action, and sometimes environment, against a set of policies.
In practice, that means you describe access rules in terms of:
Subject attributes, who is calling:
user.id, user.orgId, user.department, user.clearance, user.country, user.isOnCall.
Resource attributes, what they are touching:
resource.orgId, resource.ownerId, resource.type, resource.classification.
Action attributes, what they want to do:
action = 'read' | 'write' | 'delete' | 'approve' | 'export'.
Environment attributes, context:
timeOfDay, ipRange, geoRegion, riskScore, deviceTrustLevel.
Policies then look like if-then rules:
If user.orgId === resource.orgId and action === 'read' → allow
If user.department === 'finance' and amount < 10000 and timeOfDay in business hours → allow approve
If user.country not in ['US','EU'] and resource.classification === 'PII' → deny
ABAC is especially attractive for fine-grained, context-aware API authorization, where simply knowing someone’s role is not enough.
Provides fine-grained, context-aware API authorization using user, resource, action, and environment attributes.
Policies can be reused across services and resources, improving consistency of access control.
Models complex enterprise rules more naturally than large role hierarchies (e.g., tenant, region, sensitivity).
Higher implementation complexity: requires clear attribute models, policy language, and decision engine.
Policies can become hard to reason about or debug without good tooling and test coverage.
Performance overhead if policy evaluation is not carefully designed or cached.
Where RBAC and ABAC are usually user-centric, OAuth scopes are token-centric: they describe what a particular access token is allowed to do in your API.
Scopes are usually simple strings, each representing a capability or permission set, for example:
read:user – read user profile data
write:account – modify account information
payments:charge – create new charges
invoices:read – read invoices
invoices:read:own – read invoices that belong to the caller
In a typical OAuth-protected API:
The client (mobile app, SPA, partner integration) requests scopes during authorization.
The user/admin sees a consent screen: “This app wants to read your profile and manage your invoices.”
The authorization server issues an access token embedding those scopes.
Your API or API gateway checks: Does this token have the required scopes for this endpoint and method?
This is still API authorization, just expressed at the token level rather than the user-record level. The same human user might have admin rights in your product, but if a particular token has only read:user, then that token cannot hit admin routes.
Encodes permissions directly into tokens, enabling least-privilege access per client or integration.
Provides a standard, interoperable way to limit third-party and first-party app access in OAuth/OIDC flows.
Makes it easier to reason about what a particular token can do, independent of the full user profile.
Poorly designed scopes can become too coarse (“full_access”) or too granular, making management difficult.
Scopes typically describe capabilities, not contextual rules (tenant boundaries, ownership, time-based limits).
Requires consistent scope validation in all APIs and services that consume tokens.
Protect your APIs from threats with real-time security checks.
Treblle scans every request and alerts you to potential risks.
Explore Treblle
Protect your APIs from threats with real-time security checks.
Treblle scans every request and alerts you to potential risks.
Explore Treblle
Once you have authentication in place, API authorization becomes a matter of consistently enforcing rules like:
“Only admins can delete users, and users can delete only their own account.”
Let’s walk through that exact example for DELETE /users/:id, and then generalize the pattern.
Business rule:
Allow the request only if:
user.role === 'admin' or
user.id === params.id (the user is deleting their own account)
Everything in your authorization layer should be a clear, testable rule like this. Avoid “magic” conditions scattered across controllers.
First, you need authentication middleware that:
Validates the token/session/API key.
Attaches an identity object to the request (for example req.user in Express).
Example shape:
// after authentication
req.user = {
id: 'abcd-1234',
role: 'member', // or 'admin'
// ...other attributes if needed
};From here on, API authorization does not care how the user was authenticated, only about the resulting user object.
A clean pattern in Node.js/Express is to implement authorization as dedicated middleware, separate from the route handler.
// auth.js
const jwt = require('jsonwebtoken');
function authenticate(req, res, next) {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) {
return res.status(401).json({ error: 'Missing access token' });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// Only the minimal fields needed for API authorization
req.user = { id: payload.sub, role: payload.role };
return next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
module.exports = { authenticate };This handles authentication, not authorization.
// authz.js
function canDeleteUser(req, res, next) {
const authUser = req.user; // set by authenticate()
const targetUserId = req.params.id; // /users/:id
if (!authUser) {
// Should normally be caught by authenticate(), but guard anyway
return res.status(401).json({ error: 'Unauthenticated' });
}
const isAdmin = authUser.role === 'admin';
const isSelf = authUser.id === targetUserId;
if (!isAdmin && !isSelf) {
// Authenticated but not allowed → API authorization failure
return res.status(403).json({ error: 'Forbidden' });
}
// Authorization successful; move to the actual handler
return next();
}
module.exports = { canDeleteUser };// users.routes.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('./auth');
const { canDeleteUser } = require('./authz');
// DELETE /users/:id
router.delete('/users/:id', authenticate, canDeleteUser, async (req, res) => {
const { id } = req.params;
// Business logic: delete from DB, emit events, etc.
await deleteUserFromDb(id);
return res.status(204).send(); // No Content
});
module.exports = router;What this gives you:
Separation of concerns
authenticate → “Who is this?”
canDeleteUser → “Is this user allowed to delete this account?”
Route handler → business logic only.
Easier testing: you can unit-test canDeleteUser with simple mocks for req.user and req.params.id.
Clear, reusable API authorization rules that are not buried in controller code.
For role-based checks, you can create generic helpers and reuse them across routes:
// authz-role.js
function requireRole(...allowedRoles) {
return function (req, res, next) {
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Unauthenticated' });
}
if (!allowedRoles.includes(user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
return next();
};
}
module.exports = { requireRole };Usage:
const { authenticate } = require('./auth');
const { requireRole } = require('./authz-role');
// All /admin routes require authentication + admin role
app.use('/admin', authenticate, requireRole('admin'), adminRouter);This keeps role logic centralized and makes your access control in REST APIs much easier to audit.
By consistently applying this middleware pattern, you turn “who can do what” into a set of small, composable units. That keeps your API authorization predictable, testable, and easy to evolve as you add new roles, scopes, or policies.
Protect your APIs from threats with real-time security checks.
Treblle scans every request and alerts you to potential risks.
Explore Treblle
Protect your APIs from threats with real-time security checks.
Treblle scans every request and alerts you to potential risks.
Explore Treblle
Authorization failures are normal in any secured API. The key is to handle them consistently, safely, and in a way that’s easy for both clients and operators to reason about.
Always distinguish between authentication and authorization failures in your responses:
401 Unauthorized (actually “Unauthenticated”): Use this when the client has not provided valid credentials. According to MDN and HTTP specs, 401 means the client must authenticate itself to get the requested response.
Authorization header, invalid/expired token.403 Forbidden: Use this when the request is understood and the user is authenticated, but your API authorization logic denies access. Further authentication will not change the outcome.
This distinction helps client developers debug correctly and aligns with HTTP semantics.
For access control failures, aim for:
A stable JSON structure (e.g., { "error": "Forbidden", "reason": "missing_permission:user:delete" }).
Messages that are clear enough for legitimate clients but do not leak sensitive details (stack traces, SQL, internal IDs, or policy internals).
OWASP recommends generic error messages for security-related failures to avoid exposing implementation details that could aid an attacker.
Good patterns:
// 401 example
{
"error": "Unauthenticated",
"message": "Valid access token is required."
}
// 403 example
{
"error": "Forbidden",
"message": "You do not have permission to perform this action."
}If you need more detail for internal clients, prefer machine-readable reasons (code, reason) over verbose human text that reveals too much.
Avoid scattering res.status(403)... or equivalent all over your codebase. Instead:
Centralize API authorization checks (middleware, decorators, or a shared helper).
Centralize error handling so failed checks are processed in one place.
OWASP’s Authorization Cheat Sheet explicitly recommends centralizing failed access-control handling to avoid inconsistent behavior or edge cases that could lead to bypasses.
In practice:
Middleware throws or returns a standardized “not allowed” error.
A global error handler converts that into a 403 response with your canonical JSON format.
From a security perspective, denied requests are as important as successful ones:
Log who attempted the action (user ID / client ID, tenant).
Log what they tried to do (method, path, resource ID).
Log why it failed (missing role, missing scope, failed policy).
OWASP’s logging and monitoring guidance recommends capturing access control failures with enough context to identify suspicious behavior and support incident response.
You can then:
Detect brute-force ID enumeration (repeated 403s on different object IDs).
Spot misconfigured roles or policy bugs.
Correlate with other events in your observability stack.
When an authorization check fails:
Abort the operation as early as possible, before any stateful side effects (writes, external calls, queued jobs).
Ensure the code path exits cleanly.
OWASP notes that improper handling of failed access-control checks can leave an application in an unpredictable or insecure state.
A good pattern is:
Authenticate → attach user to request.
Authorize → if denied, immediately return 403.
Only if authorized → execute business logic.
Handled well, authorization failures become a predictable part of your API authorization story: clients get clear signals (401 vs 403), attackers learn nothing useful from responses, and your logs give you full visibility into “who tried to do what and was blocked.”
Good API authorization is more than sprinkling if (user.role === 'admin') across handlers. You want access control in your REST APIs to be consistent, explainable, and easy to evolve as roles, teams, and features change. OWASP classifies broken access control as one of the most critical risks, precisely because ad-hoc authorization is so easy to get wrong.
The API Authorization best practices below give you a durable structure for “who can do what” without turning every change into a migration project.
Start by treating least privilege as a design constraint, not an afterthought: each user, service, and token should have only the access necessary to perform its task. NIST explicitly calls this out as a core control for access management.
In practical terms, that means:
Define roles, permissions, and policies once, in a central source of truth (an IAM service, database, or config), rather than hardcoding them per service.
Default to deny for anything that is not explicitly allowed.
Keep role and permission definitions auditable so you can answer “who can do what?” without grep across microservices.
When least privilege and centralization are in place, changing access is mostly configuration work rather than editing business logic everywhere.
Authentication and authorization solve different problems and should be implemented in separate layers:
Authentication (authN) verifies the caller (API key, JWT, OAuth token, session).
Authorization (authZ) decides which actions and resources that caller is allowed to use.
In code, this typically looks like:
An authN middleware/guard that validates credentials and attaches identity (user, claims) to the request.
A separate authZ middleware/policy call that uses that identity (roles, scopes, attributes) to allow or deny specific operations.
OWASP explicitly notes that access control checks must be performed after authentication and in centralized, server-side logic.
This separation keeps the API authorization logic testable, reusable, and much less likely to be bypassed by accident.
Ad-hoc if statements like “if (user.role === 'manager' && user.department === 'finance' && amount < 10000) { ... }” don’t scale; over time, they become hard to audit and easy to misalign across services.
A more robust pattern is to express authorization rules as policies:
Define rules in a policy engine, config file, or dedicated authorization service (RBAC/ABAC).
Evaluate them via a single decision point, e.g. authorize(user, action, resource, context).
Keep policy definitions under version control and test them like code.
By externalizing rules, you can change who can do what without redeploying every API, and you reduce the risk of subtle differences between services.
When you rely on OAuth 2.0 or OpenID Connect, scopes and token claims are core inputs to API authorization:
Scopes describe what the token is allowed to do (e.g. read:user, payments:charge).
Claims carry identity and attributes (roles, tenant IDs, org IDs) that your RBAC/ABAC rules depend on.
Good practice here includes:
Designing scopes around business capabilities rather than arbitrary strings, and requesting only the minimal scopes needed.
Enforcing required scopes at the API or gateway level for each endpoint.
Combining scopes with internal roles/policies: the token might have invoices:read, but policies still decide which invoices are visible to this caller.
Used this way, scopes and claims help you maintain least privilege per token, not just per user.
Authorization is not just about blocking calls; it is also about knowing what is happening and limiting blast radius when something goes wrong.
From a monitoring perspective:
Log both successful and failed authorization decisions: who, what, and why the decision was made.
Feed these logs into your observability stack so you can detect anomalies (spikes in 403s, repeated access to admin routes, unusual tenants or regions).
From a data-exposure perspective:
Return only the fields the caller genuinely needs; don’t expose full internal models by default.
Apply field-level controls or masking for sensitive data (PII, payment details, secrets), so even correctly authorized users see only what their role requires.
OWASP’s guidance on broken access control and logging makes it clear: robust API authorization is not just a gate, but a combination of strict rules, visibility into how they are used, and restraint in what data each caller is allowed to see.
See how your APIs are adopted and used with clean, actionable data.
Treblle gives you dashboards and insights built for APIs.
Explore Treblle
See how your APIs are adopted and used with clean, actionable data.
Treblle gives you dashboards and insights built for APIs.
Explore Treblle
If authentication tells you who is calling your API, API authorization is everything that follows: the rules, checks, and guardrails that decide who can do what, where, and under which conditions across your system. Most real-world breaches like the recent FIA Cyber Breach are often around APIs that don’t come from missing logins; they come from broken access control, users being able to see or do more than they should because authorization was bolted on as an afterthought.
OWASP explicitly lists Broken Access Control as a top application and API risk for exactly this reason.
Start with clear, testable rules (“admins can delete any user”, “users can only read their own data”), implement them using a combination of RBAC, ABAC, and OAuth scopes, and keep authentication and authorization as separate layers in your architecture.
As your product grows, you can evolve from simple role checks to policy-driven, attribute-based decisions without rewriting every handler, as long as you’ve treated API authorization as a first-class concern from the start.
Where tools like Treblle help is in everything that happens around those checks:
Full-fidelity observability for every request, so you can see which roles, tokens, and clients are hitting which endpoints, with which status codes, in real time.
Access and error analytics, to spot patterns like repeated 403s, unusual access to sensitive routes, or tenants that suddenly expand their footprint.
Sensitive data masking and compliance awareness, so even when authorization is correct, you are not over-exposing PII or regulated fields in logs, traces, or responses, which is critical for GDPR/PCI-style obligations.
The net result is that authorization stops being a black box. You can define “who can do what” in code and policies, and then see how those decisions play out in production: which rules are hit, who was blocked, where access patterns look suspicious, and where data might need tighter control.
Even if your API feels “simple” today, investing in a clear authorization model plus runtime visibility pays off quickly. It reduces the risk of silent privilege creep, makes audits and debugging easier, and gives you the confidence to ship new endpoints knowing you can enforce and observe the right boundaries from day one.
Protect your APIs from threats with real-time security checks.
Treblle scans every request and alerts you to potential risks.
Explore Treblle
Protect your APIs from threats with real-time security checks.
Treblle scans every request and alerts you to potential risks.
Explore Treblle
API DesignRate limiting sets hard caps on how many requests a client can make; throttling shapes how fast requests are processed. This guide defines both, shows when to use each, and covers best practices.
API DesignUnmanaged API growth produces shadow endpoints you can’t secure or support. This guide explains how sprawl creates blind spots, the security and compliance risks, and a practical plan to stop it at the source.
API DesignAPI caching is more than a speed boost, it’s a strategic advantage. In this guide, we break down enterprise caching architecture, key performance benefits, and how tools like Treblle can help teams monitor and optimize their caching layers.