1
0
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:
Federico Maccaroni
2022-04-01 17:08:47 -03:00
committed by GitHub
parent ff23bb87c8
commit 6f60d24f5a
10 changed files with 251 additions and 23 deletions

View File

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