1
0
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:
Vincent Salucci 2020-12-04 16:45:54 -06:00 committed by GitHub
parent 0d7c876904
commit 09aea4ed38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 149 additions and 93 deletions

View File

@ -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)
{ {

View File

@ -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);
} }
} }

View File

@ -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();
}
}
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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