Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SSO] New user provision flow #945

Merged
merged 8 commits into from
Oct 13, 2020
2 changes: 1 addition & 1 deletion bitwarden_license/src/Sso/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ private async Task<User> AutoProvisionUserAsync(string provider, string provider
OrganizationId = orgId.Value,
UserId = user.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Accepted
Status = OrganizationUserStatusType.Invited
};
await _organizationUserRepository.CreateAsync(orgUser);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class SetPasswordRequestModel
public KdfType Kdf { get; set; }
[Required]
public int KdfIterations { get; set; }
public string OrgIdentifier { get; set; }

public User ToUser(User existingUser)
{
Expand Down
1 change: 1 addition & 0 deletions src/Core/Services/IOrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? inviting
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService);
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Services/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public interface IUserService
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword,
string token, string key);
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key);
Task<IdentityResult> SetPasswordAsync(User user, string newMasterPassword, string key);
Task<IdentityResult> SetPasswordAsync(User user, string newMasterPassword, string key, string orgIdentifier = null);
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
KdfType kdf, int kdfIterations);
Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
Expand Down
54 changes: 39 additions & 15 deletions src/Core/Services/Implementations/OrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,9 +1074,9 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
throw new BadRequestException("User invalid.");
}

if (orgUser.Status != OrganizationUserStatusType.Invited)
if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings))
{
throw new BadRequestException("Already accepted.");
throw new BadRequestException("Invalid token.");
}

if (string.IsNullOrWhiteSpace(orgUser.Email) ||
Expand All @@ -1085,6 +1085,42 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
throw new BadRequestException("User email does not match invite.");
}

var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgUser.OrganizationId, user.Email, true);
if (existingOrgUserCount > 0)
{
throw new BadRequestException("You are already part of this organization.");
}

return await AcceptUserAsync(orgUser, user, userService);
}

public async Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService)
{
var org = await _organizationRepository.GetByIdentifierAsync(orgIdentifier);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to null check some things here? What if org is null?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - added null check for org

if (org == null)
{
throw new BadRequestException("Organization invalid.");
}

var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id);
if (orgUser == null)
{
throw new BadRequestException("User not found within organization.");
}

return await AcceptUserAsync(orgUser, user, userService);
}

private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
IUserService userService)
{
if (orgUser.Status != OrganizationUserStatusType.Invited)
{
throw new BadRequestException("Already accepted.");
}

if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)
{
var org = await GetOrgById(orgUser.OrganizationId);
Expand All @@ -1099,18 +1135,6 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
}
}

var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgUser.OrganizationId, user.Email, true);
if (existingOrgUserCount > 0)
{
throw new BadRequestException("You are already part of this organization.");
}

if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings))
{
throw new BadRequestException("Invalid token.");
}

if (!await userService.TwoFactorIsEnabledAsync(user))
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgUser.OrganizationId);
Expand All @@ -1124,10 +1148,10 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
orgUser.Status = OrganizationUserStatusType.Accepted;
orgUser.UserId = user.Id;
orgUser.Email = null;

await _organizationUserRepository.ReplaceAsync(orgUser);

// TODO: send notification emails to org admins and accepting user?

return orgUser;
}

Expand Down
15 changes: 12 additions & 3 deletions src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IReferenceEventService _referenceEventService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;

public UserService(
IUserRepository userRepository,
Expand All @@ -74,7 +75,8 @@ public UserService(
IPolicyRepository policyRepository,
IReferenceEventService referenceEventService,
CurrentContext currentContext,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
IOrganizationService organizationService)
: base(
store,
optionsAccessor,
Expand Down Expand Up @@ -107,6 +109,7 @@ public UserService(
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_globalSettings = globalSettings;
_organizationService = organizationService;
}

public Guid? GetProperUserId(ClaimsPrincipal principal)
Expand Down Expand Up @@ -579,7 +582,8 @@ public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPa
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}

public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key)
public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key,
string orgIdentifier = null)
{
if (user == null)
{
Expand All @@ -603,7 +607,12 @@ public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassw

await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);


if (!string.IsNullOrWhiteSpace(orgIdentifier))
{
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
}

return IdentityResult.Success;
}

Expand Down
5 changes: 4 additions & 1 deletion test/Core.Test/Services/UserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class UserServiceTests
private readonly IReferenceEventService _referenceEventService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;

public UserServiceTests()
{
Expand Down Expand Up @@ -69,6 +70,7 @@ public UserServiceTests()
_referenceEventService = Substitute.For<IReferenceEventService>();
_currentContext = new CurrentContext();
_globalSettings = new GlobalSettings();
_organizationService = Substitute.For<IOrganizationService>();

_sut = new UserService(
_userRepository,
Expand All @@ -95,7 +97,8 @@ public UserServiceTests()
_policyRepository,
_referenceEventService,
_currentContext,
_globalSettings
_globalSettings,
_organizationService
);
}

Expand Down