Problem
When an administrator of organization A deactivates or revokes a membership, membership.Service.SetStatus (backend/internal/application/membership/members.go, the s.revoker.RevokeAllUserTokens(ctx, m.UserID) call) destroys every refresh-token lineage of that person, including their sessions in organization B.
The person can still sign in to B (#807 made sure of that), so this is not a lockout. But an action in A signs the person out of B, and that effect crosses the tenant boundary. Refresh tokens carry tenant_id (refresh_tokens.tenant_id), so the revocation could be limited to A's lineages.
Acceptance criteria
- Withdrawing a membership in A revokes only the refresh tokens whose
tenant_id is A.
- A test with one person holding live sessions in A and B: after A withdraws access, the A refresh fails and the B refresh succeeds.
- Paths that must still revoke everything (password change, account-level security events) keep doing so, and a test covers one of them.
Definition of Done
Criterion 2's test output is pasted on this issue.
Touches session revocation, so per CLAUDE.md it needs a tech-lead review of the approach before implementation.
Found while working #807.
Problem
When an administrator of organization A deactivates or revokes a membership,
membership.Service.SetStatus(backend/internal/application/membership/members.go, thes.revoker.RevokeAllUserTokens(ctx, m.UserID)call) destroys every refresh-token lineage of that person, including their sessions in organization B.The person can still sign in to B (#807 made sure of that), so this is not a lockout. But an action in A signs the person out of B, and that effect crosses the tenant boundary. Refresh tokens carry
tenant_id(refresh_tokens.tenant_id), so the revocation could be limited to A's lineages.Acceptance criteria
tenant_idis A.Definition of Done
Criterion 2's test output is pasted on this issue.
Touches session revocation, so per CLAUDE.md it needs a tech-lead review of the approach before implementation.
Found while working #807.