[PM-40216] feat: Data-protect OrganizationInviteLink.Code at rest#7969
[PM-40216] feat: Data-protect OrganizationInviteLink.Code at rest#7969r-tome wants to merge 3 commits into
Conversation
Stores invite link Code as IDataProtector-encrypted NVARCHAR(300) with a "P|" prefix, replacing the plaintext UNIQUEIDENTIFIER. All anonymous endpoints now require OrganizationId + Code; lookup by Code is replaced with lookup-by-org + constant-time FixedTimeEquals comparison, eliminating the Code as a DB lookup key and closing the DB-read bearer-secret exposure. Includes SQL Server migration (coordinated deploy), EF Core migrations for MySQL/PostgreSQL/SQLite, SSDT schema update, and the InviteLinkCodeValidator helper for constant-time comparisons.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the data-protection of Code Review DetailsNo new findings. Notes from analysis (no action required):
|
| migrationBuilder.DropIndex( | ||
| name: "IX_OrganizationInviteLink_Code", | ||
| table: "OrganizationInviteLink"); | ||
| } |
There was a problem hiding this comment.
❓ QUESTION: Are pre-existing invite-link codes normalized to lowercase on SQLite/MySQL/PostgreSQL?
Details
The MSSQL migration deliberately runs UPDATE ... SET [Code] = LOWER([Code]) with the comment "SQL Server casts UNIQUEIDENTIFIER to uppercase NVARCHAR; LOWER() normalizes for FixedTimeEquals". This is because InviteLinkCodeValidator.CodesMatch uses CryptographicOperations.FixedTimeEquals on raw UTF-8 bytes — a case-sensitive comparison — while the incoming request.Code.ToString() is always lowercase (Guid.ToString() in .NET is lowercase).
This SQLite migration (and the MySQL/Postgres ones) only drops the index / alters the column type; none of them lowercase existing rows. For legacy invite links created before this PR, Code was stored via EF's Guid-to-TEXT conversion — the same conversion used for Id/OrganizationId, which EF renders in uppercase in SQLite.
If SQLite stored those legacy codes uppercase, they will no longer match the lowercase incoming code after migration, silently breaking every pre-existing invite link on self-hosted SQLite deployments (Postgres uuid→text and MySQL char(36) are lowercase, so likely unaffected).
Can you confirm the stored casing on each EF provider, and add a normalization step for any provider that stores uppercase — mirroring the MSSQL LOWER() — so existing links keep working?
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #7969 +/- ##
===========================================
- Coverage 62.03% 14.99% -47.05%
===========================================
Files 2276 1382 -894
Lines 99404 60207 -39197
Branches 8980 4782 -4198
===========================================
- Hits 61667 9027 -52640
- Misses 35565 51025 +15460
+ Partials 2172 155 -2017 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…-invite-link-codes # Conflicts: # test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs # test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQueryTests.cs
…ation CI's migration order check failed because main gained 2026-07-14_00_AddCreationDateToOrganizationUserUserDetailsView.sql after this branch's migration was authored.
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-40216
📔 Objective
The invite link
Codeis a bearer secret — anyone with DB read access (breach, backup dump, self-hosted operator) could obtain every org's join capability and walk a compliant email to Accepted. This PR data-protectsCodeat rest viaIDataProtector, matching the existing pattern inUserRepositoryandSendRepository.Key changes:
OrganizationInviteLink.Codechanges fromGuidtostring(wire type staysGuid); stored as"P|" + protector.Protect(code)in a newNVARCHAR(300)columnGetByCodeAsyncis removed; all anonymous endpoints now require bothOrganizationIdandCode— fetch by org, then validateCodeviaCryptographicOperations.FixedTimeEquals(constant-time, no oracle)Codedropped (encrypted values are non-deterministic)InviteLinkCodeValidatorstatic helper centralises the constant-time comparison/#/join/{orgId}/{code}?key=...(clients PR companion)Security properties after this change: