mirror of
https://github.com/bitwarden/server.git
synced 2025-07-03 09:02:48 -05:00
Email verification for new devices (#1931)
* PS-56 Added Email 2FA on login with new devices that don't have any 2FA enabled * PS-56 Fixed wrong argument in VerifyTwoFactor call
This commit is contained in:

committed by
GitHub

parent
ff23bb87c8
commit
6f60d24f5a
@ -94,19 +94,24 @@ namespace Bit.Core.IdentityServer
|
||||
return;
|
||||
}
|
||||
|
||||
var twoFactorRequirement = await RequiresTwoFactorAsync(user, request.GrantType);
|
||||
if (twoFactorRequirement.Item1)
|
||||
var (isTwoFactorRequired, requires2FABecauseNewDevice, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
|
||||
if (isTwoFactorRequired)
|
||||
{
|
||||
// Just defaulting it
|
||||
var twoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType))
|
||||
{
|
||||
await BuildTwoFactorResultAsync(user, twoFactorRequirement.Item2, context);
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context, requires2FABecauseNewDevice);
|
||||
return;
|
||||
}
|
||||
|
||||
var verified = await VerifyTwoFactor(user, twoFactorRequirement.Item2,
|
||||
BeforeVerifyTwoFactor(user, twoFactorProviderType, requires2FABecauseNewDevice);
|
||||
|
||||
var verified = await VerifyTwoFactor(user, twoFactorOrganization,
|
||||
twoFactorProviderType, twoFactorToken);
|
||||
|
||||
AfterVerifyTwoFactor(user, twoFactorProviderType, requires2FABecauseNewDevice);
|
||||
|
||||
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
{
|
||||
await UpdateFailedAuthDetailsAsync(user, true, unknownDevice);
|
||||
@ -117,7 +122,7 @@ namespace Bit.Core.IdentityServer
|
||||
{
|
||||
// Delay for brute force.
|
||||
await Task.Delay(2000);
|
||||
await BuildTwoFactorResultAsync(user, twoFactorRequirement.Item2, context);
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context, requires2FABecauseNewDevice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -188,7 +193,7 @@ namespace Bit.Core.IdentityServer
|
||||
await SetSuccessResult(context, user, claims, customResponse);
|
||||
}
|
||||
|
||||
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
|
||||
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context, bool requires2FABecauseNewDevice)
|
||||
{
|
||||
var providerKeys = new List<byte>();
|
||||
var providers = new Dictionary<string, Dictionary<string, object>>();
|
||||
@ -213,8 +218,23 @@ namespace Bit.Core.IdentityServer
|
||||
|
||||
if (!enabledProviders.Any())
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
return;
|
||||
if (!requires2FABecauseNewDevice)
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
var emailProvider = new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
||||
Enabled = true
|
||||
};
|
||||
enabledProviders.Add(new KeyValuePair<TwoFactorProviderType, TwoFactorProvider>(
|
||||
TwoFactorProviderType.Email, emailProvider));
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = emailProvider
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var provider in enabledProviders)
|
||||
@ -234,7 +254,7 @@ namespace Bit.Core.IdentityServer
|
||||
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
|
||||
{
|
||||
// Send email now if this is their only 2FA method
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
await _userService.SendTwoFactorEmailAsync(user, requires2FABecauseNewDevice);
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,12 +290,12 @@ namespace Bit.Core.IdentityServer
|
||||
|
||||
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
private async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, string grantType)
|
||||
private async Task<Tuple<bool, bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (grantType == "client_credentials")
|
||||
if (request.GrantType == "client_credentials")
|
||||
{
|
||||
// Do not require MFA for api key logins
|
||||
return new Tuple<bool, Organization>(false, null);
|
||||
return new Tuple<bool, bool, Organization>(false, false, null);
|
||||
}
|
||||
|
||||
var individualRequired = _userManager.SupportsUserTwoFactor &&
|
||||
@ -297,7 +317,15 @@ namespace Bit.Core.IdentityServer
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
||||
var requires2FA = individualRequired || firstEnabledOrg != null;
|
||||
var requires2FABecauseNewDevice = !requires2FA
|
||||
&& user.EmailVerified
|
||||
&& request.GrantType != "authorization_code"
|
||||
&& await IsNewDeviceAndNotTheFirstOneAsync(user, request);
|
||||
|
||||
requires2FA = requires2FA || requires2FABecauseNewDevice;
|
||||
|
||||
return new Tuple<bool, bool, Organization>(requires2FA, requires2FABecauseNewDevice, firstEnabledOrg);
|
||||
}
|
||||
|
||||
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
|
||||
@ -375,6 +403,33 @@ namespace Bit.Core.IdentityServer
|
||||
};
|
||||
}
|
||||
|
||||
private void BeforeVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
||||
{
|
||||
if (type == TwoFactorProviderType.Email
|
||||
&&
|
||||
requires2FABecauseNewDevice)
|
||||
{
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void AfterVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
||||
{
|
||||
if (type == TwoFactorProviderType.Email
|
||||
&&
|
||||
requires2FABecauseNewDevice)
|
||||
{
|
||||
user.ClearTwoFactorProviders();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
|
||||
string token)
|
||||
{
|
||||
@ -480,6 +535,23 @@ namespace Bit.Core.IdentityServer
|
||||
|
||||
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
|
||||
}
|
||||
|
||||
protected async Task<bool> IsNewDeviceAndNotTheFirstOneAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (!devices.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !devices.Any(d => d.Identifier == GetDeviceFromRequest(request)?.Identifier);
|
||||
}
|
||||
|
||||
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
var device = GetDeviceFromRequest(request);
|
||||
|
Reference in New Issue
Block a user