1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

chore(comments): [PM-24624] Add more comments to AutoProvisionUserAsync

* Comments in auto-provisioning logic.

* More clarifications.

* Changed method name.

* Updated response from method.

* Clarified message.
This commit is contained in:
Todd Martin
2025-08-14 12:08:41 -04:00
committed by GitHub
parent 4b751e8cbf
commit 4bad008085

View File

@@ -3,6 +3,7 @@
using System.Security.Claims;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
@@ -254,25 +255,25 @@ public class AccountController : Controller
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {@claims}", externalClaims);
// Lookup our user and external provider info
// Note: the user will only exist if the user has already been provisioned and exists in the User table and the SSO user table.
// See if the user has logged in with this SSO provider before and has already been provisioned.
// This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using.
var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
// The user has not authenticated with this SSO provider before.
// They could have an existing Bitwarden account in the User table though.
if (user == null)
{
// User does not exist in SSO User table. They could have an existing BW account in the User table.
// This might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
// If we're manually linking to SSO, the user's external identifier will be passed as query string parameter.
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
result.Properties.Items["user_identifier"] : null;
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
}
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
// Either way, we have associated the SSO login with a Bitwarden user.
// We will now sign the Bitwarden user in.
if (user != null)
{
// User was JIT provisioned (this could be an existing user or a new user)
// This allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
@@ -350,6 +351,10 @@ public class AccountController : Controller
}
}
/// <summary>
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
/// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.
/// </summary>
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims, SsoConfigurationData config)>
FindUserFromExternalProviderAsync(AuthenticateResult result)
{
@@ -407,6 +412,23 @@ public class AccountController : Controller
return (user, provider, providerUserId, claims, ssoConfigData);
}
/// <summary>
/// Provision an SSO-linked Bitwarden user.
/// This handles three different scenarios:
/// 1. Creating an SsoUser link for an existing User and OrganizationUser
/// - User is a member of the organization, but hasn't authenticated with the org's SSO provider before.
/// 2. Creating a new User and a new OrganizationUser, then establishing an SsoUser link
/// - User is joining the organization through JIT provisioning, without a pending invitation
/// 3. Creating a new User for an existing OrganizationUser (created by invitation), then establishing an SsoUser link
/// - User is signing in with a pending invitation.
/// </summary>
/// <param name="provider">The external identity provider.</param>
/// <param name="providerUserId">The external identity provider's user identifier.</param>
/// <param name="claims">The claims from the external IdP.</param>
/// <param name="userIdentifier">The user identifier used for manual SSO linking.</param>
/// <param name="config">The SSO configuration for the organization.</param>
/// <returns>The User to sign in.</returns>
/// <exception cref="Exception">An exception if the user cannot be provisioned as requested.</exception>
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
IEnumerable<Claim> claims, string userIdentifier, SsoConfigurationData config)
{
@@ -434,50 +456,15 @@ public class AccountController : Controller
}
else
{
var split = userIdentifier.Split(",");
if (split.Length < 2)
{
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
}
var userId = split[0];
var token = split[1];
var tokenOptions = new TokenOptions();
var claimedUser = await _userService.GetUserByIdAsync(userId);
if (claimedUser != null)
{
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
if (tokenIsValid)
{
existingUser = claimedUser;
}
else
{
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
}
}
existingUser = await GetUserFromManualLinkingData(userIdentifier);
}
OrganizationUser orgUser = null;
var organization = await _organizationRepository.GetByIdAsync(orgId);
if (organization == null)
{
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
}
// Try to find the OrganizationUser if it exists.
var (organization, orgUser) = await FindOrganizationUser(existingUser, email, orgId);
// Try to find OrgUser via existing User Id (accepted/confirmed user)
if (existingUser != null)
{
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
}
// If no Org User found by Existing User Id - search all organization users via email
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email);
// All Existing User flows handled below
//----------------------------------------------------
// Scenario 1: We've found the user in the User table
//----------------------------------------------------
if (existingUser != null)
{
if (existingUser.UsesKeyConnector &&
@@ -486,6 +473,8 @@ public class AccountController : Controller
throw new Exception(_i18nService.T("UserAlreadyExistsKeyConnector"));
}
// If the user already exists in Bitwarden, we require that the user already be in the org,
// and that they are either Accepted or Confirmed.
if (orgUser == null)
{
// Org User is not created - no invite has been sent
@@ -495,7 +484,11 @@ public class AccountController : Controller
EnsureOrgUserStatusAllowed(orgUser.Status, organization.DisplayName(),
allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]);
// Accepted or Confirmed - create SSO link and return;
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
// authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).
// We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed
// with authentication.
await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser);
return existingUser;
}
@@ -538,7 +531,9 @@ public class AccountController : Controller
emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;
}
// Create user record - all existing user flows are handled above
//--------------------------------------------------
// Scenarios 2 and 3: We need to register a new user
//--------------------------------------------------
var user = new User
{
Name = name,
@@ -564,7 +559,11 @@ public class AccountController : Controller
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
}
// Create Org User if null or else update existing Org User
//-----------------------------------------------------------------
// Scenario 2: We also need to create an OrganizationUser
// This means that an invitation was not sent for this user and we
// need to establish their invited status now.
//-----------------------------------------------------------------
if (orgUser == null)
{
orgUser = new OrganizationUser
@@ -576,18 +575,77 @@ public class AccountController : Controller
};
await _organizationUserRepository.CreateAsync(orgUser);
}
//-----------------------------------------------------------------
// Scenario 3: There is already an existing OrganizationUser
// That was established through an invitation. We just need to
// update the UserId now that we have created a User record.
//-----------------------------------------------------------------
else
{
orgUser.UserId = user.Id;
await _organizationUserRepository.ReplaceAsync(orgUser);
}
// Create sso user record
// Create the SsoUser record to link the user to the SSO provider.
await CreateSsoUserRecord(providerUserId, user.Id, orgId, orgUser);
return user;
}
private async Task<User> GetUserFromManualLinkingData(string userIdentifier)
{
User user = null;
var split = userIdentifier.Split(",");
if (split.Length < 2)
{
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
}
var userId = split[0];
var token = split[1];
var tokenOptions = new TokenOptions();
var claimedUser = await _userService.GetUserByIdAsync(userId);
if (claimedUser != null)
{
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
if (tokenIsValid)
{
user = claimedUser;
}
else
{
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
}
}
return user;
}
private async Task<(Organization, OrganizationUser)> FindOrganizationUser(User existingUser, string email, Guid orgId)
{
OrganizationUser orgUser = null;
var organization = await _organizationRepository.GetByIdAsync(orgId);
if (organization == null)
{
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
}
// Try to find OrgUser via existing User Id.
// This covers any OrganizationUser state after they have accepted an invite.
if (existingUser != null)
{
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
}
// If no Org User found by Existing User Id - search all the organization's users via email.
// This covers users who are Invited but haven't accepted their invite yet.
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email);
return (organization, orgUser);
}
private void EnsureOrgUserStatusAllowed(
OrganizationUserStatusType status,
string organizationDisplayName,