Skip to content

MCP OAuth Stage 6: audit logging + onMCPTokenIssued hook #96

@heskew

Description

@heskew

MCP OAuth Stage 6: audit logging + onMCPTokenIssued hook

Sub-issue of #86. Wires MCP token lifecycle events into Harper's audit channel and adds a lifecycle hook so app authors can react to token issuance (rate limiting, billing, custom logging, etc.).

Context

By the time Stage 5 lands, Harper apps using @harperfast/oauth can serve a full MCP OAuth flow — but there's no audit trail beyond ad-hoc logger calls, and apps can't hook into the token-issuance moment the way they can hook into human OAuth (onLogin). This stage closes both gaps with one PR.

What this adds

  1. Three audit event types emitted via Harper's existing audit channel (same one used by human-OAuth events):
    • oauth.mcp.token.issued — when /oauth/mcp/token mints a new access token
    • oauth.mcp.token.refreshed — when /oauth/mcp/token rotates a refresh token
    • oauth.mcp.token.rejected — when token validation fails in withMCPAuth (any of the rejection branches from Stage 5)
  2. onMCPTokenIssued lifecycle hook added to the existing OAuthHooks interface in src/types.ts. Signature:
    onMCPTokenIssued?: (
      event: { type: 'access' | 'refresh', client_id, sub, aud, scope, jti },
      request: any
    ) => Promise<void>
    Fired from /oauth/mcp/token after the JWT is signed and before the response is sent. App-author use case: rate limit by client_id, attribute usage for billing, push to a queue, etc.
  3. onLogin already fires from the bridged authorize flow (Stage 3) — verify and document that the existing hook works unchanged for MCP-initiated auth; add a test that asserts it.

Spec requirements

  • No spec MUST/SHOULD here — auditing is a Harper-side concern, not an MCP spec mandate. The MCP authorization spec 2025-06-18 doesn't say anything about audit logging.

Acceptance

  • oauth.mcp.token.issued, oauth.mcp.token.refreshed, oauth.mcp.token.rejected events emit on the corresponding paths (Stages 4 and 5)
  • Event payloads include client_id, sub, aud, scope, jti, and a timestamp — no token material (never log tokens, per CLAUDE.md)
  • onMCPTokenIssued hook is added to OAuthHooks interface; fires after JWT mint, before response
  • onMCPTokenIssued failure in app code is logged but does NOT block token issuance (failure-tolerant)
  • onLogin hook fires on MCP-initiated auth (regression test against the bridged flow from Stage 3)
  • Unit tests cover event emission for each of the three event types and the hook fire-and-forget contract

Dependencies

  • Depends on: Stage 4 (token issuance to instrument), Stage 5 (token validation rejection paths to instrument)
  • Blocks: none — Stages 7 and 8 don't depend on audit

Out of scope

  • Transitive revocation hook onUpstreamRevoked (v1.1 per Add MCP OAuth flow support #86)
  • Per-tool scope authorization hooks (v1.1)
  • Streaming audit subscriber (out of scope for the OAuth plugin entirely)

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions