Skip to content

feat: re-authorize live subscriptions; revoke on permission loss or token expiry (#1414) - #1535

Merged
kriszyp merged 5 commits into
mainfrom
kris/sub-stale-auth-1414
Jul 6, 2026
Merged

feat: re-authorize live subscriptions; revoke on permission loss or token expiry (#1414)#1535
kriszyp merged 5 commits into
mainfrom
kris/sub-stale-auth-1414

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 30, 2026

Copy link
Copy Markdown
Member

Draft — security-sensitive, hot-path adjacent. Posting for a review pass before merge.

Summary

Subscribe-time authorization is point-in-time: once an SSE/WebSocket/MQTT stream is open it keeps delivering even after the principal loses access (drop_user, role/permission change) or the bearer token it was opened with expires (#1414). This adds continuous re-authorization that terminates such subscriptions.

Re-authorization is table/RBAC-level — it re-runs the same allowRead the subscription was granted with, against a freshly-fetched user. There is no per-record evaluation (consistent with the allowRead grant model and default RBAC's table-level granularity).

How it works

  • server/liveSubscriptionAuth.ts — a registry of live subscriptions, each with a recheck() and a terminate(). Swept:
    1. immediately on the ITC user-change broadcast — serverHandlers.userHandler rebuilds the user/role cache before firing listeners, so the recheck observes current permissions; and
    2. on a 30s interval (your choice (a)) as a backstop and to catch token expiry, which is not event-signaled.
      An error during recheck fails closed (revokes). Normal teardown (end()/close) auto-unregisters.
  • resources/Resource.ts — registers the resulting subscription at the common auth chokepoint (authorizeActionOnResource) for both the subscribe (MQTT) and connect (SSE/WebSocket) actions. Subscriptions with no user principal (internal watchers, replication, local-bypass) are skipped.
  • security/auth.ts — captures the bearer token's JWT exp on the authenticated user so a subscription opened with it can be revoked on expiry.

The recheck interval is overridable via HARPER_SUBSCRIPTION_REAUTH_INTERVAL_MS (used by the test).

Terminate semantics

terminate() calls the subscription's end(), which removes it from the broadcast notify loop — so no further events are delivered on either transport (the security-critical outcome). The transport connection may linger idle until the client disconnects; a clean socket close is a possible refinement.

Testing

integrationTests/security/subscription-revocation.test.ts opens an SSE collection subscription and asserts delivery stops after (1) drop_user (event-driven) and (2) bearer-token expiry (interval-driven), while an authorized stream keeps delivering. 2/2 pass. tsc clean.

Notes / review asks

Closes #1414.

🤖 Generated with Claude Code

…oken expiry (#1414)

Subscribe-time authorization is point-in-time: once an SSE/WebSocket/MQTT stream is
open it keeps delivering even after the principal loses access (drop_user, role or
permission change) or the bearer token it was opened with expires. This adds a
continuous re-authorization registry that terminates such subscriptions.

- server/liveSubscriptionAuth.ts: a registry of live subscriptions, each with a
  table/RBAC-level recheck and a terminate handler. Swept (1) immediately on the ITC
  user-change broadcast — serverHandlers rebuilds the user/role cache before firing
  listeners, so the recheck sees current permissions — and (2) on a 30s interval as a
  backstop and to catch token expiry, which is not event-signaled. Re-auth is
  table-level (re-runs the same allowRead the subscription was granted with against a
  freshly-fetched user); there is NO per-record evaluation. An error during recheck
  fails closed (revokes). Normal teardown auto-unregisters.

- resources/Resource.ts: at the common authorization chokepoint
  (authorizeActionOnResource), register the resulting subscription for both the
  'subscribe' (MQTT) and 'connect' (SSE/WebSocket) actions. Subscriptions with no user
  principal (internal watchers, replication, local-bypass) are skipped.

- security/auth.ts: capture the bearer token's JWT exp on the authenticated user so a
  subscription opened with it can be revoked once it expires.

Re-auth interval is overridable via HARPER_SUBSCRIPTION_REAUTH_INTERVAL_MS (tests).

Test: integrationTests/security/subscription-revocation.test.ts opens an SSE collection
subscription and asserts delivery STOPS after (1) drop_user (event-driven) and (2)
bearer-token expiry (interval-driven), while an authorized stream keeps delivering. 2/2
pass.

Closes #1414.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements continuous re-authorization for live subscriptions (SSE, WebSocket, MQTT) to ensure they are terminated if a user's permissions are revoked or if their bearer token expires. The implementation includes a new registry and background sweeper, along with comprehensive integration tests. The review feedback highlights two important issues: a critical security vulnerability where stale user data in the context could bypass authorization checks during re-evaluation, and a potential stream cleanup bug where arguments are not forwarded when overriding the subscription's end method.

Comment thread resources/Resource.ts
Comment thread server/liveSubscriptionAuth.ts
@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

kriszyp and others added 2 commits June 30, 2026 11:33
…gs, format

- recheck advances context.user to the freshly-fetched user before re-running allowRead,
  so a custom allowRead reading context.user / getCurrentUser() evaluates current state
  rather than the stale subscribe-time user (Gemini critical).
- the wrapped subscription.end() forwards all arguments to the original end() so stream
  cleanup semantics are preserved (Gemini high).
- prettier formatting on the new test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review June 30, 2026 17:38

@cb1kenobi cb1kenobi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an epic feature, love it!

Kris Zyp and others added 2 commits July 6, 2026 16:57
# Conflicts:
#	resources/Resource.ts
#	security/auth.ts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp
kriszyp merged commit ece7da4 into main Jul 6, 2026
44 of 47 checks passed
@kriszyp
kriszyp deleted the kris/sub-stale-auth-1414 branch July 6, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Live subscriptions (SSE/MQTT/WS) continue delivering events after drop_user / role revocation — stale-auth leak

2 participants