Authentication Authorization Sessions and Token Boundaries
Purpose and Outcome
Authentication answers, “Who is making this request?” Authorization answers, “What is that authenticated principal allowed to do here?” Sessions remember an authenticated browser or device across requests. Tokens carry or reference identity and authority across process boundaries. In system design, the hard part is not naming these concepts; it is deciding where identity is established, where permissions are evaluated, and where a credential must stop being accepted.
This lesson focuses on boundary design. A login page, API gateway, background worker, mobile app, and internal microservice may all touch the same user action, but they should not all receive the same credential with the same power. A well-designed system narrows each credential to the smallest useful context: a browser session for the browser, an access token for APIs, a service token for service-to-service calls, and explicit authorization checks near the resource being protected.
Mechanism: From Request to Decision
A typical request passes through several identity stages. First, credential extraction reads a cookie, bearer token, mTLS client identity, API key, or signed webhook header. Second, authentication validates that credential. For a server-side session cookie, validation usually means looking up a random session id in a database or cache and checking expiration and revocation. For a self-contained token, validation means verifying the signature, issuer, audience, timestamps, and sometimes key id. Third, principal construction turns the result into a normalized object such as subject, tenant, authentication method, assurance level, and scopes. Fourth, authorization evaluates that principal against the action, resource, and policy.
The most important design rule is to keep authentication and authorization coupled in the request path but separated in responsibility. Authentication should not silently imply broad access. A valid login for user_7 proves the request belongs to user_7; it does not prove user_7 may delete tenant_b invoices. That authorization decision needs resource context: tenant ownership, role assignment, document sharing state, account status, feature flags, and sometimes time or network constraints.
Sessions and tokens solve different problems. A session cookie is excellent for a browser application because it can be HttpOnly, Secure, SameSite, short enough to avoid large headers, and centrally revoked. Its weakness is server-side state: every request needs store availability or a carefully bounded cache. A signed access token is useful when many APIs need local validation without a central lookup. Its weakness is revocation latency: if the token is valid for fifteen minutes, a removed permission may continue to work until token expiry unless APIs consult a revocation or policy source.
Credential Anatomy
A browser session usually has a cookie name, a random opaque value, attributes, and a server-side row. The row should contain the subject, creation time, last-seen time if needed, expiry, revoked flag, device metadata, and a rotating anti-CSRF secret for state-changing browser requests. The cookie value should not be a user id or predictable sequence; it should be high-entropy random data.
An access token usually has an issuer, subject, audience, issued-at time, expiry, scopes or claims, and a signature or reference id. The audience field is a boundary: a token minted for the orders API should not be honored by billing. Scopes are also boundaries, but they are not a substitute for resource checks. A token with invoice:read still should not read another tenant’s invoice.
Authorization policies often combine role-based access control, attribute-based access control, and relationship checks. RBAC says an account_admin can invite users. ABAC says only if the account is active and the request comes from an approved region. Relationship checks say only if the actor belongs to the same tenant or has been granted access to the specific object. Large systems usually need all three.
Example 1: Separating Browser and API Credentials
The first example models a gateway that refuses to accept both a browser cookie and a bearer token on the same request. That rule prevents credential confusion, where middleware authenticates one credential but downstream code assumes another.
from dataclasses import dataclass
@dataclass(frozen=True)
class Request:
cookie_session: str | None
bearer_token: str | None
def choose_credential(req: Request) -> str:
if req.cookie_session and req.bearer_token:
raise ValueError("ambiguous credentials")
if req.cookie_session:
return "browser-session"
if req.bearer_token:
return "api-token"
return "anonymous"
print(choose_credential(Request("sid_123", None)))
print(choose_credential(Request(None, "eyJ...")))
The expected output is browser-session and api-token on separate lines. If both fields are present, the function raises an error. In a real gateway this would become a 400 or 401 response with logging, because accepting ambiguous credentials can let a client smuggle an API token into a path meant only for browser sessions.
Example 2: Authorization Needs Resource Context
The next example shows why a scope alone is incomplete. Alice has invoice:read, but her tenant still has to match the invoice tenant.
from dataclasses import dataclass
@dataclass(frozen=True)
class Principal:
subject: str
tenant: str
scopes: set[str]
def can_read_invoice(user: Principal, invoice_tenant: str) -> bool:
return user.tenant == invoice_tenant and "invoice:read" in user.scopes
alice = Principal("user_7", "tenant_a", {"invoice:read"})
print(can_read_invoice(alice, "tenant_a"))
print(can_read_invoice(alice, "tenant_b"))
The expected output is True, then False. The first request is allowed because the principal has the right action and tenant. The second is rejected even though the same user has the same scope. This pattern is central to multi-tenant system design: authorization must bind the principal to the target resource, not just to a menu item or endpoint.
Example 3: Server-Side Session Revocation
Server-side sessions are useful when immediate logout, administrator revocation, or suspicious-device shutdown matters. The server can reject the same cookie value after flipping one flag.
import time
class SessionStore:
def __init__(self):
self.rows = {}
def create(self, user_id, ttl_seconds, now):
session_id = f"s_{len(self.rows) + 1}"
self.rows[session_id] = {"user_id": user_id, "expires_at": now + ttl_seconds, "revoked": False}
return session_id
def revoke(self, session_id):
self.rows[session_id]["revoked"] = True
def authenticate(self, session_id, now):
row = self.rows.get(session_id)
if not row or row["revoked"] or row["expires_at"] <= now:
return None
return row["user_id"]
store = SessionStore()
sid = store.create("user_7", ttl_seconds=60, now=1000)
print(store.authenticate(sid, now=1030))
store.revoke(sid)
print(store.authenticate(sid, now=1031))
The expected output is user_7, then None. Before revocation, the session id maps to a valid row. After revocation, authentication fails even though the client still has the cookie. This is the main operational advantage of opaque sessions over long-lived self-contained tokens.
Example 4: Token Audience as a Boundary
Service-to-service calls should not reuse end-user browser cookies. Internal services should receive tokens minted for a specific audience and purpose.
from dataclasses import dataclass
@dataclass(frozen=True)
class Token:
audience: str
subject: str
scopes: set[str]
def call_service(token: Token, service_name: str, required_scope: str) -> str:
if token.audience != service_name:
return "reject: wrong audience"
if required_scope not in token.scopes:
return "reject: missing scope"
return f"allow: {token.subject}"
orders_token = Token("orders", "checkout-service", {"order:create"})
print(call_service(orders_token, "orders", "order:create"))
print(call_service(orders_token, "billing", "charge:create"))
The expected output is allow: checkout-service, then reject: wrong audience. The billing service refuses an orders token, even though the token is otherwise well formed. Audience checks limit blast radius when a token leaks from logs, traces, queues, or an overly broad client.
Design Choices and Trade-offs
Choose opaque sessions when central control matters more than stateless validation. They are strong for web apps, admin consoles, and regulated workflows where logout must take effect quickly. Their trade-off is dependence on a session store. Replication lag, cache eviction, or regional partitioning can make valid users appear logged out unless the architecture defines locality and failover behavior.
Choose short-lived access tokens when many APIs need low-latency local validation. They reduce load on the identity system and can work during brief identity-provider outages. Their trade-off is delayed revocation and claim staleness. The usual correction is short lifetimes plus refresh tokens, token exchange, or policy checks at sensitive operations.
Place coarse authentication at the edge, but keep resource authorization in the owning service. An API gateway can reject missing or malformed credentials and enforce broad audience rules. It usually cannot know whether invoice_123 belongs to tenant_a after a transfer, soft delete, or sharing change. The invoice service owns that data and should make the final allow or deny decision.
Failure Modes and Troubleshooting
A common symptom is users randomly being logged out. Causes include session store eviction, inconsistent encryption keys between regions, cookie domain mismatch, or clock skew causing premature expiry. Diagnose by checking Set-Cookie attributes, session lookup misses, expiry timestamps, and whether the failing region can read the session row. Correct by pinning session reads to the right replicated store, increasing capacity, fixing cookie domain/path settings, and using bounded clock skew tolerance.
Another symptom is a user retaining access after role removal. With self-contained tokens, the cause is usually that the old token still contains the old scope. Diagnose by decoding the token without trusting it, comparing issued-at time with the role-change event, and checking whether the API performs live policy checks for sensitive actions. Correct with shorter token lifetimes, permission-version claims, token revocation lists for high-risk events, or a policy lookup before destructive operations.
A third symptom is cross-tenant data exposure. The endpoint may check that the user is authenticated and has read scope, but forget to constrain the database query by tenant or object relationship. Diagnose by tracing the authorization inputs and inspecting the final query predicates. Correct by requiring tenant id in every resource lookup, adding database constraints where possible, and testing denied access paths as carefully as successful paths.
Security, Performance, and Reliability Implications
Security improves when credentials are narrow, short-lived, and bound to audience, issuer, transport, and resource context. Browser cookies should use HttpOnly, Secure, and SameSite attributes. State-changing browser actions need CSRF protection because the browser attaches cookies automatically. Bearer tokens need careful handling because whoever possesses the token can use it until it expires or is revoked.
Performance depends on where validation happens. Local token verification is fast after key caching, but authorization may still need database reads. Central introspection gives fresher control but adds latency and a dependency. Reliability improves when identity dependencies are split by function: login may be unavailable while existing short-lived tokens continue to work, but high-risk actions can still require fresh policy reads.
Hands-on Lab: Model a Boundary
Prerequisites: Python 3.10 or later and a terminal. Create a temporary file named auth_boundary_lab.py. Paste the four examples into it, or run them one at a time. The goal is to observe how changing one boundary condition changes the decision.
- Run the credential selection example and confirm that cookie and bearer credentials produce different classifications.
- Add a request containing both a cookie and a bearer token. Verify that it raises ValueError. This confirms the ambiguity guard.
- Run the invoice authorization example. Change Alice’s tenant to tenant_b and verify that tenant_b access becomes True while tenant_a becomes False.
- Run the session example. Change ttl_seconds to 1 and authenticate at now=1002 before revocation. Verify that expiration alone returns None.
- Run the audience example. Change the token audience to billing and scope to charge:create. Verify that billing then allows the call.
Cleanup is simple: delete auth_boundary_lab.py. If you added test users or sessions in a real development environment, remove them and rotate any sample secrets copied into local configuration. Never paste production tokens into a lab script.
Assessment Exercises
- A gateway validates a JWT and forwards all claims to every internal service. Which checks should still happen inside the resource-owning service, and why?
- Your product needs logout to take effect immediately on all devices. Compare opaque server-side sessions with fifteen-minute stateless access tokens for this requirement.
- An orders token is accepted by billing because both services share the same signing key. What claim and validation rule would create a stronger boundary?
- A support agent can view customer accounts but only after a ticket is assigned. Is this RBAC, ABAC, relationship-based authorization, or a combination? Design the decision inputs.
- A user reports access denied after being added to a role. List two cache or token-staleness causes and how you would verify each one.
Summary
Authentication establishes the principal, authorization evaluates the principal against an action and resource, sessions preserve browser identity with central control, and tokens carry bounded authority between clients and services. Strong system design makes those boundaries explicit: one credential type per request path, audience-specific tokens, resource-local authorization, short useful lifetimes, and diagnostics that explain whether a failure came from extraction, validation, principal construction, or policy evaluation.
