Skip to content

Add audit logging to authserver #3919

Open

Description

@jhrozek

Context

The authserver is the most security-sensitive component in ToolHive (token issuance, client registration, upstream IDP interaction) yet has zero audit logging. The rest of the codebase has a mature NIST SP 800-53 compliant audit system in pkg/audit/ with structured event types, subjects, targets, and outcomes.

Scope

Define auth event types

New EventType constants in pkg/audit/ (new file authserver_events.go, following mcp_events.go pattern). Failures are NOT separate event types — they are captured via the existing Outcome field (success, failure, error, denied) on each event.

Constant Value When emitted
EventTypeOAuthAuthorizeRequest oauth_authorize_request Client initiates authorization. Outcome: success (redirect issued) / denied (invalid client/redirect)
EventTypeOAuthAuthCodeIssued oauth_auth_code_issued Auth code generated after successful upstream callback
EventTypeOAuthTokenIssued oauth_token_issued Access/refresh token issued via authorization_code grant
EventTypeOAuthTokenRefreshed oauth_token_refreshed Token refreshed via refresh_token grant (distinct security properties from initial issuance)
EventTypeOAuthSessionRevoked oauth_session_revoked Session forcibly revoked (e.g., upstream refresh permanently failed with invalid_grant)
EventTypeOAuthClientRegistered oauth_client_registered DCR: new client registered (RFC 7591)
EventTypeOAuthUpstreamAuthComplete oauth_upstream_auth_complete Upstream IDP callback received confirming user authentication
EventTypeOAuthUpstreamTokenRefreshed oauth_upstream_token_refreshed ToolHive transparently refreshed an upstream IDP token on behalf of a session
EventTypeOAuthUserCreated oauth_user_created New internal user provisioned on first login

Plus supporting subject keys (oauth_client_id, upstream_issuer, upstream_subject), target keys (session_id, scope, resource, grant_type, revocation_reason), and component constant (toolhive-authserver).

NIST SP 800-53 AU-2 coverage

AU-2 Category Events covering it
Logon/logoff events oauth_authorize_request, oauth_upstream_auth_complete
Privileged access oauth_client_registered
Non-privileged access oauth_token_issued, oauth_token_refreshed
Account management oauth_user_created, oauth_session_revoked
Third-party interactions oauth_upstream_auth_complete, oauth_upstream_token_refreshed

Future events (oauth_token_introspected, oauth_mtls_auth_attempted, oauth_client_updated/deleted) should be added when their corresponding features land.

Create AuthServerAuditor and wire into handlers

Follow the WorkflowAuditor pattern (pkg/audit/workflow_auditor.go) — a direct auditor that handlers call, rather than generic HTTP middleware. This is preferred because auth operations have rich semantic context (client_id, grant_type, upstream_provider) that HTTP middleware can't capture, and different endpoints need very different audit data.

Wire into each handler:

  • AuthorizeHandler: oauth_authorize_request with subjects={client_id}, target={redirect_uri, scope, resource}
  • CallbackHandler success: oauth_upstream_auth_complete + oauth_auth_code_issued + optionally oauth_user_created
  • CallbackHandler failure: oauth_upstream_auth_complete with outcome=failure/error
  • TokenHandler authorization_code: oauth_token_issued with subjects={client_id, user_id, session_id}, target={grant_type, scope}
  • TokenHandler refresh_token: oauth_token_refreshed with subjects={client_id, user_id}, target={grant_type}
  • TokenHandler failure: relevant event type with outcome=failure
  • RegisterClientHandler: oauth_client_registered with subjects={client_id, client_name}
  • Upstream token refresh (when implemented): oauth_upstream_token_refreshed with target={upstream_issuer, session_id}
  • Session revocation (when implemented): oauth_session_revoked with target={session_id, revocation_reason}

Acceptance criteria

  • Auth event type constants defined in pkg/audit/authserver_events.go
  • AuthServerAuditor created following WorkflowAuditor pattern
  • Auditor injected into Handler via constructor
  • Audit events emitted for all handler success and failure paths
  • Sensitive data (tokens, PKCE verifiers, authorization codes) NEVER included in audit events
  • Source information (client IP, user agent) included in events
  • Configuration follows pkg/audit.Config patterns (enable/disable, event type filtering)
  • Unit tests verify correct event types, subjects, and outcomes per handler path

Dependencies

Delegated access events (add with each feature)

Each feature in #6886 "Authorization server: grants and ID-JAG issuance for delegated agents" and #6887 "Delegated access: subject mapping and user approval" emits its events in the same change:

  • oauth_token_exchanged (RFC 8693): subjects = client, user, actor; target = grant type, requested token type, audience
  • oauth_session_refresh_token_issued: row ID, kind (session or schedule), session key
  • oauth_id_jag_issued: jti, audience, client
  • oauth_id_jag_redeemed: jti, source issuer, source subject, local user, approval record ID; outcome denied with a reason (no mapping, no approval, reuse refused, actor not allowed)
  • oauth_delegated_approval_created / oauth_delegated_approval_revoked: source = user or administrator
  • oauth_token_revoked: single token, or user-wide with the revocation time

Record identifiers only, never token values or assertion contents. The binding key is an identifier, not a secret. Operation and attempt IDs a client passes are recorded as client-reported values.

Activity

  1. jhrozek commented on Nov 14, 2026

    @jhrozek
    ContributorAuthor

    Adding the exchange-grant events from #6886 here so they land with the rest of the authserver audit work. #7130 (ID-JAG issuance) defers its audit line to this issue on purpose.

    Events, all emitted by the token endpoint's refresh-token exchange handler:

    Event When Subjects / target
    exchange_grant.id_jag_issued An ID-JAG is minted from an exchange grant (#6898) client_id, sub, target (the AS-2 issuer), resource, jti, has_parent, exp
    exchange_grant.created A login access token is exchanged for a grant (#6897) client_id, sub, issuable token type, expires_at
    exchange_grant.derived A child grant is derived from a grant (#6897) client_id, sub, parent, issuable token type, expires_at

    Refusals use the same event types with outcome failure and the stable reason (grant_invalid, revoked, parent_revoked, not_issuable, ...), as the issue already describes for the OAuth events.

    exchange_grant.id_jag_issued is the name #6898 asks for, so I'd keep it even though it doesn't follow the oauth_* scheme above. sub_id and upstream_subject stay out of these events, like the WARN logs, because they carry the user's upstream identity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions