System Design Fundamentals

Authentication vs Authorization

A

Authentication vs Authorization

The Badge and the Keys: A Real-World Scenario

Imagine a new junior developer, Alex, joins your company on Monday morning. The receptionist hands them an employee badge with their name and photo. That badge proves who Alex is — authentication. But here’s the thing: that badge only opens certain doors. Alex can walk into the engineering office and the cafeteria, but the badge won’t open the server room, the executive suite, or the finance office. Those are authorization decisions — what Alex is allowed to do.

In distributed systems, confusing these two concepts is one of the most common sources of security breaches. A system that authenticates users but fails to properly authorize their actions is wide open to privilege escalation attacks. We’ve all heard the war story: an engineer was authenticated to the system, discovered they could modify other users’ payment methods, and suddenly you’re dealing with a massive security incident.

This chapter focuses on getting these concepts right — they’re foundational to building secure systems.

Authentication: Proving Your Identity

Authentication answers one question: “Who are you?” It’s the process of verifying that a user is who they claim to be.

Common authentication methods:

MethodHow It WorksStrengthsWeaknesses
PasswordUser provides a secret stringSimple, familiarPhishing, weak passwords, credential stuffing
Multi-Factor Auth (MFA)Something you know + something you have/areSignificantly harder to compromiseUser friction, backup codes can leak
BiometricsFingerprint, face, voice recognitionHard to impersonate, frictionlessPrivacy concerns, can’t change if compromised
Certificates (mTLS)X.509 certificates over TLSVery secure, no passwordsComplex to manage, certificate rotation overhead
API KeysLong random stringsSimple for machine-to-machineEasy to leak in code, logs, repositories
TokensJWTs, OAuth tokensStateless, scalableToken leakage, short lifespan requires refresh

The key insight: authentication is about verification. You’re proving your identity through some credential. The system verifies that credential is valid.

Authorization: Proving Your Permission

Authorization answers a different question: “What are you allowed to do?” It’s the process of determining what authenticated users can access.

Key authorization models:

Role-Based Access Control (RBAC):

  • Users are assigned roles (Admin, User, Viewer)
  • Roles have permissions (can_read, can_write, can_delete)
  • Simple to understand and implement
  • Pitfall: role explosion — you end up with 50+ roles like “Admin_EU_Finance_ReadOnly_NoDelete”

Attribute-Based Access Control (ABAC):

  • Decisions based on attributes: user attributes (department, clearance level), resource attributes (classification, owner), action (read, write, delete), and environment (IP address, time of day)
  • Example: “Only users in the finance department with a security clearance of level 3 or higher can read expense reports created in the last 30 days from within the corporate network”
  • Very flexible and expressive
  • Pitfall: policies become complex and hard to reason about; debugging permission issues becomes a nightmare

Relationship-Based Access Control (ReBAC):

  • Pioneered by Google’s Zanzibar paper
  • Decisions based on relationships: “Is this user a member of this team?” “Is this user the owner of this document?”
  • Example: A user can edit a document if they’re the owner OR a member of the team that owns it
  • Powerful for complex permission hierarchies
  • Pitfall: requires sophisticated relationship querying infrastructure

Access Control Lists (ACL):

  • Explicit list of who can do what
  • Example: File ACL says “alice: read, write” and “bob: read”
  • Granular but doesn’t scale well to many users

The Relationship: Authentication First, Always

Here’s the critical relationship: authentication always comes before authorization. You cannot authorize someone until you know who they are.

The flow looks like this:

Request arrives

[Authentication] → Who are you? Validate credentials

[Authorization] → What can you do? Check permissions

[Access Control] → Grant or deny the request

If authentication fails, you reject the request immediately. You never get to the authorization step.

Pro tip: A common mistake is trying to authorize an unauthenticated user. You might see code like: “Check if user.role == ‘admin’” without first validating that user.role actually exists or is valid. This leads to null pointer exceptions or, worse, default-allow behavior when the user object is malformed.

Authentication Flows in Distributed Systems

In a monolith, authentication might be as simple as checking a username/password against a database. In distributed systems, it gets more complex because you have multiple services that need to verify identity without tight coupling.

Centralized Identity Provider (IdP)

The standard approach: maintain a single source of truth for user identities.

Service A    Service B    Service C
    ↓            ↓            ↓
  [All services trust the Identity Provider]
    ↓            ↓            ↓
        Identity Provider (IdP)
            (Keycloak, Okta, Auth0)

When Service A needs to authenticate a user:

  1. User logs in to the IdP
  2. IdP issues a token (JWT, SAML assertion, etc.)
  3. User includes token in requests to Services A, B, C
  4. Each service validates the token independently (if using JWT and public key verification) or calls the IdP to validate (if using opaque tokens)

Session-Based vs Token-Based Authentication

Session-based (traditional web):

  • User logs in with password
  • Server creates a session, stores it in memory or cache
  • Server issues a session cookie
  • On each request, server looks up the session to verify identity
  • Logout clears the session

Pros: tokens are revoked immediately on logout; sessions are opaque (can’t decode them) Cons: requires shared session store in distributed systems; tightly couples the web server to the session layer

Token-based (stateless):

  • User logs in with password
  • Server creates a token (JWT) and sends it to the client
  • Client includes token in Authorization header
  • Server validates the token locally (no shared state needed)
  • Logout is tricky — token remains valid until expiration

Pros: stateless (scales horizontally); no shared session store needed Cons: token is valid until expiration (no immediate revocation); tokens can be large (Base64URL encoding overhead)

Multi-Factor Authentication Types

The principle: “something you know, something you have, something you are.”

  1. Something you know: Password, PIN, security question
  2. Something you have: Hardware token (FIDO2 key), phone (SMS/authenticator app), security key
  3. Something you are: Biometric (fingerprint, face, iris scan)

Did you know? SMS-based MFA (SMS OTP) is considered weaker than app-based (TOTP via Google Authenticator) or hardware keys (FIDO2) because SMS can be intercepted via SIM swapping. But it’s still better than no MFA.

Authorization in Microservices

Where should authorization happen?

Option 1: API Gateway

  • All authorization checks happen at the gateway
  • Services assume all traffic is authorized
  • Pros: Single place to manage policies; simpler service code
  • Cons: Gateway becomes a bottleneck; services can’t make nuanced decisions about what an authorized user can do

Option 2: Service-Level

  • Each service independently authorizes requests
  • Gateway authenticates (proves identity)
  • Services authorize (enforces permissions)
  • Pros: Services can have domain-specific authorization logic
  • Cons: authorization logic is scattered; harder to audit and test

Best practice: Both. The gateway authenticates and does coarse-grained authorization (is the token valid?). Each service does fine-grained authorization (can this user perform this action on this resource?).

Identity Propagation

When Service A calls Service B, how does B know who the original user is?

Client → [Service A] → [Service B] → [Service C]
         (has JWT)     (needs to know)

Options:

  1. JWT in context: Service A extracts claims from JWT and passes them to Service B (in headers or request body)
  2. Automatic propagation: Middleware/instrumentation framework automatically propagates the JWT
  3. Mutual TLS + client certificate: Service A authenticates to Service B using a certificate that identifies which service it is; Service B can trust that Service A authenticated the original user

The Principle of Least Privilege

This is non-negotiable: every user, service, and process should have the minimum permissions needed to do their job.

Why? If a service is compromised, the attacker can only do what that service is authorized to do. If your cron job that reads user data has admin access, and the cron job is compromised, the attacker has admin access.

Example: A microservice that sends notification emails needs permission to:

  • Read from the notifications table (SELECT)
  • Update sent status in the notifications table (UPDATE)

It does NOT need permission to:

  • Delete from the notifications table (DELETE)
  • Read from the payments table
  • Write to the logs table (if you have a separate logging service)

Audit your permissions regularly. You’d be surprised how often you find services with overly broad permissions because “it was easier that time” and nobody ever cleaned it up.

Authorization Decision Frameworks

Here’s a practical decision matrix:

Use RBAC if:

  • You have a small number of distinct roles (under 20)
  • Permissions map cleanly to roles
  • You’re building a traditional web application (Admin, Manager, User)

Use ABAC if:

  • Permissions depend on context (time of day, location, data classification)
  • You need fine-grained decisions
  • You can afford the operational complexity

Use ReBAC if:

  • Your data has rich hierarchies and relationships
  • You need to support “who owns this document” style permissions
  • You’re willing to invest in building or buying a relationship query system

Use ACLs if:

  • You have file systems or resource-specific permissions
  • You’re integrating with existing systems that already use ACLs
  • You don’t have many users per resource

Open Policy Agent (OPA) Example

OPA is a policy engine that lets you write authorization rules in a language called Rego. Here’s a simplified example:

# Allow reading public documents
allow_read {
    input.document.public == true
}

# Allow editing if user is the owner
allow_edit {
    input.document.owner == input.user.id
}

# Allow admin to do anything
allow_admin {
    input.user.role == "admin"
}

allow {
    allow_read
}

allow {
    allow_edit
}

allow {
    allow_admin
}

Permission middleware (in Go, Python, Node.js, etc.) would load this policy and call:

OPA input: {
  "document": {"id": "doc123", "public": false, "owner": "alice"},
  "user": {"id": "alice", "role": "user"},
  "action": "edit"
}
OPA evaluates: allow_edit succeeds
Response: {"allow": true}

Trade-Offs: Centralized vs Distributed Authorization

Centralized authorization service (OPA server, Casbin server):

  • Pros: Single source of truth for policies; easier to audit; one place to update rules
  • Cons: Network call on every authorization decision (latency); service dependency

Distributed authorization (JWT with embedded claims, local policy evaluation):

  • Pros: Fast (no network call); no dependency on auth service
  • Cons: Hard to revoke permissions (wait for token expiration); policies must be distributed to all services

Hybrid (best practice): Short-lived tokens with claims cached locally, plus a policy service for complex decisions. Policies are versioned and deployed with services.

Key Takeaways

  1. AuthN and AuthZ are different: Authentication is proving who you are; authorization is proving what you’re allowed to do. They always go together.

  2. Choose the right model: RBAC is simple but can explode in complexity. ABAC is flexible but hard to manage. ReBAC is powerful for relationships. Start with RBAC and evolve as needed.

  3. Principle of least privilege: Every user, service, and process should have the minimum permissions needed. Audit regularly.

  4. Identity propagation matters: In microservices, you need a way to pass identity context between services securely. JWTs or service-to-service authentication both work.

  5. Authorization checks go everywhere: At the API gateway, in business logic, in data access layers. Defense in depth.

  6. Consider your scale: For small systems, simple RBAC and in-memory authorization works. For large distributed systems, you may need a dedicated authorization service.

Practice Scenarios

Scenario 1: Role Explosion You have an e-commerce platform with customers, sellers, and administrators. Initially, you create three roles: Customer, Seller, Admin. Six months later, you have: Customer_US, Customer_EU, Customer_Premium, Seller_New, Seller_Verified, Seller_Premium, Admin_Finance, Admin_Support, Admin_Engineering. You’re maintaining 10 roles with overlapping permissions. What would you do? (Hint: Maybe ABAC is better here.)

Scenario 2: Privilege Escalation A junior developer finds that they can edit other users’ profile data by changing the user_id parameter in their API request. The authentication check passes (they’re logged in) but the authorization check is missing (is this their data?). How would you have caught this during code review?

Connection to Next Section

In the next section, we’ll explore OAuth 2.0 and OpenID Connect — the industry standards for delegating authentication and authorization to trusted third parties. OAuth 2.0 actually solves a specific problem: how do you let third-party apps access user data without the user giving them their password?