diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index b86969acb..27d9266db 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -11,6 +11,11 @@ + + + + + diff --git a/src/Core/Enums/DeviceType.cs b/src/Core/Enums/DeviceType.cs index 69b740756..53aa21c76 100644 --- a/src/Core/Enums/DeviceType.cs +++ b/src/Core/Enums/DeviceType.cs @@ -1,27 +1,50 @@ -namespace Bit.Core.Enums +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Enums { public enum DeviceType : byte { + [Display(Name = "Android")] Android = 0, + [Display(Name = "iOS")] iOS = 1, + [Display(Name = "Chrome Extension")] ChromeExtension = 2, + [Display(Name = "Firefox Extension")] FirefoxExtension = 3, + [Display(Name = "Opera Extension")] OperaExtension = 4, + [Display(Name = "Edge Extension")] EdgeExtension = 5, + [Display(Name = "Windows")] WindowsDesktop = 6, + [Display(Name = "macOS")] MacOsDesktop = 7, + [Display(Name = "Linux")] LinuxDesktop = 8, + [Display(Name = "Chrome")] ChromeBrowser = 9, + [Display(Name = "Firefox")] FirefoxBrowser = 10, + [Display(Name = "Opera")] OperaBrowser = 11, + [Display(Name = "Edge")] EdgeBrowser = 12, + [Display(Name = "Internet Explorer")] IEBrowser = 13, + [Display(Name = "Unknown Browser")] UnknownBrowser = 14, + [Display(Name = "Android")] AndroidAmazon = 15, + [Display(Name = "UWP")] UWP = 16, + [Display(Name = "Safari")] SafariBrowser = 17, + [Display(Name = "Vivaldi")] VivaldiBrowser = 18, + [Display(Name = "Vivaldi Extension")] VivaldiExtension = 19, + [Display(Name = "Safari Extension")] SafariExtension = 20 } } diff --git a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs index 3c3b34c71..615d9d365 100644 --- a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -15,6 +15,8 @@ using Bit.Core.Models; using Bit.Core.Identity; using Bit.Core.Models.Data; using Bit.Core.Utilities; +using System.ComponentModel.DataAnnotations; +using System.Reflection; namespace Bit.Core.IdentityServer { @@ -29,6 +31,7 @@ namespace Bit.Core.IdentityServer private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IApplicationCacheService _applicationCacheService; + private readonly IMailService _mailService; private readonly CurrentContext _currentContext; public ResourceOwnerPasswordValidator( @@ -41,6 +44,7 @@ namespace Bit.Core.IdentityServer IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, + IMailService mailService, CurrentContext currentContext) { _userManager = userManager; @@ -52,6 +56,7 @@ namespace Bit.Core.IdentityServer _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _applicationCacheService = applicationCacheService; + _mailService = mailService; _currentContext = currentContext; } @@ -373,6 +378,16 @@ namespace Bit.Core.IdentityServer { device.UserId = user.Id; await _deviceService.SaveAsync(device); + + var now = DateTime.UtcNow; + if(now - user.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = device.Type.GetType().GetMember(device.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, + _currentContext.IpAddress); + } + return device; } diff --git a/src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.html.hbs b/src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.html.hbs new file mode 100644 index 000000000..a031cb04c --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.html.hbs @@ -0,0 +1,21 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + +
+ Your Bitwarden account was just logged into from a new device. +
+ Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
+ IP Address: {{IpAddress}}
+ Device Type: {{DeviceType}} +
+ You can deauthorize all devices that have access to your account from the web vault under Settings → My Account → Deauthorize Sessions. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.text.hbs b/src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.text.hbs new file mode 100644 index 000000000..06b3661cb --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.text.hbs @@ -0,0 +1,10 @@ +{{#>BasicTextLayout}} +Your Bitwarden account was just logged into from a new device. + +Date: {{TheDate}} at {{TheTime}} {{TimeZone}} +IP Address: {{IpAddress}} +Device Type: {{DeviceType}} + +You can deauthorize all devices that have access to your account from the +web vault under Settings > My Account > Deauthorize Sessions. +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Models/Mail/NewDeviceLoggedInModel.cs b/src/Core/Models/Mail/NewDeviceLoggedInModel.cs new file mode 100644 index 000000000..ee550fc4e --- /dev/null +++ b/src/Core/Models/Mail/NewDeviceLoggedInModel.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Models.Mail +{ + public class NewDeviceLoggedInModel : BaseMailModel + { + public string TheDate { get; set; } + public string TheTime { get; set; } + public string TimeZone { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 9a2a2aefe..f43dc3c14 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -20,5 +20,6 @@ namespace Bit.Core.Services Task SendOrganizationConfirmedEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, List items, bool mentionInvoices); + Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip); } } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 1d99cf4d7..7f2d9e0c5 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -236,6 +236,24 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip) + { + var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email); + var model = new NewDeviceLoggedInModel + { + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + DeviceType = deviceType, + TheDate = timestamp.ToLongDateString(), + TheTime = timestamp.ToShortTimeString(), + TimeZone = "UTC", + IpAddress = ip + }; + await AddMessageContentAsync(message, "NewDeviceLoggedIn", model); + message.MetaData.Add("SendGridCategories", new List { "NewDeviceLoggedIn" }); + await _mailDeliveryService.SendEmailAsync(message); + } + private MailMessage CreateDefaultMessage(string subject, string toEmail) { return CreateDefaultMessage(subject, new List { toEmail }); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index fa6ec07dd..5857555f2 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -72,5 +72,10 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip) + { + return Task.FromResult(0); + } } }