Skip to content

fix: validate identity_key against member's keyring, not org owner's#871

Merged
rohan-chaturvedi merged 1 commit intomainfrom
fix--user-wrapped-secrets-identity-check
May 4, 2026
Merged

fix: validate identity_key against member's keyring, not org owner's#871
rohan-chaturvedi merged 1 commit intomainfrom
fix--user-wrapped-secrets-identity-check

Conversation

@rohan-chaturvedi
Copy link
Copy Markdown
Member

@rohan-chaturvedi rohan-chaturvedi commented May 4, 2026

Summary

Three mutations introduced by the auth refactor (#849) — UpdateUserWrappedSecretsMutation, RecoverAccountKeyringMutation, and ChangeAccountPasswordMutation — compared the user-supplied identity_key against org.identity_key. That column is the org owner's identity_key, set once at org creation. Every Phase user derives their own keyring from their own mnemonic, so for any non-owner member the supplied identity_key won't match org.identity_key and the mutation rejects the call as "Invalid recovery proof."

This breaks production for non-owner members:

  • SSO recovery via `/recovery` (calls UpdateUserWrappedSecretsMutation)
  • Password recovery via `/recovery` (calls RecoverAccountKeyringMutation)
  • In-session password change via the change-password dialog (calls ChangeAccountPasswordMutation)

The intent of the check is "the new identity_key must match the one this member registered with", which is org_member.identity_key. Owner-only orgs happen to work because the owner's org_member.identity_key == org.identity_key.

Changes

  • backend/backend/graphene/mutations/organisation.py: in all three mutations, move the OrganisationMember.objects.get(...) lookup ahead of the validation and compare against org_member.identity_key. Update docstrings.
  • backend/tests/test_auth_password.py: tests had been mocking org.identity_key to drive the (incorrect) check. Switch the mocks to org_member.identity_key and update the assert-not-called assertions affected by the lookup-order change.

Test plan

  • pytest backend/tests/test_auth_password.py passes (54/54)
  • Manual: with a non-owner member account, run `/recovery` → mnemonic accepted, keyring rewrapped (currently rejected as "Invalid recovery proof")
  • Manual: change-password dialog as a non-owner → succeeds (currently rejected)

UpdateUserWrappedSecrets, RecoverAccountKeyring, and ChangeAccountPassword
all compared the supplied identity_key to org.identity_key — the org
owner's. Phase users each derive their own keyring from their own
mnemonic, so this rejected every legitimate call from a non-owner member
trying to rewrap their keyring (SSO recovery, password recovery, password
change). Compare against org_member.identity_key instead.

Update the docstrings and tests to reflect the corrected semantics.
@rohan-chaturvedi rohan-chaturvedi requested a review from nimish-ks May 4, 2026 10:03
@rohan-chaturvedi rohan-chaturvedi merged commit 4ee6543 into main May 4, 2026
15 checks passed
@rohan-chaturvedi rohan-chaturvedi deleted the fix--user-wrapped-secrets-identity-check branch May 4, 2026 11:10
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.

2 participants