mirror of
https://github.com/bitwarden/server.git
synced 2025-04-07 05:58:13 -05:00
[Bug] Improve SSO user provision flow (#1022)
* Initial commit of provisioning updates * Updated strings * removed extra BANG * Separated orgUsers db lookup - prioritized existing user Id * Updated create sso record method // Added sproc for org/email retrieval
This commit is contained in:
parent
0d7c876904
commit
09aea4ed38
@ -358,12 +358,7 @@ namespace Bit.Sso.Controllers
|
|||||||
email = providerUserId;
|
email = providerUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid? orgId = null;
|
if (!Guid.TryParse(provider, out var orgId))
|
||||||
if (Guid.TryParse(provider, out var oId))
|
|
||||||
{
|
|
||||||
orgId = oId;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// TODO: support non-org (server-wide) SSO in the future?
|
// TODO: support non-org (server-wide) SSO in the future?
|
||||||
throw new Exception(_i18nService.T("SSOProviderIsNotAnOrgId", provider));
|
throw new Exception(_i18nService.T("SSOProviderIsNotAnOrgId", provider));
|
||||||
@ -407,55 +402,55 @@ namespace Bit.Sso.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
OrganizationUser orgUser = null;
|
OrganizationUser orgUser = null;
|
||||||
if (orgId.HasValue)
|
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||||
{
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgId.Value);
|
|
||||||
if (organization == null)
|
if (organization == null)
|
||||||
{
|
{
|
||||||
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
|
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to find OrgUser via existing User Id (accepted/confirmed user)
|
||||||
if (existingUser != null)
|
if (existingUser != null)
|
||||||
{
|
{
|
||||||
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
|
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
|
||||||
orgUser = orgUsers.SingleOrDefault(u => u.OrganizationId == orgId.Value &&
|
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
|
||||||
u.Status != OrganizationUserStatusType.Invited);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
if (orgUser == null)
|
if (orgUser == null)
|
||||||
{
|
{
|
||||||
if (organization.Seats.HasValue)
|
// Org User is not created - no invite has been sent
|
||||||
|
throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId.Value);
|
// Org User is invited - they must manually accept the invite via email and authenticate with MP
|
||||||
|
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepted or Confirmed - create SSO link and return;
|
||||||
|
await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId);
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
||||||
|
if (orgUser == null && organization.Seats.HasValue)
|
||||||
|
{
|
||||||
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId);
|
||||||
var availableSeats = organization.Seats.Value - userCount;
|
var availableSeats = organization.Seats.Value - userCount;
|
||||||
if (availableSeats < 1)
|
if (availableSeats < 1)
|
||||||
{
|
{
|
||||||
// No seats are available
|
|
||||||
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name));
|
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure user is not already invited to this org
|
// Create user record - all existing user flows are handled above
|
||||||
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
|
var user = new User
|
||||||
orgId.Value, email, false);
|
|
||||||
if (existingOrgUserCount > 0)
|
|
||||||
{
|
|
||||||
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
User user = null;
|
|
||||||
if (orgUser == null)
|
|
||||||
{
|
|
||||||
if (existingUser != null)
|
|
||||||
{
|
|
||||||
// TODO: send an email inviting this user to link SSO to their account?
|
|
||||||
throw new Exception(_i18nService.T("UserAlreadyExistsUseLinkViaSso"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user record
|
|
||||||
user = new User
|
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Email = email,
|
Email = email,
|
||||||
@ -463,16 +458,13 @@ namespace Bit.Sso.Controllers
|
|||||||
};
|
};
|
||||||
await _userService.RegisterUserAsync(user);
|
await _userService.RegisterUserAsync(user);
|
||||||
|
|
||||||
if (orgId.HasValue)
|
|
||||||
{
|
|
||||||
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
||||||
var twoFactorPolicy =
|
var twoFactorPolicy =
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(orgId.Value, PolicyType.TwoFactorAuthentication);
|
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.TwoFactorAuthentication);
|
||||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
||||||
{
|
{
|
||||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
{
|
{
|
||||||
|
|
||||||
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
||||||
{
|
{
|
||||||
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
||||||
@ -481,31 +473,27 @@ namespace Bit.Sso.Controllers
|
|||||||
});
|
});
|
||||||
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||||
}
|
}
|
||||||
// Create organization user record
|
|
||||||
|
// Create Org User if null or else update existing Org User
|
||||||
|
if (orgUser == null)
|
||||||
|
{
|
||||||
orgUser = new OrganizationUser
|
orgUser = new OrganizationUser
|
||||||
{
|
{
|
||||||
OrganizationId = orgId.Value,
|
OrganizationId = orgId,
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Type = OrganizationUserType.User,
|
Type = OrganizationUserType.User,
|
||||||
Status = OrganizationUserStatusType.Invited
|
Status = OrganizationUserStatusType.Invited
|
||||||
};
|
};
|
||||||
await _organizationUserRepository.CreateAsync(orgUser);
|
await _organizationUserRepository.CreateAsync(orgUser);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Since the user is already a member of this organization, let's link their existing user account
|
orgUser.UserId = user.Id;
|
||||||
user = existingUser;
|
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create sso user record
|
// Create sso user record
|
||||||
var ssoUser = new SsoUser
|
await CreateSsoUserRecord(providerUserId, user.Id, orgId);
|
||||||
{
|
|
||||||
ExternalId = providerUserId,
|
|
||||||
UserId = user.Id,
|
|
||||||
OrganizationId = orgId
|
|
||||||
};
|
|
||||||
await _ssoUserRepository.CreateAsync(ssoUser);
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@ -554,6 +542,17 @@ namespace Bit.Sso.Controllers
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateSsoUserRecord(string providerUserId, Guid userId, Guid orgId)
|
||||||
|
{
|
||||||
|
var ssoUser = new SsoUser
|
||||||
|
{
|
||||||
|
ExternalId = providerUserId,
|
||||||
|
UserId = userId,
|
||||||
|
OrganizationId = orgId
|
||||||
|
};
|
||||||
|
await _ssoUserRepository.CreateAsync(ssoUser);
|
||||||
|
}
|
||||||
|
|
||||||
private void ProcessLoginCallback(AuthenticateResult externalResult,
|
private void ProcessLoginCallback(AuthenticateResult externalResult,
|
||||||
List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||||
{
|
{
|
||||||
|
@ -27,5 +27,6 @@ namespace Bit.Core.Repositories
|
|||||||
Task CreateAsync(OrganizationUser obj, IEnumerable<SelectionReadOnly> collections);
|
Task CreateAsync(OrganizationUser obj, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task ReplaceAsync(OrganizationUser obj, IEnumerable<SelectionReadOnly> collections);
|
Task ReplaceAsync(OrganizationUser obj, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds);
|
Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds);
|
||||||
|
Task<OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,5 +244,18 @@ namespace Bit.Core.Repositories.SqlServer
|
|||||||
return results.ToList();
|
return results.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryAsync<OrganizationUser>(
|
||||||
|
"[dbo].[OrganizationUser_ReadByOrganizationIdEmail]",
|
||||||
|
new { OrganizationId = organizationId, Email = email },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return results.SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -521,10 +521,10 @@
|
|||||||
<value>No seats available for organization, '{0}'</value>
|
<value>No seats available for organization, '{0}'</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserAlreadyInvited" xml:space="preserve">
|
<data name="UserAlreadyInvited" xml:space="preserve">
|
||||||
<value>User, '{0}', has already been invited to this organization, '{1}'</value>
|
<value>User, '{0}', has already been invited to this organization, '{1}'. Accept the invite in order to log in with SSO.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserAlreadyExistsUseLinkViaSso" xml:space="preserve">
|
<data name="UserAlreadyExistsInviteProcess" xml:space="preserve">
|
||||||
<value>User already exists, please link account to SSO after logging in</value>
|
<value>In order to join this organization, contact an admin to send you an invite and follow the instructions within to accept.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RedirectGet" xml:space="preserve">
|
<data name="RedirectGet" xml:space="preserve">
|
||||||
<value>Redirect GET</value>
|
<value>Redirect GET</value>
|
||||||
|
@ -294,5 +294,7 @@
|
|||||||
<Build Include="dbo\Stored Procedures\TaxRate_ReadAllActive.sql" />
|
<Build Include="dbo\Stored Procedures\TaxRate_ReadAllActive.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\TaxRate_Create.sql" />
|
<Build Include="dbo\Stored Procedures\TaxRate_Create.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\TaxRate_Archive.sql" />
|
<Build Include="dbo\Stored Procedures\TaxRate_Archive.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\OrganizationUser_ReadByOrganizationIdEmail.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdEmail]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Email NVARCHAR(50)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUserView]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @OrganizationId
|
||||||
|
AND [Email] IS NOT NULL
|
||||||
|
AND @Email IS NOT NULL
|
||||||
|
AND [Email] = @Email
|
||||||
|
END
|
@ -0,0 +1,24 @@
|
|||||||
|
IF OBJECT_ID('[dbo].[OrganizationUser_ReadByOrganizationEmail]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationEmail]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdEmail]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Email NVARCHAR(50)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUserView]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @OrganizationId
|
||||||
|
AND [Email] IS NOT NULL
|
||||||
|
AND @Email IS NOT NULL
|
||||||
|
AND [Email] = @Email
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user