From 37ec1de7a34856e831f7a230dbc7495a18344cd1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 21 Jun 2016 00:08:22 -0400 Subject: [PATCH] Added device identifier, APIs for updating token by identifier, Device creation/update upon signin. --- src/Api/Controllers/AuthController.cs | 2 +- src/Api/Controllers/DevicesController.cs | 28 +++++++++++++++++++ .../Models/Request/AuthTokenRequestModel.cs | 1 + ...RequestModel.cs => DeviceRequestModels.cs} | 16 +++++++++++ .../Models/Response/DeviceResponseModel.cs | 2 ++ src/Core/Domains/Device.cs | 1 + src/Core/Identity/JwtBearerSignInManager.cs | 25 +++++++++++++---- src/Core/Repositories/IDeviceRepository.cs | 1 + .../SqlServer/DeviceRepository.cs | 17 +++++++++++ src/Sql/Sql.sqlproj | 1 + .../Device_ReadByIdentifierUserId.sql | 15 ++++++++++ src/Sql/dbo/Tables/Device.sql | 6 ++++ 12 files changed, 109 insertions(+), 6 deletions(-) rename src/Api/Models/Request/{DeviceRequestModel.cs => DeviceRequestModels.cs} (66%) create mode 100644 src/Sql/dbo/Stored Procedures/Device_ReadByIdentifierUserId.sql diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index c1bc14c6df..9b6dbeb596 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -27,7 +27,7 @@ namespace Bit.Api.Controllers [AllowAnonymous] public async Task PostToken([FromBody]AuthTokenRequestModel model) { - var result = await _signInManager.PasswordSignInAsync(model.Email.ToLower(), model.MasterPasswordHash); + var result = await _signInManager.PasswordSignInAsync(model.Email.ToLower(), model.MasterPasswordHash, model.Device?.ToDevice()); if(result == JwtBearerSignInResult.Success) { return new AuthTokenResponseModel(result.Token, result.User); diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index 8e41d83d7a..0b22a2991f 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -40,6 +40,19 @@ namespace Bit.Api.Controllers return response; } + [HttpGet("identifier/{identifier}")] + public async Task GetByIdentifier(string identifier) + { + var device = await _deviceRepository.GetByIdentifierAsync(identifier, new Guid(_userManager.GetUserId(User))); + if(device == null) + { + throw new NotFoundException(); + } + + var response = new DeviceResponseModel(device); + return response; + } + [HttpGet("")] public async Task> Get() { @@ -73,6 +86,21 @@ namespace Bit.Api.Controllers return response; } + [HttpPut("identifier/{identifier}/token")] + public async Task PutToken(string identifier, [FromBody]DeviceTokenRequestModel model) + { + var device = await _deviceRepository.GetByIdentifierAsync(identifier, new Guid(_userManager.GetUserId(User))); + if(device == null) + { + throw new NotFoundException(); + } + + await _deviceRepository.ReplaceAsync(model.ToDevice(device)); + + var response = new DeviceResponseModel(device); + return response; + } + [HttpDelete("{id}")] public async Task Delete(string id) { diff --git a/src/Api/Models/Request/AuthTokenRequestModel.cs b/src/Api/Models/Request/AuthTokenRequestModel.cs index 1ccd66c31f..6ac5804cb8 100644 --- a/src/Api/Models/Request/AuthTokenRequestModel.cs +++ b/src/Api/Models/Request/AuthTokenRequestModel.cs @@ -10,5 +10,6 @@ namespace Bit.Api.Models public string Email { get; set; } [Required] public string MasterPasswordHash { get; set; } + public DeviceRequestModel Device { get; set; } } } diff --git a/src/Api/Models/Request/DeviceRequestModel.cs b/src/Api/Models/Request/DeviceRequestModels.cs similarity index 66% rename from src/Api/Models/Request/DeviceRequestModel.cs rename to src/Api/Models/Request/DeviceRequestModels.cs index af406d8a40..a836e110c4 100644 --- a/src/Api/Models/Request/DeviceRequestModel.cs +++ b/src/Api/Models/Request/DeviceRequestModels.cs @@ -13,6 +13,9 @@ namespace Bit.Api.Models [Required] [StringLength(50)] public string Name { get; set; } + [Required] + [StringLength(50)] + public string Identifier { get; set; } [StringLength(255)] public string PushToken { get; set; } @@ -27,10 +30,23 @@ namespace Bit.Api.Models public Device ToDevice(Device existingDevice) { existingDevice.Name = Name; + existingDevice.Identifier = Identifier; existingDevice.PushToken = PushToken; existingDevice.Type = Type.Value; return existingDevice; } } + + public class DeviceTokenRequestModel + { + [StringLength(255)] + public string PushToken { get; set; } + + public Device ToDevice(Device existingDevice) + { + existingDevice.PushToken = PushToken; + return existingDevice; + } + } } diff --git a/src/Api/Models/Response/DeviceResponseModel.cs b/src/Api/Models/Response/DeviceResponseModel.cs index 2a0c4b54ce..690c06d80f 100644 --- a/src/Api/Models/Response/DeviceResponseModel.cs +++ b/src/Api/Models/Response/DeviceResponseModel.cs @@ -17,12 +17,14 @@ namespace Bit.Api.Models Id = device.Id.ToString(); Name = device.Name; Type = device.Type; + Identifier = device.Identifier; CreationDate = device.CreationDate; } public string Id { get; set; } public string Name { get; set; } public DeviceType Type { get; set; } + public string Identifier { get; set; } public DateTime CreationDate { get; set; } } } diff --git a/src/Core/Domains/Device.cs b/src/Core/Domains/Device.cs index 62d3e86199..8376b35d02 100644 --- a/src/Core/Domains/Device.cs +++ b/src/Core/Domains/Device.cs @@ -9,6 +9,7 @@ namespace Bit.Core.Domains public Guid UserId { get; set; } public string Name { get; set; } public Enums.DeviceType Type { get; set; } + public string Identifier { get; set; } public string PushToken { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Identity/JwtBearerSignInManager.cs b/src/Core/Identity/JwtBearerSignInManager.cs index 400362f413..dc12dc66c9 100644 --- a/src/Core/Identity/JwtBearerSignInManager.cs +++ b/src/Core/Identity/JwtBearerSignInManager.cs @@ -9,11 +9,14 @@ using Microsoft.Extensions.Options; using Bit.Core.Domains; using Microsoft.AspNetCore.Builder; using Microsoft.IdentityModel.Tokens; +using Bit.Core.Repositories; namespace Bit.Core.Identity { public class JwtBearerSignInManager { + private readonly IDeviceRepository _deviceRepository; + public JwtBearerSignInManager( UserManager userManager, IHttpContextAccessor contextAccessor, @@ -21,7 +24,8 @@ namespace Bit.Core.Identity IOptions optionsAccessor, IOptions jwtIdentityOptionsAccessor, IOptions jwtOptionsAccessor, - ILogger logger) + ILogger logger, + IDeviceRepository deviceRepository) { UserManager = userManager; Context = contextAccessor.HttpContext; @@ -29,6 +33,7 @@ namespace Bit.Core.Identity IdentityOptions = optionsAccessor?.Value ?? new IdentityOptions(); JwtIdentityOptions = jwtIdentityOptionsAccessor?.Value ?? new JwtBearerIdentityOptions(); JwtBearerOptions = jwtOptionsAccessor?.Value ?? new JwtBearerOptions(); + _deviceRepository = deviceRepository; } internal UserManager UserManager { get; set; } @@ -54,7 +59,7 @@ namespace Bit.Core.Identity return Task.FromResult(false); } - public async Task PasswordSignInAsync(User user, string password) + public async Task PasswordSignInAsync(User user, string password, Device device = null) { if(user == null) { @@ -63,13 +68,23 @@ namespace Bit.Core.Identity if(await UserManager.CheckPasswordAsync(user, password)) { - return await SignInOrTwoFactorAsync(user); + var result = await SignInOrTwoFactorAsync(user); + if(result.Succeeded && device != null) + { + var existingDevice = await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id); + if(existingDevice == null) + { + await _deviceRepository.CreateAsync(device); + } + } + + return result; } return JwtBearerSignInResult.Failed; } - public async Task PasswordSignInAsync(string userName, string password) + public async Task PasswordSignInAsync(string userName, string password, Device device = null) { var user = await UserManager.FindByNameAsync(userName); if(user == null) @@ -77,7 +92,7 @@ namespace Bit.Core.Identity return JwtBearerSignInResult.Failed; } - return await PasswordSignInAsync(user, password); + return await PasswordSignInAsync(user, password, device); } public async Task TwoFactorSignInAsync(User user, string provider, string code) diff --git a/src/Core/Repositories/IDeviceRepository.cs b/src/Core/Repositories/IDeviceRepository.cs index acd14c4644..0f379be0d6 100644 --- a/src/Core/Repositories/IDeviceRepository.cs +++ b/src/Core/Repositories/IDeviceRepository.cs @@ -8,6 +8,7 @@ namespace Bit.Core.Repositories public interface IDeviceRepository : IRepository { Task GetByIdAsync(Guid id, Guid userId); + Task GetByIdentifierAsync(string identifier, Guid userId); Task> GetManyByUserIdAsync(Guid userId); } } diff --git a/src/Core/Repositories/SqlServer/DeviceRepository.cs b/src/Core/Repositories/SqlServer/DeviceRepository.cs index 2ae9096c65..ea3263035c 100644 --- a/src/Core/Repositories/SqlServer/DeviceRepository.cs +++ b/src/Core/Repositories/SqlServer/DeviceRepository.cs @@ -30,6 +30,23 @@ namespace Bit.Core.Repositories.SqlServer return device; } + public async Task GetByIdentifierAsync(string identifier, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadByIdentifierUserId]", + new + { + UserId = userId, + Identifier = identifier + }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + } + public async Task> GetManyByUserIdAsync(Guid userId) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index d8e7ea9b58..bfd0b0a53d 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -90,6 +90,7 @@ + diff --git a/src/Sql/dbo/Stored Procedures/Device_ReadByIdentifierUserId.sql b/src/Sql/dbo/Stored Procedures/Device_ReadByIdentifierUserId.sql new file mode 100644 index 0000000000..00e38687e7 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Device_ReadByIdentifierUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[Device_ReadByIdentifierUserId] + @UserId UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[DeviceView] + WHERE + [UserId] = @UserId + AND [Identifier] = @Identifier +END diff --git a/src/Sql/dbo/Tables/Device.sql b/src/Sql/dbo/Tables/Device.sql index 4899ec025e..d3c60d8c74 100644 --- a/src/Sql/dbo/Tables/Device.sql +++ b/src/Sql/dbo/Tables/Device.sql @@ -3,6 +3,7 @@ [UserId] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (50) NOT NULL, [Type] SMALLINT NOT NULL, + [Identifier] NVARCHAR (50) NOT NULL, [PushToken] NVARCHAR (255) NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, @@ -15,3 +16,8 @@ GO CREATE NONCLUSTERED INDEX [IX_Device_UserId] ON [dbo].[Device]([UserId] ASC); + +GO +CREATE UNIQUE NONCLUSTERED INDEX [UX_Device_UserId_Identifier] + ON [dbo].[Device]([UserId] ASC, [Identifier] ASC); +