Backend Engineering · Field guide 04

Identity, Authentication, and Authorization

Separate who a caller is, how that was proved, and what the caller may do now. This chapter connects protocol behavior to the decisions a production service must make.

Application layer8 sections42 min0% read

One request crosses four security boundaries

A service cannot safely answer ‘may this request continue?’ with one vague authenticated flag. It needs separate evidence about identity, authentication, session state, and the requested resource.

Identity names a subject. Authentication establishes confidence that a claimant controls an authenticator bound to that subject. A session or token carries the result across requests. Authorization decides whether the resulting principal may perform one action on one resource under the current conditions. Each answer becomes an input to the next boundary, but none replaces it.

Consider a valid session for a member of tenant A requesting document 42. The session can identify the member without proving that document 42 belongs to tenant A, that the member owns it, or that the current assurance is high enough to delete it. The service must load trusted resource attributes and evaluate policy after credential validation, on every request.

System visualFour decisions on one request
  1. 01Identity

    Name the subject with an issuer-scoped identifier.

  2. 02Authentication

    Verify control of a bound authenticator.

  3. 03Session or token

    Validate current state, scope, type, and time.

  4. 04Resource context

    Load tenant, owner, classification, and action.

  5. 05Authorization

    Return an explicit allow or deny decision.

Read diagram as text

The service identifies a subject, verifies authentication evidence, validates the current session or token, loads trusted attributes for the requested resource, and only then makes an explicit authorization decision.

Authenticators provide different kinds of assurance

An authentication ceremony proves control of an authenticator, not the claimant's honesty, device safety, or permission to access every resource.

Passwords are shared secrets that users can reveal to a convincing impostor and attackers can replay. OTP authenticators reduce dependence on one static secret, but manually entered OTPs remain phishable and replayable during their validity window. Public-key authenticators can bind proof to the verifier's origin, which is why phishing resistance is a property of the protocol rather than the presence of a second screen.

Choose assurance from risk. A low-impact read may accept a baseline session, while exporting sensitive data, deleting a resource, or changing membership may require a recent phishing-resistant step-up. The application should record the resulting assurance and its bounded lifetime rather than infer strength from a role name.

Qualitative authenticator comparison
AuthenticatorPrimary proofReplay/phishing concernProduction implication
PasswordKnowledge of a shared secretReplayable and readily phishedBlock breached/common values, rate-limit attempts, and never log the secret
One-time passwordPossession plus a short-lived codeA live code can still be relayed or phishedAccept once, bound its lifetime, and do not label OTP alone phishing-resistant
Public-key authenticatorPossession of a private key and protocol proofCan be verifier-name boundPrefer for higher-assurance and phishing-resistant flows
  • Use generic authentication failures so account existence is not disclosed unnecessarily.
  • Rate-limit failed attempts and monitor abuse without storing supplied credentials.
  • Treat recovery and authenticator replacement as high-risk authentication events.
  • Require a fresh step-up for sensitive actions rather than trusting an old login forever.

A session is a revocable state machine

Authentication is an event. A session is the changing server-side policy that decides whether its result can still be used.

A browser receives an opaque, high-entropy bearer value while the service stores only a lookup digest and session metadata. Authentication creates a fresh identifier instead of promoting an anonymous identifier supplied by the browser. Privilege changes rotate it again and invalidate the predecessor, closing the session-fixation path.

Idle and absolute deadlines answer different questions. The idle deadline limits unattended use; the absolute deadline bounds the total lifetime even when requests continue. Logout, compromise response, recovery, and administrative action can revoke earlier. Every lookup must reject expired, revoked, and replaced records before refreshing activity.

System visualA session changes state over time
  1. S0
    Anonymous

    No authenticated session exists; browser input has no privileged meaning.

  2. S1
    Active

    Authentication issues a fresh browser value and stores its lookup digest.

  3. S2
    Elevated

    Step-up raises assurance for a bounded period and rotates the identifier.

  4. S3
    Expired

    Idle or absolute time reaches its limit; use fails closed.

  5. S4
    Replaced

    A rotated predecessor remains unusable even if copied earlier.

  6. S5
    Revoked

    Logout, recovery, or compromise response terminates acceptance.

Read diagram as text

A user begins anonymous, receives a fresh active session after authentication, may temporarily step up to elevated assurance, and eventually reaches an unusable expired, replaced, or revoked state. Rotation never makes the predecessor active again.

session-policy.ts
export function inspectSession(
  record: SessionRecord,
  now: number,
  idleTimeoutMs: number,
): SessionInspection {
  if (record.revokedAt !== null) {
    return { ok: false, reason: "revoked" };
  }
  if (record.replacedByDigest !== null) {
    return { ok: false, reason: "replaced" };
  }
  if (now >= record.expiresAt) {
    return { ok: false, reason: "absolute-expired" };
  }
  if (now - record.lastSeenAt >= idleTimeoutMs) {
    return { ok: false, reason: "idle-expired" };
  }
  return { ok: true };
}

Credential transport changes the threat model

Cookies and bearer authorization headers can both transport session material. Their delivery rules expose different default failure paths.

A browser automatically attaches a matching cookie according to its domain, path, Secure, and SameSite rules. HttpOnly prevents ordinary script from reading the value but does not stop injected script from sending same-origin requests. Automatic attachment also creates cross-site request-forgery concerns, so SameSite is defense-in-depth rather than a complete substitute for a deliberate CSRF design.

Application code usually attaches a bearer token explicitly. That reduces automatic cross-site sending but makes any script-accessible storage part of the token-theft boundary. Possession normally enables replay, so minimize scope and lifetime, protect transport and storage, and design rotation, revocation, or sender constraint before choosing the format.

Cookie and bearer-token transport responsibilities
QuestionSecure cookie sessionExplicit bearer token
Who attaches it?Browser for matching requestsClient application code
Primary browser concernCSRF plus same-origin action through XSSTheft from script-accessible storage plus XSS
Important controlsSecure, HttpOnly, SameSite, narrow scope, CSRF designShort lifetime, narrow audience/scope, protected storage, rotation or sender constraint
Revocation shapeServer-side session status can fail immediatelySelf-contained tokens need bounded life or an online status strategy

A JWT is a claims container, not session magic

Base64url decoding reveals attacker-controlled fields. Trust begins only after cryptographic verification and the complete application validation profile succeed.

A maintained security library should parse the compact token, reject algorithms outside an explicit allowlist, verify the signature with key material bound to the configured issuer, and enforce structural limits. Application policy must then validate issuer, audience, token type, subject, expiry, not-before time, and mutually exclusive rules for different token kinds. A token for another API or an ID token used as an access token must fail even when its signature is valid.

The example type is intentionally named VerifiedTokenEnvelope: it represents the boundary after library verification, not an object produced by decoding. The helper keeps only allowlisted roles and converts claims into a minimal principal. Resource authorization still occurs afterward because neither a valid signature nor an admin-looking claim proves access to a particular tenant object.

verified-token-policy.ts
export function validateVerifiedToken(
  envelope: VerifiedTokenEnvelope,
  policy: TokenPolicy,
  nowSeconds: number,
): TokenValidation {
  const { claims } = envelope;
  if (!policy.algorithms.includes(envelope.algorithm)) {
    return { ok: false, reason: "unexpected-algorithm" };
  }
  if (claims.iss !== policy.issuer) {
    return { ok: false, reason: "wrong-issuer" };
  }
  if (!Array.isArray(claims.aud) ||
      !claims.aud.includes(policy.audience)) {
    return { ok: false, reason: "wrong-audience" };
  }
  if (claims.typ !== policy.type) {
    return { ok: false, reason: "wrong-token-type" };
  }
  return validateTimesAndBuildPrincipal(claims, policy, nowSeconds);
}
  • Configure accepted algorithms; never select trust from an unverified header alone.
  • Bind verification keys to the expected issuer and validate the intended audience.
  • Give access, identity, logout, and security-event tokens mutually exclusive validation profiles.
  • Reject missing or invalid time and subject claims required by your profile.
  • Never put secrets in a JWT payload; signing does not encrypt its claims.

Authorization is a resource decision

A role can narrow policy candidates, but the final decision usually needs the action, tenant, resource owner, classification, and current assurance.

The example begins by denying an absent principal and then rejects a tenant mismatch before inspecting ownership or roles. This order prevents an administrator in tenant B from becoming an administrator in tenant A. It also demonstrates why object identifiers from the URL are not authorization evidence: the service must load the resource and compare trusted attributes.

RBAC groups permissions and remains useful for broad responsibilities. ABAC considers subject, resource, action, and environmental attributes. ReBAC captures relationships such as owner-of or member-of. Real policies often combine all three. The essential properties are explicit inputs, deny by default, one enforcement path, and tests for every matrix cell.

System visualAuthorization narrows toward allow

DecisionMay this principal perform this action on this resource now?

  1. Principal absent
    Deny

    No authentication context can satisfy a protected operation.

  2. Tenant differs
    Deny

    Reject before role or ownership evaluation.

  3. No relationship or role rule
    Deny

    An unmatched policy never inherits access.

  4. Assurance too low
    Deny

    Sensitive reads and destructive actions require step-up.

  5. Every gate passes
    Allow

    Return the exact policy reason for safe internal audit.

Read diagram as text

Authorization starts with deny. A protected request can reach allow only when a principal exists, the tenant matches, a specific ownership or role rule matches the action and resource, and the current assurance satisfies the operation.

authorization.ts
export function authorize(
  { principal, action, resource }: AuthorizationRequest,
): AuthorizationDecision {
  if (!principal) return deny("anonymous");
  if (principal.tenantId !== resource.tenantId) {
    return deny("tenant-mismatch");
  }

  const rule = matchOwnerRoleOrRelationship(
    principal,
    action,
    resource,
  );
  if (!rule) return deny("no-matching-policy");
  if (rule.needsStepUp && principal.assurance !== "elevated") {
    return deny("insufficient-assurance");
  }
  return { allowed: true, reason: rule.reason };
}
Executable authorization matrix for a same-tenant resource
Principal/contextRead standardRead restrictedUpdateDeleteManage members
AnonymousDenyDenyDenyDenyDeny
Member, not ownerDenyDenyDenyDenyDeny
Resource ownerAllowStep-upAllowStep-upDeny
SupportAllowDenyDenyDenyDeny
Tenant adminAllowStep-upAllowStep-upStep-up
Any cross-tenant principalDenyDenyDenyDenyDeny

Security state must be able to move backward

Systems that can create trust but cannot quickly reduce it turn one stolen credential or mistaken grant into a long-lived incident.

Revocation is a propagation problem. A server-side session can consult current state on each request, while a self-contained access token may remain accepted until expiry unless the service adds an online status or version check. Short token lifetimes reduce the window but do not replace refresh-token rotation, reuse detection, or incident procedures where those risks apply.

Recovery, password change, authenticator replacement, membership removal, and role reduction should identify which sessions and tokens become stale. Audit events need stable subject, tenant, action, resource, decision, policy version, and correlation identifiers—but never passwords, OTPs, session bearer values, complete tokens, or sensitive resource bodies.

State changes and the evidence they should invalidate
EventState transitionOperational evidence
LogoutRevoke the active sessionSession identifier category, subject, time, outcome
Privilege changeRotate session and recompute authorizationOld/new policy version and administrative actor
Account recoveryRevoke affected sessions and require renewed assuranceRecovery method, notifications, revocation completion
Refresh-token replayRevoke the token family and investigateFamily identifier, reuse signal, affected client—never raw token
Tenant membership removalDeny future resource decisions immediatelyMembership version and enforcement timestamp
  • Define which events revoke one session, every session, or a token family.
  • Test propagation delay instead of assuming a revocation write is instantly visible everywhere.
  • Version authorization policy so an audit decision can be reconstructed.
  • Keep credential material and sensitive resource content out of logs and traces.

Debug the boundary that made the decision

An unexpected 401, 403, or allow result becomes tractable when authentication evidence, session state, resource context, and policy output are inspected separately.

Start with the request correlation identifier and the credential transport mechanism, not the raw credential. Confirm whether authentication failed, the session or token was expired or revoked, the issuer/audience/type profile matched, and a minimal principal was created. Then inspect the trusted resource tenant, owner, classification, requested action, assurance, and policy version used by authorization.

Keep public failures deliberately small. A service may map missing and unauthorized resources to the same response to avoid confirming existence, while internal audit records a safe decision category. Reproduce with deterministic policy fixtures and a controlled clock; do not paste production tokens into logs, tickets, tests, or decoding websites.

  • Distinguish missing credentials, invalid credentials, expired state, and insufficient permission internally.
  • Confirm the verification key belongs to the expected issuer and the audience names this service.
  • Load the resource independently and compare its tenant and owner with the principal.
  • Check idle, absolute, elevated-assurance, and revocation times with one documented clock policy.
  • Re-run the exact authorization matrix row using sanitized identifiers and the deployed policy version.

Design questions

  1. Which decisions in your current service are hidden inside a single authenticated boolean?
  2. What event rotates a session identifier, and can the predecessor still be accepted anywhere?
  3. Can a valid token issued for another audience or token type reach your handlers?
  4. Which test proves that an administrator from one tenant cannot read a guessed resource ID from another tenant?