Authentication & Authorization Architecture

The Cashier Who Became Admin (Accidentally)

Two months into production, during a routine audit, I discovered something terrifying: a cashier account had full admin privileges. They could:

  • Delete other users

  • Access financial reports

  • Modify system settings

  • See data from other restaurant tenants

How did this happen? One line of code:

# The bug - in token verification
def get_current_user(token: str):
    payload = jwt.decode(token, SECRET_KEY)
    user_id = payload["user_id"]
    # BUG: Returning user without checking token claims!
    user = db.query(User).filter(User.id == user_id).first()
    return user  # User object from DB, not token claims

# User had been promoted to cashier, but old JWT still had "admin" role
# I was using DB role, not token role!

The cashier had logged in as admin months ago (testing), got a JWT with role: admin, then was demoted to cashier in the database. But the old JWT was still valid for 24 hours, and I was checking the wrong role.

That day I learned: Authentication (who you are) and Authorization (what you can do) are different problems requiring different solutions.

JWT Structure with Multi-Tenant Claims

Here's the correct JWT structure for the POS system:

Auth Service Implementation

Complete authentication flow:

Token Validation Middleware

Middleware for protecting routes:

RBAC (Role-Based Access Control)

Implementing roles and permissions:

Refresh Token Flow

Implementing token refresh for better security:

Session Management with Redis

Key Learnings

  1. Separate authentication from authorization

    • Authentication: Who are you? (JWT verification)

    • Authorization: What can you do? (Permissions check)

  2. Use claims from token, not database

    • Token represents user at time of login

    • If role changes, force re-login or refresh token

  3. Refresh tokens improve security

    • Short-lived access tokens (hours)

    • Long-lived refresh tokens (days/weeks)

    • Can revoke refresh tokens

  4. Session management prevents replay attacks

    • Store active sessions in Redis

    • Logout invalidates session

    • Check session on every request

  5. Permission-based access is more flexible than roles

    • Roles map to permissions

    • Can change role permissions without code changes

Common Mistakes

  1. Trusting client-sent role

    • Always get role from verified JWT

    • Never accept role from request body

  2. Not invalidating sessions on logout

    • JWT is stateless but sessions aren't

    • Must track and invalidate

  3. Using same secret for access and refresh tokens

    • Different secrets for different token types

    • Limits blast radius if secret leaks

  4. Not refreshing permissions

    • User promoted to admin but token still says cashier

    • Force token refresh on role change

When to Use

JWT + Sessions (My approach):

  • Best security (stateless + revocable)

  • Supports logout

  • Can track active sessions

Pure JWT (Stateless):

  • Simpler implementation

  • Better horizontal scaling

  • Can't revoke tokens

Session-only (No JWT):

  • Traditional approach

  • Requires sticky sessions

  • Limited in microservices

Next Steps

Next Article: 07-data-architecture-patterns.md - Learn database patterns for microservices.


Remember: Security is not a feature. Get authentication and authorization right from the start, because fixing security holes in production is exponentially harder than building it correctly.

Last updated