Authentication vs Authorization
Authentication and authorization are frequently discussed together, but they solve different security problems. Authentication establishes who or what is making a request. Authorization decides what that identity is allowed to do. Confusing the two creates serious production vulnerabilities because a valid identity does not automatically imply permission to access every resource or operation.
In distributed systems, the distinction becomes even more important. Identity may be established at an API gateway, propagated through services, and then evaluated against tenant boundaries, roles, scopes, ownership rules, and resource state. Each layer needs a clear responsibility so that access control remains consistent without duplicating security logic everywhere.
A secure production design should make authentication centralized enough to be reliable and consistent, while keeping authorization close enough to business context to make correct decisions. The challenge is balancing security, latency, availability, policy complexity, and operational maintainability.
Table of Contents
- Authentication vs Authorization: The Core Difference
- Designing the Authentication Flow
- Designing Authorization Models
- Where to Enforce Access Control
- Authentication and Authorization in Multi-Tenant Systems
- Failure Scenarios and Trade-Offs
- Production Design Example
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Conclusion
- More Articles to Read
Authentication vs Authorization: The Core Difference
The cleanest way to distinguish the two concepts is:
Authentication:
Who is making the request?
Authorization:
Can that identity perform this operation?
A request can therefore pass authentication and still fail authorization.
Request
|
v
Authentication
|
+--> invalid identity --> reject
|
v
Identity established
|
v
Authorization
|
+--> insufficient permission --> reject
|
v
Business operation
For example, a user may successfully authenticate with a valid token but still be unable to:
- read another tenant's shipment;
- delete an account;
- access an administrative endpoint;
- modify a resource owned by another user;
- perform an operation outside the token's granted scope.
| Property | Authentication | Authorization |
|---|---|---|
| Primary question | Who is this? | What can this identity do? |
| Typical inputs | Password, token, API key, certificate | Identity, roles, scopes, resource, policy |
| Typical result | Authenticated identity | Allow or deny decision |
| Common location | Gateway, identity layer, middleware | Application or policy engine |
| Failure response | Usually 401 | Usually 403 or resource-hidden 404 |
| Primary risk | Identity impersonation | Excessive or incorrect access |
Authentication should create trusted identity context. Authorization should consume that context rather than reinterpreting raw credentials repeatedly.
Designing the Authentication Flow
Authentication belongs early in the request path because invalid requests should be rejected before they consume expensive application, database, or downstream resources.
Client
|
v
TLS
|
v
API Gateway
|
v
Credential Extraction
|
v
Credential Validation
|
v
Identity Context
|
v
Application
The authentication layer should validate not only cryptographic correctness but also whether the credential belongs to the expected trust domain.
Building Trusted Identity Context
After validation, raw credentials should be converted into an internal identity representation.
{
"subject": "user-481",
"account_id": "tenant-82",
"roles": ["operator"],
"scopes": [
"shipments:read",
"shipments:create"
],
"authentication_method": "oauth"
}
This context can then move through trusted application layers.
The application should not depend directly on arbitrary headers such as:
X-User-Id
X-Account-Id
X-Is-Admin
unless those headers are created by a trusted boundary and cannot be supplied directly by external clients.
A safer pattern is:
External Request
|
v
Authentication Layer
|
| validated identity
v
Trusted Internal Context
|
v
Application
Advantages:
- authentication logic remains consistent across services;
- business code does not repeatedly parse credentials;
- security-sensitive claims have one validation boundary;
- identity context can be logged and traced safely without exposing credentials.
Disadvantages:
- identity propagation must remain trustworthy;
- gateway-generated context can become a security boundary itself;
- distributed systems need rules for service-to-service identity.
Token Validation and Session State
Token validation should verify the properties required by the credential format and trust model.
For signed bearer tokens, common checks include:
signature valid?
issuer expected?
audience expected?
token expired?
not-before valid?
required claims present?
credential revoked where applicable?
A token that has a valid signature but the wrong audience should not be accepted by an unrelated API.
Authentication architecture often involves a trade-off between stateless validation and centralized session state.
| Property | Stateless Token Validation | Central Session Lookup |
|---|---|---|
| Request latency | Low | Includes remote lookup |
| Horizontal scalability | High | Depends on session store |
| Immediate revocation | Harder | Easier |
| Dependency availability | Validation can be local | Session store is on request path |
| Operational complexity | Key lifecycle | Session storage and replication |
Short-lived tokens reduce the revocation gap in stateless systems, while centralized session stores provide stronger immediate control at the cost of another availability dependency.
Specific authentication mechanisms and credential trade-offs are covered in OAuth 2.0 vs JWT vs API Keys.
Designing Authorization Models
Authorization is more application-specific than authentication because it depends on the operation, resource, tenant, business state, and sometimes environmental context.
One system may require only a few roles, while another needs fine-grained permissions based on resource ownership and workflow state.
Role-Based Access Control
Role-Based Access Control groups permissions into roles.
User
|
+--> Role: Operator
|
+--> shipment.read
+--> shipment.create
+--> shipment.cancel
RBAC is useful when permission sets are stable and map naturally to organizational responsibilities.
Advantages:
- easy to understand;
- simple administration;
- good fit for internal applications;
- roles can bundle related permissions.
Disadvantages:
- role explosion as rules become more granular;
- roles alone do not express resource ownership;
- one role may accidentally become too broad;
- business-state rules still need separate enforcement.
RBAC should therefore often answer:
Can this identity perform this TYPE of operation?
rather than:
Can this identity access THIS exact resource?
Scopes and Permissions
Scopes and permissions provide more explicit capabilities.
shipments:read
shipments:create
shipments:cancel
users:read
users:manage
billing:read
billing:manage
A token or identity can carry only the permissions required for its workflow.
{
"subject": "integration-72",
"scopes": [
"shipments:create",
"shipments:read"
]
}
The credential cannot manage users or billing even if the owning account has those capabilities.
This limits the blast radius of credential compromise.
Scopes work particularly well for:
- API integrations;
- service accounts;
- delegated access;
- machine-to-machine APIs;
- restricting third-party clients.
Scopes still do not replace resource-level authorization.
Resource-Level Authorization
Resource authorization answers whether an identity can access a specific object.
A common insecure pattern is:
shipment = await repository.get(shipment_id)
return shipment
If the endpoint is authenticated but does not verify ownership, changing the resource ID may expose another tenant's data.
A safer query scopes access directly:
SELECT
id,
account_id,
status,
created_at
FROM shipments
WHERE id = $1
AND account_id = $2
LIMIT 1;
This creates an authorization boundary inside the data access path.
Resource authorization can include:
tenant ownership
resource ownership
team membership
role
scope
resource status
region
workflow state
For example, cancelling a shipment might require:
scope = shipments:cancel
AND
shipment.account_id = identity.account_id
AND
shipment.status IN (
draft,
booked
)
This shows why authorization belongs close to business logic: a gateway cannot know every domain rule.
Where to Enforce Access Control
Authentication can often be centralized. Authorization usually needs several layers with clearly separated responsibilities.
API Gateway
|
| authentication
| broad scopes
v
Application Service
|
| resource authorization
| business rules
v
Repository / Database
|
| tenant scope
v
Data
The gateway can efficiently enforce:
- credential presence;
- token validity;
- coarse endpoint scopes;
- general authentication policy.
The application should enforce:
- resource ownership;
- tenant access;
- domain-specific permissions;
- workflow-state rules;
- field-level restrictions where required.
The data layer can reinforce tenant boundaries:
Request
|
v
Application authorization
|
v
tenant-scoped repository
|
v
Database
Advantages of layered enforcement:
- invalid requests are rejected early;
- business rules stay close to domain context;
- data access can reinforce tenant isolation;
- one missed middleware check does not necessarily expose data.
Disadvantages:
- duplicate rules can drift;
- developers need clarity about the authoritative enforcement point;
- complex systems may require shared policy tooling.
A useful principle is:
Centralize identity verification.
Keep authorization decisions
close to the context they require.
Authentication and Authorization in Multi-Tenant Systems
Multi-tenant systems introduce another security dimension: a valid user may have legitimate access to one tenant but no access to another.
The identity should carry a trusted tenant context:
{
"subject": "user-481",
"account_id": "account-82",
"roles": ["operator"]
}
The tenant ID should come from authenticated identity or validated membership, not directly from an arbitrary request parameter.
This is dangerous:
GET /shipments?account_id=82
if the application simply trusts account_id from the query.
A safer model is:
Authenticated identity
account_id = 82
|
v
Request:
GET /shipments
|
v
Repository automatically filters:
account_id = 82
For users with access to multiple tenants, tenant switching should itself be authorized:
User
|
+--> account 82
|
+--> account 91
|
+--> account 104
Request selects account 91
|
v
Membership validation
|
v
Tenant context established
Tenant-aware authorization should also apply to indirect resources.
Shipment
|
v
Label
|
v
File
Accessing the label file should still validate that the underlying shipment belongs to the authorized tenant rather than assuming that possession of a file ID implies permission.
Failure Scenarios and Trade-Offs
Authentication and authorization are on critical request paths, so their failure behavior must be explicit.
Identity provider unavailable. APIs that validate signed tokens locally may continue serving requests until existing credentials expire. Login, token refresh, or introspection operations may fail.
Authorization service unavailable. High-risk operations should generally fail closed. Lower-risk read paths may use carefully bounded caches only if stale authorization is acceptable.
Authorization Service
|
X unavailable
|
v
Cached decision?
/ \
no yes
| |
deny validate age
|
acceptable?
/ \
no yes
| |
deny allow
Caching authorization decisions introduces a consistency trade-off. If permissions are revoked centrally, a cached decision may remain valid until its TTL expires.
Token revocation. Stateless tokens cannot always be revoked immediately without introducing a central revocation check. Short expiration periods reduce the exposure window.
Signing key rotation. Services need enough overlap to validate tokens signed shortly before rotation. Removing old public keys too quickly can invalidate active tokens.
Clock skew. Distributed systems should allow a small controlled tolerance when checking expiration and not-before timestamps, but excessive tolerance increases credential lifetime.
Permission propagation delay. In distributed policy systems, a role update may take time to reach caches or replicas. High-risk permission changes may require cache invalidation or stronger consistency.
Gateway bypass. Internal services should not assume every request necessarily came through the public gateway. Network architecture and service authentication should prevent alternate paths from bypassing identity checks.
Production Design Example
Consider a multi-tenant logistics platform supporting interactive users and external customer integrations.
Clients
/ | \
v v v
Web Mobile Partner API
\ | /
\ | /
v v v
API Gateway
/ \
v v
Authentication Rate Limits
|
v
Identity Context
|
+----------+----------+
| |
v v
Shipment Service Account Service
|
+------> PostgreSQL
|
+------> Booking Queue
There are three identities:
User Alice
account=82
role=operator
User Bob
account=91
role=admin
Integration X
account=82
scopes=
shipments:read
shipments:create
Scenario 1: Alice reads a shipment.
GET /shipments/481
Token:
subject=alice
account=82
scope=shipments:read
|
v
Authentication succeeds
|
v
Scope succeeds
|
v
Query:
id=481
AND account_id=82
|
v
Return shipment
Scenario 2: Alice guesses Bob's shipment ID.
GET /shipments/912
Token:
account=82
Shipment 912:
account=91
|
v
Tenant-scoped query returns no row
|
v
404
The system avoids revealing whether the foreign resource exists.
Scenario 3: Integration X tries to manage users.
POST /users
Integration scopes:
shipments:read
shipments:create
|
v
users:manage missing
|
v
403
Scenario 4: Alice tries to cancel a delivered shipment.
scope:
shipments:cancel
|
v
tenant:
account=82
|
v
shipment status:
delivered
|
v
business authorization denies
The user is authenticated and may even possess the correct permission, but the resource state makes the operation invalid.
Service-to-service flow.
Shipment Service publishes booking work using its own workload identity. It should not reuse Alice's external credential as infrastructure authentication.
User Identity
|
v
Shipment Service
Service Identity
|
v
Booking Queue
The user identity can still be included as audit context when useful, but infrastructure access is performed using service credentials.
Monitoring. The platform tracks authentication failures, authorization denials by reason, token-validation latency, identity-provider availability, and unusual tenant-boundary violations.
Scaling. Local token validation avoids a remote identity lookup on every request. Authorization uses application context and tenant-scoped database access without creating a centralized policy bottleneck for simple rules.
Deployment. Permission changes are treated as API behavior changes. Canary testing verifies that both allowed and denied access cases continue behaving correctly.
Ready-to-Use Example
A FastAPI service can separate identity verification from authorization so business code receives an already trusted identity object.
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
app = FastAPI()
@dataclass(frozen=True)
class Identity:
subject: str
account_id: int
roles: frozenset[str]
scopes: frozenset[str]
async def authenticate() -> Identity:
# Production implementation validates the credential:
# signature, issuer, audience, expiration, and required claims.
return Identity(
subject="user-481",
account_id=82,
roles=frozenset({"operator"}),
scopes=frozenset({
"shipments:read",
"shipments:cancel",
}),
)
Operation-level authorization can be expressed separately:
def require_scope(
identity: Identity,
required_scope: str,
) -> None:
if required_scope not in identity.scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
The repository should preserve the tenant boundary:
CREATE TABLE shipments (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_shipments_account_id_id
ON shipments (account_id, id);
The account-first index supports the authorization-aware access pattern:
SELECT
id,
account_id,
status,
created_at
FROM shipments
WHERE account_id = $1
AND id = $2
LIMIT 1;
The endpoint combines operation, tenant, and business authorization:
from enum import StrEnum
class ShipmentStatus(StrEnum):
DRAFT = "draft"
BOOKED = "booked"
IN_TRANSIT = "in_transit"
DELIVERED = "delivered"
@app.post("/shipments/{shipment_id}/cancel")
async def cancel_shipment(
shipment_id: int,
identity: Annotated[Identity, Depends(authenticate)],
) -> dict[str, str]:
require_scope(
identity,
"shipments:cancel",
)
shipment = await shipment_repository.find_for_account(
shipment_id=shipment_id,
account_id=identity.account_id,
)
if shipment is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shipment not found",
)
if shipment.status not in {
ShipmentStatus.DRAFT,
ShipmentStatus.BOOKED,
}:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Shipment cannot be cancelled",
)
await shipment_repository.cancel(
shipment_id=shipment.id,
account_id=identity.account_id,
)
return {"status": "cancelled"}
The data update should preserve the same authorization and state constraints atomically:
UPDATE shipments
SET status = 'cancelled'
WHERE id = $1
AND account_id = $2
AND status IN ('draft', 'booked')
RETURNING id;
This prevents a race where the application authorizes the resource in one state but the state changes before the write occurs.
Authorization events should also be observable:
from dataclasses import asdict, dataclass
import json
import logging
logger = logging.getLogger("security")
@dataclass(frozen=True)
class AuthorizationDecision:
subject: str
account_id: int
operation: str
resource_type: str
decision: str
reason: str
trace_id: str
def log_authorization_decision(
decision: AuthorizationDecision,
) -> None:
logger.info(
json.dumps({
"event": "authorization_decision",
**asdict(decision),
})
)
Passwords, bearer tokens, API keys, and session cookies should never be included in these events.
Common Mistakes
| Mistake | Production Impact | Better Approach |
|---|---|---|
| Treating authenticated users as fully authorized | Valid users can access unrelated resources. | Authorize every protected operation and resource. |
| Trusting user-supplied tenant IDs | Attackers can switch tenants by changing request input. | Derive tenant context from trusted identity or validated membership. |
| Checking only roles | Role membership may grant access beyond the intended resource. | Combine roles with scopes and resource rules. |
| Checking only scopes | A token may access another tenant's resource. | Enforce tenant and resource ownership separately. |
| Authorizing only at the gateway | Business and resource rules cannot be expressed accurately. | Keep domain authorization in the application. |
| Loading a resource before applying tenant scope | Ownership checks can be accidentally omitted. | Include tenant boundaries in repository queries. |
| Returning 403 for every hidden resource | Attackers can confirm that foreign resources exist. | Use 404 where resource existence should remain hidden. |
| Using long-lived broad tokens | Credential compromise creates large and persistent exposure. | Use short-lived, scoped credentials. |
| Skipping issuer or audience validation | A valid token from another trust domain may be accepted. | Validate all trust-boundary claims. |
| Trusting arbitrary identity headers | Clients may impersonate identities. | Accept identity context only from trusted boundaries. |
| Sharing user credentials between services | Infrastructure access becomes tied to external credentials. | Use workload identities for service authentication. |
| Ignoring business state | Users perform technically permitted but invalid operations. | Include domain state in authorization decisions. |
| Caching authorization indefinitely | Revoked permissions remain effective. | Use bounded TTLs and explicit invalidation where needed. |
| No authorization telemetry | Enumeration and access attempts remain difficult to detect. | Record structured allow/deny events and metrics. |
| No policy-failure strategy | Authorization outages lead to bypasses or uncontrolled downtime. | Define failure behavior for each risk class. |
Production Checklist
- Separate authentication and authorization: keep identity verification distinct from permission decisions.
- Authenticate before expensive work: reject invalid credentials early.
- Validate signatures: do not trust unsigned or incorrectly signed credentials.
- Validate issuer: accept tokens only from approved identity providers.
- Validate audience: ensure credentials were issued for the API being called.
- Validate expiration: reject expired credentials.
- Handle clock skew conservatively: avoid excessive expiration tolerance.
- Plan credential rotation: support overlapping verification keys safely.
- Create trusted identity context: avoid passing raw credentials into business logic.
- Use stable subject identifiers: avoid authorization based only on mutable usernames or emails.
- Derive tenant context securely: do not trust arbitrary tenant IDs from requests.
- Use scopes for operations: restrict credentials to required capabilities.
- Use roles where they simplify administration: avoid encoding every resource rule as a role.
- Authorize resources: verify tenant or ownership boundaries on every protected object.
- Authorize nested resources: do not assume indirect identifiers are safe.
- Enforce business-state rules: include workflow state in sensitive operations.
- Scope repository queries: include tenant ownership directly in database access.
- Use atomic writes: enforce authorization-relevant state constraints during updates.
- Hide resource existence when appropriate: use consistent 404 behavior for inaccessible objects.
- Use service identities: authenticate machine-to-machine requests separately from users.
- Apply least privilege: keep service and user permissions narrow.
- Define authorization-cache TTLs: balance latency against revocation speed.
- Define fail-closed behavior: protect high-risk operations during policy outages.
- Monitor authentication failures: detect credential attacks and broken clients.
- Monitor authorization denials: detect enumeration and tenant-boundary probing.
- Record denial reasons: distinguish missing scope, tenant mismatch, and business-state denial.
- Include trace IDs in security events: correlate access decisions with distributed requests.
- Never log credentials: redact passwords, tokens, API keys, and cookies.
- Test negative cases: verify that users cannot access foreign resources or unauthorized operations.
- Test policy failures: validate behavior when identity and authorization dependencies are unavailable.
Conclusion
Authentication and authorization solve different security problems and should remain distinct in the architecture. Authentication establishes a trusted identity, while authorization combines that identity with permissions, tenant boundaries, resource ownership, and business state to decide whether an operation should proceed.
Production systems work best when identity verification is standardized and reusable, but authorization stays close enough to the domain to make accurate decisions. Gateway checks can enforce broad scopes, application services can evaluate business rules, and tenant-scoped data access can reinforce resource isolation.
Key Takeaway: Authentication proves identity; authorization proves permission. Build trusted identity context early, validate credentials rigorously, enforce scopes and roles only as the first authorization layer, scope every protected resource to the correct tenant or owner, include business state in sensitive decisions, and define explicit behavior for revocation, caching, and authorization-system failures.
More Articles to Read
- API Security Explained: Threats and Defense Strategies
- OAuth 2.0 vs JWT vs API Keys
- Designing Secure API Architectures
- Protecting APIs Against Common Attacks
- Secrets Management in Cloud Applications
- API Security Best Practices for Production Systems
Comments (0)