Skip to content

Auth/PM-42167 - SSO - Add Staged Org User Support - #8228

Merged
JaredSnider-Bitwarden merged 11 commits into
mainfrom
auth/pm-42167/sso-add-staged-org-users-support
Aug 25, 2026
Merged

Auth/PM-42167 - SSO - Add Staged Org User Support#8228
JaredSnider-Bitwarden merged 11 commits into
mainfrom
auth/pm-42167/sso-add-staged-org-users-support

Conversation

@JaredSnider-Bitwarden

@JaredSnider-Bitwarden JaredSnider-Bitwarden commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-42167
Clients PR: bitwarden/clients#22562

📔 Objective

Adds SSO support for organization users in the Staged status (PM34423StagedStatus flag-gated).

  • New user JIT-provisioning against a Staged row — creates the BW user and completes the join in one flow, mutating the Staged row in place. Seat check runs before user creation so rejection preserves prior state.
  • Existing BW user SSO against a Staged row — promotes Staged → Invited, dispatches the invite email, and returns a dedicated StagedOrgUserInviteAcceptanceRequired SSO redirect error code (client PR renders a "check your email" terminal page).

Also extracts a shared seat-availability check for both paths and adds Staged to the existing Key Connector guard so KC users hitting a Staged row get the KC error instead of falling through to promotion.

📸 Screenshots

See Clients PR: bitwarden/clients#22562

Promotes a Staged OrgUser row to Invited when a brand-new user JIT-provisions
against it via SSO (AutoProvisionUserAsync Scenario 3). Behind the
PM34423StagedStatus feature flag.

Extracts the pre-existing seat-availability check into a private helper and
calls it from both the fresh-JIT site and the new Staged-promotion site.
Staged rows don't count against occupied seats but Invited does, so the
promotion must run the same "enough seats or autoscale" check.

Integration tests cover: the Staged-promotion happy path, promotion blocked
at seat cap (self-hosted throw and cloud autoscale-fails throw), and all six
branches of the seat-availability check as lock-in for the extraction.
…tations

Scenario 3 of AutoProvisionUserAsync mutates the pre-existing OrganizationUser
row (UserId always, Status when the Staged promotion fires). The
OrganizationUser_Update sproc uses whatever RevisionDate the entity carries,
so callers must bump it explicitly — otherwise the row's watermark stays at
its Staged-creation timestamp and watermark-driven consumers miss the change.

Also updates the Staged-promotion integration test to capture the seeded
row's initial RevisionDate and assert the refreshed value has advanced.
…ged OrgUsers

When an existing BW user attempts SSO against a Staged OrganizationUser row
matching their email, promote the row to Invited, send the standard invite
email, and redirect the SSO callback to /login with InviteAcceptanceRequired
so the user completes acceptance via the normal token flow.

Behind the PM34423StagedStatus feature flag. Extracted into a
PromoteStagedOrgUserAndSendInviteAsync helper that verifies seat capacity
(autoscales on cloud when possible), flips status, bumps RevisionDate, logs
OrganizationUser_Invited, and dispatches ISendOrganizationInvitesCommand.
UserId stays null to match the standard admin-invite shape.

Integration tests cover: promote+invite happy path, self-hosted seat cap
throws NoSeatsAvailable (no invite sent, row unmutated), cloud autoscale
fails NoSeatsAvailable (same).

TODOs left inline for product/AC to weigh in on (a) whether
OrganizationUser_Invited is the right event type when the trigger is a user
SSO login rather than an admin action and (b) whether the client redirect
should carry a Staged-specific error code distinct from InviteAcceptanceRequired.
@JaredSnider-Bitwarden JaredSnider-Bitwarden added ai-review Request a Claude code review t:feature Change Type - Feature Development labels Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-reviewed at head 4b6946ee. The only delta since the previously-approved 1d2960f6 is a merge from main — no PR-scoped source file changed (the three Sso packages.lock.json updates came in with main and fall outside the PR diff against the new merge base), so all six changed files are byte-identical to the state already approved.

Re-verified the two places the merge could have interacted with this feature and both hold: Organization_ReadOccupiedSeatCountByOrganizationId still counts only Status IN (0, 1, 2), so the Staged = 3 row genuinely does not occupy a seat and the EnsureSeatAvailableAsync gating in front of both the JIT and the promote-and-invite paths is correctly placed; and the new user-filtering in SendOrganizationInvitesCommand.ValidateInvitedUsersAsync (from #8151) cannot silently drop the promoted row, because a Staged row has UserId = null and is therefore only ever resolved through GetByOrganizationEmailAsync, which guarantees a non-empty Email.

No new findings; the two findings raised on earlier heads remain fixed and all five existing threads are resolved.

Code Review Details

No findings.

Considered and dropped: the seat cap grown by AutoAddSeatsAsync is not reverted when the invite send later fails and PromoteStagedOrgUserAndSendInviteAsync rolls the row back to Staged. This matches the pre-existing behavior of the JIT-provisioning path that shares the same helper, so it is not a regression introduced here.

Comment thread bitwarden_license/src/Sso/Controllers/AccountController.cs Outdated
Comment thread bitwarden_license/src/Sso/Controllers/AccountController.cs Outdated
Comment thread bitwarden_license/src/Sso/Controllers/AccountController.cs Dismissed
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.95833% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 63.23%. Comparing base (13faf3e) to head (4b6946e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...n_license/src/Sso/Controllers/AccountController.cs 98.85% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8228      +/-   ##
==========================================
+ Coverage   63.19%   63.23%   +0.03%     
==========================================
  Files        2409     2410       +1     
  Lines      104393   104463      +70     
  Branches     9453     9458       +5     
==========================================
+ Hits        65971    66052      +81     
+ Misses      36164    36155       -9     
+ Partials     2258     2256       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…motion

Introduces SsoAuthnStagedOrgUserRequiresInviteAcceptanceException + a
StagedOrgUserInviteAcceptanceRequired redirect error code so the web client
can prompt the user to check their email for the freshly-sent invite,
rather than reusing ssoOrgInviteAcceptanceRequired which implies the user
should already have an invite in hand.
@JaredSnider-Bitwarden JaredSnider-Bitwarden changed the title Auth/pm 42167/sso add staged org users support Auth/PM-42167 - SSO - Add Staged Org User Support Aug 19, 2026
Product confirmed no user event should be emitted when an SSO login
promotes a Staged OrganizationUser to Invited; drop the
OrganizationUser_Invited event log call and its pending TODO.
Comment thread bitwarden_license/src/Sso/Controllers/AccountController.cs Outdated
Comment thread bitwarden_license/src/Sso/Utilities/SsoRedirectUrlBuilder.cs
…ment

Drops the stale OrganizationUser_Invited event log claim (removed in
3575878) and rewrites the summary to describe only what this method
mutates, not the internals of EnsureSeatAvailableAsync,
ISendOrganizationInvitesCommand, or the accept endpoint.
…d promotions

When JIT-provisioning a BW User against a Staged OrganizationUser row and
the org is at seat cap without autoscale headroom, the seat check previously
ran after RegisterSSOAutoProvisionedUserAsync — leaving a password-less BW
User row plus a welcome email for a login that never completed.

Move the seat check to the pre-user-creation gate so a rejection preserves
prior state. Adds regression coverage: at-cap-autoscale-succeeds branch,
feature-flag-off boundary at seat cap, and a two-phase test verifying
rejection preserves state and retry after cap increase provisions cleanly.
…at guard

Adds OrganizationUserStatusType.Staged to the existing Key Connector guard in
AccountController.ExternalCallback so a Key Connector user who hits a Staged
OrganizationUser row in the SSO-target org is cleanly rejected before the
Staged-promotion branch runs. Previously the Staged case bypassed the guard
and fell through to PromoteStagedOrgUserAndSendInviteAsync, consuming a seat
and mailing an invite the KC user could not accept.

Adds integration test ExternalCallback_WithExistingKeyConnectorUser_AndStagedOrgUser_ReturnsError
alongside the two existing KC guard tests, asserting the KC error message,
that no invite email is sent, and that the Staged row is not mutated.
@JaredSnider-Bitwarden
JaredSnider-Bitwarden marked this pull request as ready for review August 20, 2026 20:06
@JaredSnider-Bitwarden
JaredSnider-Bitwarden requested a review from a team as a code owner August 20, 2026 20:06
@JaredSnider-Bitwarden
JaredSnider-Bitwarden requested review from enmande and ike-kottlowski and removed request for enmande August 20, 2026 20:06
…nd failure

If SendInvitesAsync throws after the Staged row has been flipped to
Invited, revert the row so a seat isn't consumed for an invite the
user never received and the next SSO attempt cleanly re-runs the
promotion instead of dead-ending on the "accept your invite" redirect.

@ike-kottlowski ike-kottlowski 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.

Out of band finding of the email failure seat consumption issue has been addressed.

Transition of User from staged to invited provided auto scale is enabled is accomplished. All other flows for JIT registration can complete with the promotion of the user to invited.

Everything is well tested, and code readability improvements are sound.

@bre-deploy
bre-deploy Bot deployed to EU-QA Cloud August 21, 2026 15:31 Active
@JaredSnider-Bitwarden
JaredSnider-Bitwarden enabled auto-merge (squash) August 25, 2026 20:37
@JaredSnider-Bitwarden
JaredSnider-Bitwarden merged commit 571f880 into main Aug 25, 2026
40 checks passed
@JaredSnider-Bitwarden
JaredSnider-Bitwarden deleted the auth/pm-42167/sso-add-staged-org-users-support branch August 25, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants