hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Authorization with @hectoday/http

Beyond Authentication

  • Authentication vs. Authorization
  • Project Setup

Role-Based Access Control (RBAC)

  • Roles and What They Mean
  • Checking Roles in Route Handlers
  • Role Hierarchy

Permission-Based Access Control

  • From Roles to Permissions
  • Checking Permissions
  • Custom Permissions

Organization Scoping

  • Multi-Tenancy
  • Switching Organizations
  • Inviting Members

API Keys and Scoping

  • API Keys
  • Scoped API Keys

Putting It All Together

  • Policy Functions
  • Audit Logging
  • Authorization Checklist
  • Capstone: Multi-Tenant Notes API

Audit Logging

Why audit logging matters for authorization

The Securing Your API course introduced structured logging for security events (failed logins, rate limits). Authorization adds a new category: who tried to access what, and was it allowed?

Audit logs answer questions like: “Who deleted the Q4 Plan note?” “When did Bob’s role change from viewer to editor?” “Which API key accessed the billing data?” These questions come up during incident investigations, compliance audits, and debugging.

What to log

Every call to the policy function is an authorization decision. Log it:

// src/policy.ts — update the can function
import { log } from "./logger.js";

export function can(ctx: PolicyContext, action: string): boolean {
  const membership = getMembership(ctx.user.id, ctx.orgId);

  if (!membership) {
    log("authz_denied", {
      userId: ctx.user.id,
      orgId: ctx.orgId,
      action,
      reason: "not_a_member",
    });
    return false;
  }

  const permissions = getPermissions(membership.role, ctx.orgId);
  let allowed = permissions.has(action);

  // Special cases...
  if (!allowed && action === "notes:edit" && ctx.resourceOwnerId === ctx.user.id) {
    allowed = permissions.has("notes:read");
  }

  // Check API key scopes
  if (allowed && ctx.user.scopes !== null && !ctx.user.scopes.includes(action)) {
    allowed = false;
  }

  log(allowed ? "authz_allowed" : "authz_denied", {
    userId: ctx.user.id,
    orgId: ctx.orgId,
    action,
    role: membership.role,
    ...(ctx.resourceOwnerId ? { resourceOwnerId: ctx.resourceOwnerId } : {}),
    ...(ctx.user.scopes !== null ? { apiKeyScoped: true } : {}),
  });

  return allowed;
}

Every authorization decision produces a log entry with the user, org, action, role, and result.

Additional events to log

Beyond the policy function, log these authorization-specific events:

// Membership changes
log("membership_created", { userId, orgId, role, invitedBy });
log("membership_role_changed", { userId, orgId, oldRole, newRole, changedBy });
log("membership_removed", { userId, orgId, removedBy });

// Invite lifecycle
log("invite_created", { email, orgId, role, invitedBy });
log("invite_accepted", { email, orgId, role });

// API key lifecycle
log("api_key_created", { userId, orgId, name, scopes });
log("api_key_revoked", { keyId, orgId, revokedBy });

// Custom role changes
log("custom_role_created", { orgId, name, permissions, createdBy });
log("custom_role_updated", { orgId, name, permissions, updatedBy });

What NOT to log

Same rules as the Securing Your API course:

  • Never log API keys (the actual key value)
  • Never log session IDs
  • Never log passwords
  • Log the key prefix or ID, not the key itself

Querying the audit log

For now, audit logs go to stdout as JSON. In production, they would go to a log aggregation service. Common queries:

“What happened to note X?” Filter by action containing “notes:” and the note ID.

“What did Bob do yesterday?” Filter by userId and timestamp.

“Which API keys were used?” Filter by apiKeyScoped: true.

Exercises

Exercise 1: Add logging to the can function. Perform several operations as different users and review the log output. Can you reconstruct who did what?

Exercise 2: Add logging to the invite acceptance flow. Accept an invite and verify the log includes the email, org, and role.

Exercise 3: Simulate an incident: “Someone deleted the Q4 Plan note.” Search the logs for notes:delete actions in Acme. Who did it?

Why do we log both allowed and denied authorization decisions?

← Policy Functions Authorization Checklist →

© 2026 hectoday. All rights reserved.