Revoke connector refresh families and shared-password cookies on credential change - #29
Merged
Merged
Conversation
…ential change One on_credentials_changed(email, *, keep=None) hook, built by the plugin and injected into the auth and admin routers, now runs on every path that changes an account's credentials (admin delete, admin password set, self-service change, reset-link redemption; the set-password CLI does the same). It revokes the account's browser sessions as before and, when the OAuth server keeps refresh tokens, tombstones and deletes every refresh family issued to that account and drops its unredeemed authorization codes. shared_auth_<app> cookies now sign a keyed fingerprint of the app's current shared-password hash instead of a constant; the middleware and the shared-login page compare it, so rotating a shared password ends older cookies, and an app with no configured password admits no cookie. SessionStore takes an optional max_age and sweeps a bounded batch of expired records whenever a session is created. Closes #26 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
Author
|
Dependents run against this branch (fresh venvs, local editable enlace + this enlace_auth):
|
- revoke_refresh_subject first writes a per-subject marker (revoked_before=T, lives for the family max lifetime). The code grant refuses codes issued at or before T (codes now record when the session was read), and the refresh grant and replay path refuse families authorized at or before T (records carry auth_at). This covers a family whose code was consumed before the scan but written after it, and a code written after the code-store scan. - The plugin wires connector revocation even while the OAuth server is off. - The hook runs connector revocation even if session revocation raises. - set-password reports clearly if connector revocation fails. - list-connector-sessions skips revocation markers. - SessionStore sweeps at most once per sweep_interval (default 1h) per process, so a login never pays a directory walk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #26
What
enlace_auth.auth.revocationmodule.make_on_credentials_changed(session_store, *, refresh_store=None, code_store=None, tombstone_ttl=0)returnshook(email, *, keep=None). The plugin builds it and injects it intomake_auth_routerandmake_admin_routerthrough a new keyword-onlyon_credentials_changedargument. If the argument is not passed, the hook revokes sessions only, as before. It runs on admin delete, admin password set, self-service change and reset-link redemption. Theset-passwordCLI also revokes the account's connector families.revoke_refresh_subjectfinds every family whose records belong to the email (case-insensitive). For each one,revoke_refresh_familywrites thefamily:<id>tombstone first and then deletes the records. This is the same logic the router's_revoke_familynow delegates to, so the refresh grant refuses any family that is rotating concurrently. It also drops the subject's unredeemed authorization codes. Access JWTs that were already issued still live out their TTL.shared_auth_<app>now signs an HMAC (keyed with the signing key) of the app's current shared-password hash, where it used to sign the constant"1". The middleware and/auth/shared-logincompare it in constant time. Rotating the hash therefore ends older cookies. An app with no configured hash now admits no cookie; before, it would accept a signed constant.SessionStore(store, *, max_age=None, sweep_batch=100)gets a bounded sweep with a cursor, run oncreate(). The plugin passessession_max_age.Compatibility
"1") are refused after deploy, so shared-app users re-enter the password once.tests/test_auth_middleware.py::test_protected_shared_with_valid_cookieasserted the old constant-cookie behaviour that this issue removes. It now mints a fingerprint cookie. Two added tests cover the legacy/rotated cookie and the no-hash case.enlace-auth revoke-connector-sessionstill deletes records without writing tombstones. Its tests assume the store holds only token records.Tests
tests/test_credential_revocation.py: helper unit tests; end-to-end through the plugin with the OAuth server enabled (admin set password, admin delete, self-service change → the victim's family is tombstoned, the other user's family survives, codes are dropped); shared-password rotation across a restart refuses the old cookie; sweep boundedness. Full suite: 399 passed.Independent refute-review (security) — findings and fixes
revoke_refresh_subjectnow writes a per-subject marker first (revoked_before, which lives for the family max lifetime). The code grant refuses codes issued at or before it; codes now record when the session was read. The refresh grant and_replayedrefuse and revoke families authorized at or before it; records carryauth_at, and legacy records derive it fromfamily_exp. Tests cover both cases and fail when the check is removed.sweep_interval(1 h) per process.set-passwordreports a partial failure clearly.list-connector-sessionsskips markers.revoke-connector-sessionstill deletes without tombstones (existing tests). Revocation cost is O(families × store), but it only runs on admin and credential-change requests.🤖 Generated with Claude Code