From 1850c23e60ef7a133e50b5413f601d8b81d7a9b1 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 14 Aug 2020 23:56:30 +0200 Subject: [PATCH 01/20] WIP --- src/Api/Controllers/TwoFactorController.cs | 38 ++++++++++++++++++- src/Api/Properties/serviceDependencies.json | 7 ++++ .../Properties/serviceDependencies.local.json | 7 ++++ src/Api/Startup.cs | 9 +++++ src/Core/Core.csproj | 1 + .../Properties/serviceDependencies.json | 7 ++++ .../Properties/serviceDependencies.local.json | 7 ++++ 7 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/Api/Properties/serviceDependencies.json create mode 100644 src/Api/Properties/serviceDependencies.local.json create mode 100644 src/Identity/Properties/serviceDependencies.json create mode 100644 src/Identity/Properties/serviceDependencies.local.json diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index e82a86d79d72..dd30732462e4 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -13,6 +13,9 @@ using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Core.Utilities.Duo; +using Fido2NetLib; +using System.Collections.Generic; +using Fido2NetLib.Objects; namespace Bit.Api.Controllers { @@ -26,6 +29,7 @@ public class TwoFactorController : Controller private readonly GlobalSettings _globalSettings; private readonly UserManager _userManager; private readonly CurrentContext _currentContext; + private readonly IFido2 _fido2; public TwoFactorController( IUserService userService, @@ -33,7 +37,8 @@ public class TwoFactorController : Controller IOrganizationService organizationService, GlobalSettings globalSettings, UserManager userManager, - CurrentContext currentContext) + CurrentContext currentContext, + IFido2 fido2) { _userService = userService; _organizationRepository = organizationRepository; @@ -41,6 +46,7 @@ public class TwoFactorController : Controller _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; + _fido2 = fido2; } [HttpGet("")] @@ -218,6 +224,36 @@ public async Task PutDuo([FromBody]UpdateTwoFactorDuo return response; } + [HttpPost("get-webauthn-challenge")] + public async Task GetWebAuthnChallenge([FromBody] TwoFactorRequestModel model) + { + var user = await CheckAsync(model.MasterPasswordHash, true); + var fidoUser = new Fido2User + { + DisplayName = user.Name, + Name = user.Email, + Id = user.Id.ToByteArray(), + }; + + var options = _fido2.RequestNewCredential(fidoUser, new List(), AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); + return options; + } + + [HttpPut("webauthn")] + [HttpPost("webauthn")] + public async Task PutWebAuthn([FromBody] TwoFactorU2fRequestModel model) + { + var user = await CheckAsync(model.MasterPasswordHash, true); + var success = await _userService.CompleteU2fRegistrationAsync( + user, model.Id.Value, model.Name, model.DeviceResponse); + if (!success) + { + throw new BadRequestException("Unable to complete WebAuthn key registration."); + } + var response = new TwoFactorU2fResponseModel(user); + return response; + } + [HttpPost("get-u2f")] public async Task GetU2f([FromBody]TwoFactorRequestModel model) { diff --git a/src/Api/Properties/serviceDependencies.json b/src/Api/Properties/serviceDependencies.json new file mode 100644 index 000000000000..a4e7aa3d33c5 --- /dev/null +++ b/src/Api/Properties/serviceDependencies.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets" + } + } +} \ No newline at end of file diff --git a/src/Api/Properties/serviceDependencies.local.json b/src/Api/Properties/serviceDependencies.local.json new file mode 100644 index 000000000000..09b109bc6fe1 --- /dev/null +++ b/src/Api/Properties/serviceDependencies.local.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets.user" + } + } +} \ No newline at end of file diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index b1978c29288d..48290a93bcc0 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -111,6 +111,15 @@ public void ConfigureServices(IServiceCollection services) services.AddBaseServices(); services.AddDefaultServices(globalSettings); + // Fido2 + services.AddFido2(options => + { + options.ServerDomain = "localhost"; + options.ServerName = "Bitwarden"; + options.Origin = "https://localhost:4000"; + options.TimestampDriftTolerance = 300000; + }); + // MVC services.AddMvc(config => { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index dd12b072602c..a6f4d498a576 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Identity/Properties/serviceDependencies.json b/src/Identity/Properties/serviceDependencies.json new file mode 100644 index 000000000000..a4e7aa3d33c5 --- /dev/null +++ b/src/Identity/Properties/serviceDependencies.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets" + } + } +} \ No newline at end of file diff --git a/src/Identity/Properties/serviceDependencies.local.json b/src/Identity/Properties/serviceDependencies.local.json new file mode 100644 index 000000000000..09b109bc6fe1 --- /dev/null +++ b/src/Identity/Properties/serviceDependencies.local.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets.user" + } + } +} \ No newline at end of file From a6a3a7efe9b58bd774b8a4a5a74fb43921fe0e93 Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 19 Aug 2020 22:49:29 +0200 Subject: [PATCH 02/20] Registration mostly done --- src/Api/Controllers/TwoFactorController.cs | 69 +++++++++++--- src/Api/Startup.cs | 2 +- src/Core/Enums/TwoFactorProviderType.cs | 3 +- .../Api/Request/TwoFactorRequestModels.cs | 8 ++ .../TwoFactor/TwoFactorU2fResponseModel.cs | 1 + .../TwoFactorWebAuthnResponseModel.cs | 45 +++++++++ src/Core/Models/TwoFactorProvider.cs | 22 +++++ src/Core/Services/IUserService.cs | 3 + .../Services/Implementations/UserService.cs | 94 +++++++++++++++++++ src/Identity/Startup.cs | 9 ++ 10 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index dd30732462e4..2ba42a7daf51 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -29,7 +29,6 @@ public class TwoFactorController : Controller private readonly GlobalSettings _globalSettings; private readonly UserManager _userManager; private readonly CurrentContext _currentContext; - private readonly IFido2 _fido2; public TwoFactorController( IUserService userService, @@ -37,8 +36,7 @@ public class TwoFactorController : Controller IOrganizationService organizationService, GlobalSettings globalSettings, UserManager userManager, - CurrentContext currentContext, - IFido2 fido2) + CurrentContext currentContext) { _userService = userService; _organizationRepository = organizationRepository; @@ -46,7 +44,6 @@ public class TwoFactorController : Controller _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; - _fido2 = fido2; } [HttpGet("")] @@ -228,28 +225,72 @@ public async Task PutDuo([FromBody]UpdateTwoFactorDuo public async Task GetWebAuthnChallenge([FromBody] TwoFactorRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); - var fidoUser = new Fido2User - { - DisplayName = user.Name, - Name = user.Email, - Id = user.Id.ToByteArray(), - }; - - var options = _fido2.RequestNewCredential(fidoUser, new List(), AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); - return options; + var reg = await _userService.StartWebAuthnRegistrationAsync(user); + return reg; } [HttpPut("webauthn")] [HttpPost("webauthn")] - public async Task PutWebAuthn([FromBody] TwoFactorU2fRequestModel model) + public async Task PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); + + var success = await _userService.CompleteWebAuthRegistrationAsync( + user, model.Id.Value, model.Name, model.DeviceResponse); + if (!success) + { + throw new BadRequestException("Unable to complete U2F key registration."); + } + + /* + * try + { + // 1. get the options we sent the client + var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); + var options = CredentialCreateOptions.FromJson(jsonOptions); + + // 2. Create callback so that lib can verify credential id is unique to this user + IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) => + { + var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId); + if (users.Count > 0) + return false; + + return true; + }; + + // 2. Verify and make the credentials + var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); + + // 3. Store the credentials in db + DemoStorage.AddCredentialToUser(options.User, new StoredCredential + { + Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), + PublicKey = success.Result.PublicKey, + UserHandle = success.Result.User.Id, + SignatureCounter = success.Result.Counter, + CredType = success.Result.CredType, + RegDate = DateTime.Now, + AaGuid = success.Result.Aaguid + }); + + // 4. return "ok" to the client + return Json(success); + } + catch (Exception e) + { + return Json(new CredentialMakeResult { Status = "error", ErrorMessage = FormatException(e) }); + } + */ + + /* var success = await _userService.CompleteU2fRegistrationAsync( user, model.Id.Value, model.Name, model.DeviceResponse); if (!success) { throw new BadRequestException("Unable to complete WebAuthn key registration."); } + */ var response = new TwoFactorU2fResponseModel(user); return response; } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 48290a93bcc0..a2b59f277c83 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -116,7 +116,7 @@ public void ConfigureServices(IServiceCollection services) { options.ServerDomain = "localhost"; options.ServerName = "Bitwarden"; - options.Origin = "https://localhost:4000"; + options.Origin = "https://localhost:8080"; options.TimestampDriftTolerance = 300000; }); diff --git a/src/Core/Enums/TwoFactorProviderType.cs b/src/Core/Enums/TwoFactorProviderType.cs index 1a1b7ffc94e5..0d77b92284bc 100644 --- a/src/Core/Enums/TwoFactorProviderType.cs +++ b/src/Core/Enums/TwoFactorProviderType.cs @@ -8,6 +8,7 @@ public enum TwoFactorProviderType : byte YubiKey = 3, U2f = 4, Remember = 5, - OrganizationDuo = 6 + OrganizationDuo = 6, + WebAuthn = 7 } } diff --git a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs index 5cd27a36f34c..c8748bd84da2 100644 --- a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs +++ b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs @@ -1,5 +1,6 @@ using Bit.Core.Enums; using Bit.Core.Models.Table; +using Fido2NetLib; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -223,6 +224,13 @@ public User ToUser(User extistingUser) } } + public class TwoFactorWebAuthnRequestModel : TwoFactorU2fDeleteRequestModel + { + [Required] + public AuthenticatorAttestationRawResponse DeviceResponse { get; set; } + public string Name { get; set; } + } + public class TwoFactorU2fRequestModel : TwoFactorU2fDeleteRequestModel { [Required] diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs index 121be3d4be47..5e25ef6e235b 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs @@ -17,6 +17,7 @@ public TwoFactorU2fResponseModel(User user) throw new ArgumentNullException(nameof(user)); } + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); Enabled = provider?.Enabled ?? false; Keys = provider?.MetaData?.Select(k => new KeyModel(k.Key, diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs new file mode 100644 index 000000000000..ad0f9be1d2e6 --- /dev/null +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs @@ -0,0 +1,45 @@ +using System; +using Bit.Core.Models.Table; +using Bit.Core.Models.Business; +using Bit.Core.Enums; +using System.Collections.Generic; +using System.Linq; +using Fido2NetLib; + +namespace Bit.Core.Models.Api +{ + public class TwoFactorWebAuthnResponseModel : ResponseModel + { + public TwoFactorWebAuthnResponseModel(User user) + : base("twoFactorU2f") + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + Enabled = provider?.Enabled ?? false; + Keys = provider?.MetaData?.Select(k => new KeyModel(k.Key, + new TwoFactorProvider.U2fMetaData((dynamic)k.Value))); + } + + public bool Enabled { get; set; } + public IEnumerable Keys { get; set; } + + public class KeyModel + { + public KeyModel(string id, TwoFactorProvider.U2fMetaData data) + { + Name = data.Name; + Id = Convert.ToInt32(id.Replace("Key", string.Empty)); + Compromised = data.Compromised; + } + + public string Name { get; set; } + public int Id { get; set; } + public bool Compromised { get; set; } + } + } +} diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 8d51a8c02c44..b903d3a486e6 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -1,5 +1,8 @@ using Bit.Core.Enums; +using Fido2NetLib.Objects; +using Microsoft.Extensions.Options; using Newtonsoft.Json; +using System; using System.Collections.Generic; using U2F.Core.Utils; @@ -41,6 +44,25 @@ public U2fMetaData(dynamic o) public bool Compromised { get; set; } } + public class WebAuthnData + { + public WebAuthnData() { } + + public WebAuthnData(dynamic o) + { + Options = o.Options; + } + + public string Options { get; set; } + public PublicKeyCredentialDescriptor Descriptor { get; internal set; } + public byte[] PublicKey { get; internal set; } + public byte[] UserHandle { get; internal set; } + public uint SignatureCounter { get; internal set; } + public string CredType { get; internal set; } + public DateTime RegDate { get; internal set; } + public Guid AaGuid { get; internal set; } + } + public static bool RequiresPremium(TwoFactorProviderType type) { switch (type) diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 86417bfbdcf4..6d0e8096b214 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -7,6 +7,7 @@ using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Business; +using Fido2NetLib; namespace Bit.Core.Services { @@ -23,7 +24,9 @@ public interface IUserService Task SendMasterPasswordHintAsync(string email); Task SendTwoFactorEmailAsync(User user); Task VerifyTwoFactorEmailAsync(User user, string token); + Task StartWebAuthnRegistrationAsync(User user); Task StartU2fRegistrationAsync(User user); + Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); Task DeleteU2fKeyAsync(User user, int id); Task CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse); Task SendEmailVerificationAsync(User user); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 21844ccd1938..1b7b64483c5a 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -20,6 +20,8 @@ using Newtonsoft.Json; using Microsoft.AspNetCore.DataProtection; using U2F.Core.Exceptions; +using Fido2NetLib; +using Fido2NetLib.Objects; namespace Bit.Core.Services { @@ -46,6 +48,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IPolicyRepository _policyRepository; private readonly IDataProtector _organizationServiceDataProtector; private readonly IReferenceEventService _referenceEventService; + private readonly IFido2 _fido2; private readonly CurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -73,6 +76,7 @@ public class UserService : UserManager, IUserService, IDisposable IPaymentService paymentService, IPolicyRepository policyRepository, IReferenceEventService referenceEventService, + IFido2 fido2, CurrentContext currentContext, GlobalSettings globalSettings) : base( @@ -105,6 +109,7 @@ public class UserService : UserManager, IUserService, IDisposable _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); _referenceEventService = referenceEventService; + _fido2 = fido2; _currentContext = currentContext; _globalSettings = globalSettings; } @@ -356,6 +361,95 @@ public async Task VerifyTwoFactorEmailAsync(User user, string token) "2faEmail:" + email, token); } + public async Task StartWebAuthnRegistrationAsync(User user) + { + await _u2fRepository.DeleteManyByUserIdAsync(user.Id); + + var fidoUser = new Fido2User + { + DisplayName = user.Name, + Name = user.Email, + Id = user.Id.ToByteArray(), + }; + + // TODO: We need to get the existing keys to exclude them from generation + + var options = _fido2.RequestNewCredential(fidoUser, new List(), AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); + + var providers = user.GetTwoFactorProviders(); + if (providers == null) + { + providers = new Dictionary(); + } + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + if (provider == null) + { + provider = new TwoFactorProvider + { + Enabled = false + }; + } + if (provider.MetaData == null) + { + provider.MetaData = new Dictionary(); + } + provider.MetaData.Add("1", new TwoFactorProvider.WebAuthnData + { + Options = options.ToJson() + }); + + + if (providers.ContainsKey(TwoFactorProviderType.WebAuthn)) + { + providers.Remove(TwoFactorProviderType.WebAuthn); + } + + providers.Add(TwoFactorProviderType.WebAuthn, provider); + user.SetTwoFactorProviders(providers); + await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + + return options; + } + + public async Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + if (!provider?.MetaData?.ContainsKey("1") ?? true) + { + return false; + } + + var providerData = new TwoFactorProvider.WebAuthnData(provider.MetaData["1"]); + var options = CredentialCreateOptions.FromJson(providerData.Options); + + // Callback to ensure credential id is unique + IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) => + { + // TODO: Check other keys + return true; + }; + + var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); + + providerData.Options = ""; + providerData.Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId); + providerData.PublicKey = success.Result.PublicKey; + providerData.UserHandle = success.Result.User.Id; + providerData.SignatureCounter = success.Result.Counter; + providerData.CredType = success.Result.CredType; + providerData.RegDate = DateTime.Now; + providerData.AaGuid = success.Result.Aaguid; + provider.MetaData["1"] = providerData; + + var providers = user.GetTwoFactorProviders(); + providers.Remove(TwoFactorProviderType.WebAuthn); + providers.Add(TwoFactorProviderType.WebAuthn, provider); + user.SetTwoFactorProviders(providers); + await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + + return true; + } + public async Task StartU2fRegistrationAsync(User user) { await _u2fRepository.DeleteManyByUserIdAsync(user.Id); diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 119a0ac402e5..569d7b674bdd 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -55,6 +55,15 @@ public void ConfigureServices(IServiceCollection services) // Caching services.AddMemoryCache(); + // Fido2 + services.AddFido2(options => + { + options.ServerDomain = "localhost"; + options.ServerName = "Bitwarden"; + options.Origin = "https://localhost:4000"; + options.TimestampDriftTolerance = 300000; + }); + // Mvc services.AddMvc(); From abd14685a275af06edb67321e8bd7ba887051446 Mon Sep 17 00:00:00 2001 From: Hinton Date: Sun, 23 Aug 2020 23:19:05 +0200 Subject: [PATCH 03/20] Basic registration and login implemented --- src/Api/Controllers/TwoFactorController.cs | 59 +----- src/Api/Properties/serviceDependencies.json | 7 - .../Properties/serviceDependencies.local.json | 7 - src/Api/Startup.cs | 4 +- src/Core/Enums/TwoFactorProviderType.cs | 2 +- src/Core/Identity/WebAuthnTokenProvider.cs | 172 ++++++++++++++++++ .../IdentityServer/BaseRequestValidator.cs | 7 + .../TwoFactor/TwoFactorU2fResponseModel.cs | 1 - .../TwoFactorWebAuthnResponseModel.cs | 11 +- src/Core/Models/TwoFactorProvider.cs | 9 + src/Core/Services/IUserService.cs | 2 +- .../Services/Implementations/UserService.cs | 29 ++- src/Core/Utilities/CoreHelpers.cs | 1 + .../Utilities/ServiceCollectionExtensions.cs | 4 +- .../Properties/serviceDependencies.json | 7 - .../Properties/serviceDependencies.local.json | 7 - src/Identity/Startup.cs | 4 +- 17 files changed, 231 insertions(+), 102 deletions(-) delete mode 100644 src/Api/Properties/serviceDependencies.json delete mode 100644 src/Api/Properties/serviceDependencies.local.json create mode 100644 src/Core/Identity/WebAuthnTokenProvider.cs delete mode 100644 src/Identity/Properties/serviceDependencies.json delete mode 100644 src/Identity/Properties/serviceDependencies.local.json diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index 2ba42a7daf51..5b7a7509f64f 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -221,6 +221,14 @@ public async Task PutDuo([FromBody]UpdateTwoFactorDuo return response; } + [HttpPost("get-webauthn")] + public async Task GetWebAuthn([FromBody] TwoFactorRequestModel model) + { + var user = await CheckAsync(model.MasterPasswordHash, true); + var response = new TwoFactorWebAuthnResponseModel(user); + return response; + } + [HttpPost("get-webauthn-challenge")] public async Task GetWebAuthnChallenge([FromBody] TwoFactorRequestModel model) { @@ -239,58 +247,9 @@ public async Task PutWebAuthn([FromBody] TwoFactorWeb user, model.Id.Value, model.Name, model.DeviceResponse); if (!success) { - throw new BadRequestException("Unable to complete U2F key registration."); - } - - /* - * try - { - // 1. get the options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); - var options = CredentialCreateOptions.FromJson(jsonOptions); - - // 2. Create callback so that lib can verify credential id is unique to this user - IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) => - { - var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId); - if (users.Count > 0) - return false; - - return true; - }; - - // 2. Verify and make the credentials - var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); - - // 3. Store the credentials in db - DemoStorage.AddCredentialToUser(options.User, new StoredCredential - { - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), - PublicKey = success.Result.PublicKey, - UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter, - CredType = success.Result.CredType, - RegDate = DateTime.Now, - AaGuid = success.Result.Aaguid - }); - - // 4. return "ok" to the client - return Json(success); - } - catch (Exception e) - { - return Json(new CredentialMakeResult { Status = "error", ErrorMessage = FormatException(e) }); + throw new BadRequestException("Unable to complete WebAuthn registration."); } - */ - /* - var success = await _userService.CompleteU2fRegistrationAsync( - user, model.Id.Value, model.Name, model.DeviceResponse); - if (!success) - { - throw new BadRequestException("Unable to complete WebAuthn key registration."); - } - */ var response = new TwoFactorU2fResponseModel(user); return response; } diff --git a/src/Api/Properties/serviceDependencies.json b/src/Api/Properties/serviceDependencies.json deleted file mode 100644 index a4e7aa3d33c5..000000000000 --- a/src/Api/Properties/serviceDependencies.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets" - } - } -} \ No newline at end of file diff --git a/src/Api/Properties/serviceDependencies.local.json b/src/Api/Properties/serviceDependencies.local.json deleted file mode 100644 index 09b109bc6fe1..000000000000 --- a/src/Api/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets.user" - } - } -} \ No newline at end of file diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index a2b59f277c83..caabbc0a5844 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -114,9 +114,9 @@ public void ConfigureServices(IServiceCollection services) // Fido2 services.AddFido2(options => { - options.ServerDomain = "localhost"; + options.ServerDomain = "vault.bitwarden2.com"; options.ServerName = "Bitwarden"; - options.Origin = "https://localhost:8080"; + options.Origin = "https://vault.bitwarden2.com:8080"; options.TimestampDriftTolerance = 300000; }); diff --git a/src/Core/Enums/TwoFactorProviderType.cs b/src/Core/Enums/TwoFactorProviderType.cs index 0d77b92284bc..d3d0c195ae9b 100644 --- a/src/Core/Enums/TwoFactorProviderType.cs +++ b/src/Core/Enums/TwoFactorProviderType.cs @@ -9,6 +9,6 @@ public enum TwoFactorProviderType : byte U2f = 4, Remember = 5, OrganizationDuo = 6, - WebAuthn = 7 + WebAuthn = 7, } } diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs new file mode 100644 index 000000000000..2230b1afea9c --- /dev/null +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -0,0 +1,172 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Bit.Core.Models.Table; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.Repositories; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using U2fLib = U2F.Core.Crypto.U2F; +using U2F.Core.Models; +using U2F.Core.Exceptions; +using U2F.Core.Utils; +using System; +using Bit.Core.Services; +using Microsoft.Extensions.DependencyInjection; +using Fido2NetLib.Objects; +using Fido2NetLib; + +namespace Bit.Core.Identity +{ + public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider + { + private readonly IServiceProvider _serviceProvider; + private readonly IU2fRepository _u2fRepository; + private readonly IFido2 _fido2; + private readonly IUserService _userService; + private readonly GlobalSettings _globalSettings; + + public WebAuthnTokenProvider( + IServiceProvider serviceProvider, + IU2fRepository u2fRepository, + IFido2 fido2, + GlobalSettings globalSettings) + { + _serviceProvider = serviceProvider; + _u2fRepository = u2fRepository; + _fido2 = fido2; + _globalSettings = globalSettings; + } + + public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + { + var userService = _serviceProvider.GetRequiredService(); + if (!(await userService.CanAccessPremium(user))) + { + return false; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + if (!HasProperMetaData(provider)) + { + return false; + } + + return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user); + } + + public async Task GenerateAsync(string purpose, UserManager manager, User user) + { + var userService = _serviceProvider.GetRequiredService(); + if (!(await userService.CanAccessPremium(user))) + { + return null; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + var keys = LoadKeys(provider); + if (keys.Count == 0) + { + return null; + } + + var existingCredentials = LoadKeys(provider).Select(key => key.Item2.Descriptor).ToList(); + + var exts = new AuthenticationExtensionsClientInputs() + { + SimpleTransactionAuthorization = "FIDO", + GenericTransactionAuthorization = new TxAuthGenericArg + { + ContentType = "text/plain", + Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } + }, + UserVerificationIndex = true, + UserVerificationMethod = true, + }; + + var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred, exts); + + provider.MetaData.Remove("login"); + provider.MetaData.Add("login", options); + + var providers = user.GetTwoFactorProviders(); + providers.Remove(TwoFactorProviderType.WebAuthn); + providers.Add(TwoFactorProviderType.WebAuthn, provider); + user.SetTwoFactorProviders(providers); + await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + + return options.ToJson(); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) + { + var userService = _serviceProvider.GetRequiredService(); + if (!(await userService.CanAccessPremium(user)) || string.IsNullOrWhiteSpace(token)) + { + return false; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + var keys = LoadKeys(provider); + if (keys.Count == 0) + { + return false; + } + + if (!provider.MetaData.ContainsKey("login")) + { + return false; + } + + var clientResponse = JsonConvert.DeserializeObject(token); + + var jsonOptions = provider.MetaData["login"].ToString(); + var options = AssertionOptions.FromJson(jsonOptions); + + var creds = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)).Item2; + + // 3. Get credential counter from database + var storedCounter = creds.SignatureCounter; + + // 4. Create callback to check if userhandle owns the credentialId + IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => + { + return true; + }; + + // 5. Make the assertion + var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback); + + return res.Status == "ok"; + } + + private bool HasProperMetaData(TwoFactorProvider provider) + { + return (provider?.MetaData?.Count ?? 0) > 0; + } + + private List> LoadKeys(TwoFactorProvider provider) + { + var keys = new List>(); + if (!HasProperMetaData(provider)) + { + return keys; + } + + // Support up to 5 keys + for (var i = 1; i <= 5; i++) + { + var keyName = $"Key{i}"; + if (provider.MetaData.ContainsKey(keyName)) + { + var key = new TwoFactorProvider.WebAuthnData((dynamic)provider.MetaData[keyName]); + + keys.Add(new Tuple(keyName, key)); + } + } + + return keys; + } + } +} diff --git a/src/Core/IdentityServer/BaseRequestValidator.cs b/src/Core/IdentityServer/BaseRequestValidator.cs index e67993d7cd72..12a85de55a03 100644 --- a/src/Core/IdentityServer/BaseRequestValidator.cs +++ b/src/Core/IdentityServer/BaseRequestValidator.cs @@ -17,6 +17,7 @@ using System.Reflection; using Microsoft.Extensions.Logging; using Bit.Core.Models.Api; +using System.Text.Json; namespace Bit.Core.IdentityServer { @@ -297,6 +298,7 @@ private Device GetDeviceFromRequest(ValidatedRequest request) case TwoFactorProviderType.Duo: case TwoFactorProviderType.YubiKey: case TwoFactorProviderType.U2f: + case TwoFactorProviderType.WebAuthn: case TwoFactorProviderType.Remember: if (type != TwoFactorProviderType.Remember && !(await _userService.TwoFactorProviderIsEnabledAsync(type, user))) @@ -324,6 +326,7 @@ private Device GetDeviceFromRequest(ValidatedRequest request) { case TwoFactorProviderType.Duo: case TwoFactorProviderType.U2f: + case TwoFactorProviderType.WebAuthn: case TwoFactorProviderType.Email: case TwoFactorProviderType.YubiKey: if (!(await _userService.TwoFactorProviderIsEnabledAsync(type, user))) @@ -351,6 +354,10 @@ private Device GetDeviceFromRequest(ValidatedRequest request) ["Challenges"] = tokens != null && tokens.Length > 1 ? tokens[1] : null }; } + else if (type == TwoFactorProviderType.WebAuthn) + { + return JsonSerializer.Deserialize>(token); + } else if (type == TwoFactorProviderType.Email) { return new Dictionary diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs index 5e25ef6e235b..121be3d4be47 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs @@ -17,7 +17,6 @@ public TwoFactorU2fResponseModel(User user) throw new ArgumentNullException(nameof(user)); } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); Enabled = provider?.Enabled ?? false; Keys = provider?.MetaData?.Select(k => new KeyModel(k.Key, diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs index ad0f9be1d2e6..e90fc0245d8b 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs @@ -19,10 +19,11 @@ public TwoFactorWebAuthnResponseModel(User user) } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); Enabled = provider?.Enabled ?? false; - Keys = provider?.MetaData?.Select(k => new KeyModel(k.Key, - new TwoFactorProvider.U2fMetaData((dynamic)k.Value))); + Keys = provider?.MetaData? + .Where(k => k.Key.StartsWith("Key")) + .Select(k => new KeyModel(k.Key, new TwoFactorProvider.WebAuthnData((dynamic)k.Value))); } public bool Enabled { get; set; } @@ -30,16 +31,14 @@ public TwoFactorWebAuthnResponseModel(User user) public class KeyModel { - public KeyModel(string id, TwoFactorProvider.U2fMetaData data) + public KeyModel(string id, TwoFactorProvider.WebAuthnData data) { Name = data.Name; Id = Convert.ToInt32(id.Replace("Key", string.Empty)); - Compromised = data.Compromised; } public string Name { get; set; } public int Id { get; set; } - public bool Compromised { get; set; } } } } diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index b903d3a486e6..0b4e7d1d85ef 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -50,9 +50,18 @@ public class WebAuthnData public WebAuthnData(dynamic o) { + Name = o.Name; Options = o.Options; + Descriptor = JsonConvert.DeserializeObject(o.Descriptor.ToString()); + PublicKey = o.PublicKey; + UserHandle = o.UserHandle; + SignatureCounter = o.SignatureCounter; + CredType = o.CredType; + RegDate = o.RegDate; + AaGuid = o.AaGuid; } + public string Name { get; set; } public string Options { get; set; } public PublicKeyCredentialDescriptor Descriptor { get; internal set; } public byte[] PublicKey { get; internal set; } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 6d0e8096b214..9ce8b1a3d359 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -41,7 +41,7 @@ public interface IUserService Task UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, IEnumerable ciphers, IEnumerable folders); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); - Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type); + Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true); Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type, IOrganizationService organizationService); Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode, diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 1b7b64483c5a..519f7f53e5c1 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -393,7 +393,8 @@ public async Task StartWebAuthnRegistrationAsync(User u { provider.MetaData = new Dictionary(); } - provider.MetaData.Add("1", new TwoFactorProvider.WebAuthnData + provider.MetaData.Remove("pending"); + provider.MetaData.Add("pending", new TwoFactorProvider.WebAuthnData { Options = options.ToJson() }); @@ -406,20 +407,22 @@ public async Task StartWebAuthnRegistrationAsync(User u providers.Add(TwoFactorProviderType.WebAuthn, provider); user.SetTwoFactorProviders(providers); - await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false); return options; } - public async Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse) + public async Task CompleteWebAuthRegistrationAsync(User user, int id, string name, AuthenticatorAttestationRawResponse attestationResponse) { + var keyId = $"Key{id}"; + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); - if (!provider?.MetaData?.ContainsKey("1") ?? true) + if (!provider?.MetaData?.ContainsKey("pending") ?? true) { return false; } - var providerData = new TwoFactorProvider.WebAuthnData(provider.MetaData["1"]); + var providerData = new TwoFactorProvider.WebAuthnData(provider.MetaData["pending"]); var options = CredentialCreateOptions.FromJson(providerData.Options); // Callback to ensure credential id is unique @@ -432,6 +435,7 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int value, s var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); providerData.Options = ""; + providerData.Name = name; providerData.Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId); providerData.PublicKey = success.Result.PublicKey; providerData.UserHandle = success.Result.User.Id; @@ -439,7 +443,9 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int value, s providerData.CredType = success.Result.CredType; providerData.RegDate = DateTime.Now; providerData.AaGuid = success.Result.Aaguid; - provider.MetaData["1"] = providerData; + + provider.MetaData[keyId] = providerData; + provider.MetaData.Remove("pending"); var providers = user.GetTwoFactorProviders(); providers.Remove(TwoFactorProviderType.WebAuthn); @@ -785,9 +791,9 @@ public async Task RefreshSecurityStampAsync(User user, string ma return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } - public async Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type) + public async Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true) { - SetTwoFactorProvider(user, type); + SetTwoFactorProvider(user, type, setEnabled); await SaveUserAsync(user); await _eventService.LogUserEventAsync(user.Id, EventType.User_Updated2fa); } @@ -1230,7 +1236,7 @@ private async Task ValidatePasswordInternal(User user, string pa return IdentityResult.Success; } - public void SetTwoFactorProvider(User user, TwoFactorProviderType type) + public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true) { var providers = user.GetTwoFactorProviders(); if (!providers?.ContainsKey(type) ?? true) @@ -1238,7 +1244,10 @@ public void SetTwoFactorProvider(User user, TwoFactorProviderType type) return; } - providers[type].Enabled = true; + if (setEnabled) + { + providers[type].Enabled = true; + } user.SetTwoFactorProviders(providers); if (string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode)) diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 594d8d0ae2ca..be491569ea79 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -598,6 +598,7 @@ public static string GetApplicationCacheServiceBusSubcriptionName(GlobalSettings public static bool IsCorsOriginAllowed(string origin, GlobalSettings globalSettings) { + return true; return // Web vault origin == globalSettings.BaseServiceUri.Vault || diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 4ffa54b86251..9882e45da707 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -268,7 +268,9 @@ public static void AddNoopServices(this IServiceCollection services) CoreHelpers.CustomProviderName(TwoFactorProviderType.U2f)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)) - .AddTokenProvider>(TokenOptions.DefaultEmailProvider); + .AddTokenProvider>(TokenOptions.DefaultEmailProvider) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.WebAuthn)); return identityBuilder; } diff --git a/src/Identity/Properties/serviceDependencies.json b/src/Identity/Properties/serviceDependencies.json deleted file mode 100644 index a4e7aa3d33c5..000000000000 --- a/src/Identity/Properties/serviceDependencies.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets" - } - } -} \ No newline at end of file diff --git a/src/Identity/Properties/serviceDependencies.local.json b/src/Identity/Properties/serviceDependencies.local.json deleted file mode 100644 index 09b109bc6fe1..000000000000 --- a/src/Identity/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets.user" - } - } -} \ No newline at end of file diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 569d7b674bdd..092db0d7020c 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -58,9 +58,9 @@ public void ConfigureServices(IServiceCollection services) // Fido2 services.AddFido2(options => { - options.ServerDomain = "localhost"; + options.ServerDomain = "vault.bitwarden2.com"; options.ServerName = "Bitwarden"; - options.Origin = "https://localhost:4000"; + options.Origin = "https://vault.bitwarden2.com:8080"; options.TimestampDriftTolerance = 300000; }); From 2d50a1dd31487af1f56a7a947c93c0117a278f9b Mon Sep 17 00:00:00 2001 From: Hinton Date: Sun, 30 Aug 2020 00:57:03 +0200 Subject: [PATCH 04/20] Cleanup hardcoded urls, fix delete, prevent user from registering same key twice --- src/Api/Controllers/TwoFactorController.cs | 12 +++++- src/Api/Startup.cs | 5 ++- src/Core/Models/TwoFactorProvider.cs | 9 ++++- src/Core/Services/IUserService.cs | 3 +- .../Services/Implementations/UserService.cs | 39 +++++++++++++++++-- src/Identity/Startup.cs | 4 +- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index 5b7a7509f64f..a642f7f6e9f4 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -239,7 +239,7 @@ public async Task GetWebAuthnChallenge([FromBody] TwoFa [HttpPut("webauthn")] [HttpPost("webauthn")] - public async Task PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) + public async Task PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); @@ -249,8 +249,16 @@ public async Task PutWebAuthn([FromBody] TwoFactorWeb { throw new BadRequestException("Unable to complete WebAuthn registration."); } + var response = new TwoFactorWebAuthnResponseModel(user); + return response; + } - var response = new TwoFactorU2fResponseModel(user); + [HttpDelete("webauthn")] + public async Task DeleteWebAuthn([FromBody] TwoFactorU2fDeleteRequestModel model) + { + var user = await CheckAsync(model.MasterPasswordHash, true); + await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value); + var response = new TwoFactorWebAuthnResponseModel(user); return response; } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index caabbc0a5844..0f0fef7dc3cf 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using System.Collections.Generic; +using System; namespace Bit.Api { @@ -114,9 +115,9 @@ public void ConfigureServices(IServiceCollection services) // Fido2 services.AddFido2(options => { - options.ServerDomain = "vault.bitwarden2.com"; + options.ServerDomain = new Uri(globalSettings.BaseServiceUri.Vault).Host; options.ServerName = "Bitwarden"; - options.Origin = "https://vault.bitwarden2.com:8080"; + options.Origin = globalSettings.BaseServiceUri.Vault; options.TimestampDriftTolerance = 300000; }); diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 0b4e7d1d85ef..300b42dec758 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -52,7 +52,14 @@ public WebAuthnData(dynamic o) { Name = o.Name; Options = o.Options; - Descriptor = JsonConvert.DeserializeObject(o.Descriptor.ToString()); + try + { + Descriptor = o.Descriptor; + } catch + { + // Handle newtonsoft parsing + Descriptor = JsonConvert.DeserializeObject(o.Descriptor.ToString()); + } PublicKey = o.PublicKey; UserHandle = o.UserHandle; SignatureCounter = o.SignatureCounter; diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 9ce8b1a3d359..7e01145636ca 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -25,8 +25,9 @@ public interface IUserService Task SendTwoFactorEmailAsync(User user); Task VerifyTwoFactorEmailAsync(User user, string token); Task StartWebAuthnRegistrationAsync(User user); - Task StartU2fRegistrationAsync(User user); + Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); + Task StartU2fRegistrationAsync(User user); Task DeleteU2fKeyAsync(User user, int id); Task CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse); Task SendEmailVerificationAsync(User user); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 519f7f53e5c1..e30d0d1380d5 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -372,10 +372,6 @@ public async Task StartWebAuthnRegistrationAsync(User u Id = user.Id.ToByteArray(), }; - // TODO: We need to get the existing keys to exclude them from generation - - var options = _fido2.RequestNewCredential(fidoUser, new List(), AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); - var providers = user.GetTwoFactorProviders(); if (providers == null) { @@ -393,6 +389,14 @@ public async Task StartWebAuthnRegistrationAsync(User u { provider.MetaData = new Dictionary(); } + + var excludeCredentials = provider.MetaData + .Where(k => k.Key.StartsWith("Key")) + .Select(k => new TwoFactorProvider.WebAuthnData((dynamic)k.Value).Descriptor) + .ToList(); + + var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); + provider.MetaData.Remove("pending"); provider.MetaData.Add("pending", new TwoFactorProvider.WebAuthnData { @@ -456,6 +460,33 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri return true; } + public async Task DeleteWebAuthnKeyAsync(User user, int id) + { + var providers = user.GetTwoFactorProviders(); + if (providers == null) + { + return false; + } + + var keyName = $"Key{id}"; + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + if (!provider?.MetaData?.ContainsKey(keyName) ?? true) + { + return false; + } + + if (provider.MetaData.Count < 2) + { + return false; + } + + provider.MetaData.Remove(keyName); + providers[TwoFactorProviderType.WebAuthn] = provider; + user.SetTwoFactorProviders(providers); + await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + return true; + } + public async Task StartU2fRegistrationAsync(User user) { await _u2fRepository.DeleteManyByUserIdAsync(user.Id); diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 092db0d7020c..a40b7dc6fd06 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -58,9 +58,9 @@ public void ConfigureServices(IServiceCollection services) // Fido2 services.AddFido2(options => { - options.ServerDomain = "vault.bitwarden2.com"; + options.ServerDomain = new Uri(globalSettings.BaseServiceUri.Vault).Host; options.ServerName = "Bitwarden"; - options.Origin = "https://vault.bitwarden2.com:8080"; + options.Origin = globalSettings.BaseServiceUri.Vault; options.TimestampDriftTolerance = 300000; }); From 2d9d3dae05bb96179c9741b4f38406b8f1065727 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Sep 2020 21:35:53 +0200 Subject: [PATCH 05/20] Add support for old u2f tokens --- src/Core/Identity/WebAuthnTokenProvider.cs | 102 +++++++++++++++++++-- src/Core/Models/Table/User.cs | 5 + 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs index 2230b1afea9c..e3cfcaaafdfe 100644 --- a/src/Core/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -16,6 +16,11 @@ using Microsoft.Extensions.DependencyInjection; using Fido2NetLib.Objects; using Fido2NetLib; +using Bit.Core.Utilities; +using System.Text; +using System.Security.Cryptography.X509Certificates; +using PeterO.Cbor; +using System.Security.Cryptography; namespace Bit.Core.Identity { @@ -66,13 +71,23 @@ public async Task GenerateAsync(string purpose, UserManager manage var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (keys.Count == 0) + var existingCredentials = keys.Select(key => key.Item2.Descriptor); + + // Old u2f tokens + var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + var u2fKeys = LoadU2fKeys(u2fProvider); + var existingU2fCredentials = u2fKeys.Select(key => new PublicKeyCredentialDescriptor{ + Id = key.Item2.KeyHandleBytes, + Type = PublicKeyCredentialType.PublicKey + }); + + var allCredentials = existingCredentials.Union(existingU2fCredentials).ToList(); + + if (allCredentials.Count == 0) { return null; } - var existingCredentials = LoadKeys(provider).Select(key => key.Item2.Descriptor).ToList(); - var exts = new AuthenticationExtensionsClientInputs() { SimpleTransactionAuthorization = "FIDO", @@ -83,9 +98,10 @@ public async Task GenerateAsync(string purpose, UserManager manage }, UserVerificationIndex = true, UserVerificationMethod = true, + AppID = CoreHelpers.U2fAppIdUrl(_globalSettings), }; - var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred, exts); + var options = _fido2.GetAssertionOptions(allCredentials, UserVerificationRequirement.Preferred, exts); provider.MetaData.Remove("login"); provider.MetaData.Add("login", options); @@ -109,7 +125,11 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (keys.Count == 0) + + var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + var u2fKeys = LoadU2fKeys(u2fProvider); + + if (keys.Count == 0 && u2fKeys.Count == 0) { return false; } @@ -124,10 +144,21 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var jsonOptions = provider.MetaData["login"].ToString(); var options = AssertionOptions.FromJson(jsonOptions); - var creds = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)).Item2; + var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); + var u2fCred = u2fKeys.Find(k => k.Item2.KeyHandleBytes.SequenceEqual(clientResponse.Id)); - // 3. Get credential counter from database - var storedCounter = creds.SignatureCounter; + uint storedCounter; + byte[] key; + if (webAuthCred != null) + { + storedCounter = webAuthCred.Item2.SignatureCounter; + key = webAuthCred.Item2.PublicKey; + } + else + { + storedCounter = u2fCred.Item2.Counter; + key = CreatePublicKeyFromU2fRegistrationData(u2fCred.Item2.KeyHandleBytes, u2fCred.Item2.PublicKeyBytes).EncodeToBytes(); + } // 4. Create callback to check if userhandle owns the credentialId IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => @@ -135,12 +166,40 @@ public async Task ValidateAsync(string purpose, string token, UserManager< return true; }; + var test = new CredentialPublicKey(key); + // 5. Make the assertion - var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback); + var res = await _fido2.MakeAssertionAsync(clientResponse, options, key, storedCounter, callback); return res.Status == "ok"; } + public static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] keyHandleData, byte[] publicKeyData) + { + var x = new byte[32]; + var y = new byte[32]; + Buffer.BlockCopy(publicKeyData, 1, x, 0, 32); + Buffer.BlockCopy(publicKeyData, 33, y, 0, 32); + + var point = new ECPoint + { + X = x, + Y = y, + }; + + var coseKey = CBORObject.NewMap(); + + coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2); + coseKey.Add(COSE.KeyCommonParameter.Alg, -7); + + coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256); + + coseKey.Add(COSE.KeyTypeParameter.X, point.X); + coseKey.Add(COSE.KeyTypeParameter.Y, point.Y); + + return coseKey; + } + private bool HasProperMetaData(TwoFactorProvider provider) { return (provider?.MetaData?.Count ?? 0) > 0; @@ -168,5 +227,30 @@ private bool HasProperMetaData(TwoFactorProvider provider) return keys; } + + private List> LoadU2fKeys(TwoFactorProvider provider) + { + var keys = new List>(); + if (!HasProperMetaData(provider)) + { + return keys; + } + + // Support up to 5 keys + for (var i = 1; i <= 5; i++) + { + var keyName = $"Key{i}"; + if (provider.MetaData.ContainsKey(keyName)) + { + var key = new TwoFactorProvider.U2fMetaData((dynamic)provider.MetaData[keyName]); + if (!key?.Compromised ?? false) + { + keys.Add(new Tuple(keyName, key)); + } + } + } + + return keys; + } } } diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index 85417adab632..e8523741381e 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -95,6 +95,11 @@ public bool IsUser() TwoFactorProviders); } + if (!_twoFactorProviders.ContainsKey(TwoFactorProviderType.WebAuthn) && _twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) + { + _twoFactorProviders.Add(TwoFactorProviderType.WebAuthn, new TwoFactorProvider { Enabled = true }); + } + return _twoFactorProviders; } catch (JsonSerializationException) From 1ffa1ad52286973e8317a2a2f4aae4d983e6c8e9 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Sep 2020 22:38:08 +0200 Subject: [PATCH 06/20] Cleanup WebAuthnTokenProvider, set SignatureCounter --- src/Core/Identity/WebAuthnTokenProvider.cs | 121 ++++++++------------- src/Core/Models/TwoFactorProvider.cs | 61 ++++++++++- 2 files changed, 104 insertions(+), 78 deletions(-) diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs index e3cfcaaafdfe..ad4ae6bcfad0 100644 --- a/src/Core/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -3,22 +3,15 @@ using Bit.Core.Models.Table; using Bit.Core.Enums; using Bit.Core.Models; -using Bit.Core.Repositories; using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; -using U2fLib = U2F.Core.Crypto.U2F; -using U2F.Core.Models; -using U2F.Core.Exceptions; -using U2F.Core.Utils; using System; using Bit.Core.Services; using Microsoft.Extensions.DependencyInjection; using Fido2NetLib.Objects; using Fido2NetLib; using Bit.Core.Utilities; -using System.Text; -using System.Security.Cryptography.X509Certificates; using PeterO.Cbor; using System.Security.Cryptography; @@ -27,19 +20,12 @@ namespace Bit.Core.Identity public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider { private readonly IServiceProvider _serviceProvider; - private readonly IU2fRepository _u2fRepository; private readonly IFido2 _fido2; - private readonly IUserService _userService; private readonly GlobalSettings _globalSettings; - public WebAuthnTokenProvider( - IServiceProvider serviceProvider, - IU2fRepository u2fRepository, - IFido2 fido2, - GlobalSettings globalSettings) + public WebAuthnTokenProvider(IServiceProvider serviceProvider, IFido2 fido2, GlobalSettings globalSettings) { _serviceProvider = serviceProvider; - _u2fRepository = u2fRepository; _fido2 = fido2; _globalSettings = globalSettings; } @@ -52,8 +38,9 @@ public async Task CanGenerateTwoFactorTokenAsync(UserManager manager return false; } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); - if (!HasProperMetaData(provider)) + var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + if (!HasProperMetaData(webAuthnProvider) && !HasProperMetaData(u2fProvider)) { return false; } @@ -74,37 +61,29 @@ public async Task GenerateAsync(string purpose, UserManager manage var existingCredentials = keys.Select(key => key.Item2.Descriptor); // Old u2f tokens - var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - var u2fKeys = LoadU2fKeys(u2fProvider); - var existingU2fCredentials = u2fKeys.Select(key => new PublicKeyCredentialDescriptor{ - Id = key.Item2.KeyHandleBytes, - Type = PublicKeyCredentialType.PublicKey - }); - - var allCredentials = existingCredentials.Union(existingU2fCredentials).ToList(); + var existingU2fCredentials = LoadU2fKeys(user.GetTwoFactorProvider(TwoFactorProviderType.U2f)) + .Select(key => new PublicKeyCredentialDescriptor + { + Id = key.Item2.KeyHandleBytes, + Type = PublicKeyCredentialType.PublicKey + }); - if (allCredentials.Count == 0) + var allowedCredentials = existingCredentials.Union(existingU2fCredentials).ToList(); + if (allowedCredentials.Count == 0) { return null; } var exts = new AuthenticationExtensionsClientInputs() { - SimpleTransactionAuthorization = "FIDO", - GenericTransactionAuthorization = new TxAuthGenericArg - { - ContentType = "text/plain", - Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } - }, UserVerificationIndex = true, UserVerificationMethod = true, AppID = CoreHelpers.U2fAppIdUrl(_globalSettings), }; - var options = _fido2.GetAssertionOptions(allCredentials, UserVerificationRequirement.Preferred, exts); + var options = _fido2.GetAssertionOptions(allowedCredentials, UserVerificationRequirement.Preferred, exts); - provider.MetaData.Remove("login"); - provider.MetaData.Add("login", options); + provider.MetaData["login"] = options; var providers = user.GetTwoFactorProviders(); providers.Remove(TwoFactorProviderType.WebAuthn); @@ -129,11 +108,6 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); var u2fKeys = LoadU2fKeys(u2fProvider); - if (keys.Count == 0 && u2fKeys.Count == 0) - { - return false; - } - if (!provider.MetaData.ContainsKey("login")) { return false; @@ -147,59 +121,56 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); var u2fCred = u2fKeys.Find(k => k.Item2.KeyHandleBytes.SequenceEqual(clientResponse.Id)); - uint storedCounter; - byte[] key; - if (webAuthCred != null) + if (webAuthCred == null && u2fCred == null) { - storedCounter = webAuthCred.Item2.SignatureCounter; - key = webAuthCred.Item2.PublicKey; - } - else - { - storedCounter = u2fCred.Item2.Counter; - key = CreatePublicKeyFromU2fRegistrationData(u2fCred.Item2.KeyHandleBytes, u2fCred.Item2.PublicKeyBytes).EncodeToBytes(); + return false; } - // 4. Create callback to check if userhandle owns the credentialId + var key = webAuthCred != null ? webAuthCred.Item2 : (TwoFactorProvider.BaseMetaData)u2fCred.Item2; + IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => { return true; }; - var test = new CredentialPublicKey(key); + var res = await _fido2.MakeAssertionAsync(clientResponse, options, key.GetPublicKey(), key.GetSignatureCounter(), callback); - // 5. Make the assertion - var res = await _fido2.MakeAssertionAsync(clientResponse, options, key, storedCounter, callback); - - return res.Status == "ok"; - } + provider.MetaData.Remove("login"); - public static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] keyHandleData, byte[] publicKeyData) - { - var x = new byte[32]; - var y = new byte[32]; - Buffer.BlockCopy(publicKeyData, 1, x, 0, 32); - Buffer.BlockCopy(publicKeyData, 33, y, 0, 32); + // Update SignatureCounter + var providers = user.GetTwoFactorProviders(); - var point = new ECPoint + if (webAuthCred != null) { - X = x, - Y = y, - }; - - var coseKey = CBORObject.NewMap(); + webAuthCred.Item2.SignatureCounter = res.Counter; + providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2; + } + providers.Remove(TwoFactorProviderType.WebAuthn); + providers.Add(TwoFactorProviderType.WebAuthn, provider); - coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2); - coseKey.Add(COSE.KeyCommonParameter.Alg, -7); + if (u2fCred != null) + { + u2fCred.Item2.Counter = res.Counter; - coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256); + providers[TwoFactorProviderType.U2f].MetaData[u2fCred.Item1] = u2fCred.Item2; + providers.Remove(TwoFactorProviderType.U2f); + providers.Add(TwoFactorProviderType.U2f, u2fProvider); + user.SetTwoFactorProviders(providers); + await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + } - coseKey.Add(COSE.KeyTypeParameter.X, point.X); - coseKey.Add(COSE.KeyTypeParameter.Y, point.Y); + user.SetTwoFactorProviders(providers); + await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); + if (u2fCred != null) + { + await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); + } - return coseKey; + return res.Status == "ok"; } + + private bool HasProperMetaData(TwoFactorProvider provider) { return (provider?.MetaData?.Count ?? 0) > 0; diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 300b42dec758..b4c639b3eb42 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -2,8 +2,10 @@ using Fido2NetLib.Objects; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using PeterO.Cbor; using System; using System.Collections.Generic; +using System.Security.Cryptography; using U2F.Core.Utils; namespace Bit.Core.Models @@ -13,7 +15,14 @@ public class TwoFactorProvider public bool Enabled { get; set; } public Dictionary MetaData { get; set; } = new Dictionary(); - public class U2fMetaData + public abstract class BaseMetaData + { + public abstract uint GetSignatureCounter(); + + public abstract byte[] GetPublicKey(); + } + + public class U2fMetaData: BaseMetaData { public U2fMetaData() { } @@ -42,9 +51,45 @@ public U2fMetaData(dynamic o) string.IsNullOrWhiteSpace(Certificate) ? null : Utils.Base64StringToByteArray(Certificate); public uint Counter { get; set; } public bool Compromised { get; set; } + + public override byte[] GetPublicKey() + { + return CreatePublicKeyFromU2fRegistrationData(KeyHandleBytes, PublicKeyBytes).EncodeToBytes(); + } + + public override uint GetSignatureCounter() + { + return Counter; + } + + private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] keyHandleData, byte[] publicKeyData) + { + var x = new byte[32]; + var y = new byte[32]; + Buffer.BlockCopy(publicKeyData, 1, x, 0, 32); + Buffer.BlockCopy(publicKeyData, 33, y, 0, 32); + + var point = new ECPoint + { + X = x, + Y = y, + }; + + var coseKey = CBORObject.NewMap(); + + coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2); + coseKey.Add(COSE.KeyCommonParameter.Alg, -7); + + coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256); + + coseKey.Add(COSE.KeyTypeParameter.X, point.X); + coseKey.Add(COSE.KeyTypeParameter.Y, point.Y); + + return coseKey; + } } - public class WebAuthnData + public class WebAuthnData: BaseMetaData { public WebAuthnData() { } @@ -73,10 +118,20 @@ public WebAuthnData(dynamic o) public PublicKeyCredentialDescriptor Descriptor { get; internal set; } public byte[] PublicKey { get; internal set; } public byte[] UserHandle { get; internal set; } - public uint SignatureCounter { get; internal set; } + public uint SignatureCounter { get; set; } public string CredType { get; internal set; } public DateTime RegDate { get; internal set; } public Guid AaGuid { get; internal set; } + + public override uint GetSignatureCounter() + { + return SignatureCounter; + } + + public override byte[] GetPublicKey() + { + return PublicKey; + } } public static bool RequiresPremium(TwoFactorProviderType type) From a4e6965e47988f1f7dd69cd645cca57f0111002c Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Sep 2020 22:44:37 +0200 Subject: [PATCH 07/20] Cleanup UserService --- src/Core/Identity/WebAuthnTokenProvider.cs | 5 +---- .../TwoFactor/TwoFactorWebAuthnResponseModel.cs | 3 +-- src/Core/Models/Table/User.cs | 1 + src/Core/Services/Implementations/UserService.cs | 12 ++---------- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs index ad4ae6bcfad0..41c6f109f4c8 100644 --- a/src/Core/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -128,10 +128,7 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var key = webAuthCred != null ? webAuthCred.Item2 : (TwoFactorProvider.BaseMetaData)u2fCred.Item2; - IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => - { - return true; - }; + IsUserHandleOwnerOfCredentialIdAsync callback = (args) => Task.FromResult(true); var res = await _fido2.MakeAssertionAsync(clientResponse, options, key.GetPublicKey(), key.GetSignatureCounter(), callback); diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs index e90fc0245d8b..a05a054437fc 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs @@ -11,14 +11,13 @@ namespace Bit.Core.Models.Api public class TwoFactorWebAuthnResponseModel : ResponseModel { public TwoFactorWebAuthnResponseModel(User user) - : base("twoFactorU2f") + : base("twoFactorWebAuthn") { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); Enabled = provider?.Enabled ?? false; Keys = provider?.MetaData? diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index e8523741381e..11158ef7a038 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -95,6 +95,7 @@ public bool IsUser() TwoFactorProviders); } + // Enable WebAuthn if user has U2f tokens. if (!_twoFactorProviders.ContainsKey(TwoFactorProviderType.WebAuthn) && _twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) { _twoFactorProviders.Add(TwoFactorProviderType.WebAuthn, new TwoFactorProvider { Enabled = true }); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index e30d0d1380d5..ffc208b0e5cd 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -397,11 +397,7 @@ public async Task StartWebAuthnRegistrationAsync(User u var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); - provider.MetaData.Remove("pending"); - provider.MetaData.Add("pending", new TwoFactorProvider.WebAuthnData - { - Options = options.ToJson() - }); + provider.MetaData["pending"] = new TwoFactorProvider.WebAuthnData{ Options = options.ToJson() }; if (providers.ContainsKey(TwoFactorProviderType.WebAuthn)) @@ -430,11 +426,7 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri var options = CredentialCreateOptions.FromJson(providerData.Options); // Callback to ensure credential id is unique - IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) => - { - // TODO: Check other keys - return true; - }; + IsCredentialIdUniqueToUserAsyncDelegate callback = (IsCredentialIdUniqueToUserParams args) => Task.FromResult(true); var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); From 750bdafcf6672f6f1b60983f06a2036733fc0ecf Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 2 Sep 2020 20:28:24 +0200 Subject: [PATCH 08/20] Review comments --- src/Core/Services/Implementations/UserService.cs | 16 +++++++--------- src/Core/Utilities/CoreHelpers.cs | 1 - 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index ffc208b0e5cd..adabc69f9454 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -363,15 +363,6 @@ public async Task VerifyTwoFactorEmailAsync(User user, string token) public async Task StartWebAuthnRegistrationAsync(User user) { - await _u2fRepository.DeleteManyByUserIdAsync(user.Id); - - var fidoUser = new Fido2User - { - DisplayName = user.Name, - Name = user.Email, - Id = user.Id.ToByteArray(), - }; - var providers = user.GetTwoFactorProviders(); if (providers == null) { @@ -390,6 +381,13 @@ public async Task StartWebAuthnRegistrationAsync(User u provider.MetaData = new Dictionary(); } + var fidoUser = new Fido2User + { + DisplayName = user.Name, + Name = user.Email, + Id = user.Id.ToByteArray(), + }; + var excludeCredentials = provider.MetaData .Where(k => k.Key.StartsWith("Key")) .Select(k => new TwoFactorProvider.WebAuthnData((dynamic)k.Value).Descriptor) diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index be491569ea79..594d8d0ae2ca 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -598,7 +598,6 @@ public static string GetApplicationCacheServiceBusSubcriptionName(GlobalSettings public static bool IsCorsOriginAllowed(string origin, GlobalSettings globalSettings) { - return true; return // Web vault origin == globalSettings.BaseServiceUri.Vault || From c16143f01197c82113b84965d7a94810a2bbab68 Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 9 Sep 2020 18:39:05 +0200 Subject: [PATCH 09/20] Add migration for u2f data --- src/Admin/Startup.cs | 9 ++ src/Core/Identity/WebAuthnTokenProvider.cs | 86 ++---------- src/Core/Models/Table/User.cs | 6 - src/Core/Models/TwoFactorProvider.cs | 49 +++---- ...020-09-09_00-ScriptMigrateU2FToWebAuthn.cs | 122 ++++++++++++++++++ 5 files changed, 161 insertions(+), 111 deletions(-) create mode 100644 util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 11c79be64126..75b1fabe744b 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -65,6 +65,15 @@ public void ConfigureServices(IServiceCollection services) services.AddBaseServices(); services.AddDefaultServices(globalSettings); + // Fido2 + services.AddFido2(options => + { + options.ServerDomain = new Uri(globalSettings.BaseServiceUri.Vault).Host; + options.ServerName = "Bitwarden"; + options.Origin = globalSettings.BaseServiceUri.Vault; + options.TimestampDriftTolerance = 300000; + }); + // Mvc services.AddMvc(config => { diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs index 41c6f109f4c8..b46b31625795 100644 --- a/src/Core/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -12,8 +12,6 @@ using Fido2NetLib.Objects; using Fido2NetLib; using Bit.Core.Utilities; -using PeterO.Cbor; -using System.Security.Cryptography; namespace Bit.Core.Identity { @@ -39,8 +37,7 @@ public async Task CanGenerateTwoFactorTokenAsync(UserManager manager } var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); - var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if (!HasProperMetaData(webAuthnProvider) && !HasProperMetaData(u2fProvider)) + if (!HasProperMetaData(webAuthnProvider)) { return false; } @@ -58,18 +55,9 @@ public async Task GenerateAsync(string purpose, UserManager manage var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - var existingCredentials = keys.Select(key => key.Item2.Descriptor); + var existingCredentials = keys.Select(key => key.Item2.Descriptor).ToList(); - // Old u2f tokens - var existingU2fCredentials = LoadU2fKeys(user.GetTwoFactorProvider(TwoFactorProviderType.U2f)) - .Select(key => new PublicKeyCredentialDescriptor - { - Id = key.Item2.KeyHandleBytes, - Type = PublicKeyCredentialType.PublicKey - }); - - var allowedCredentials = existingCredentials.Union(existingU2fCredentials).ToList(); - if (allowedCredentials.Count == 0) + if (existingCredentials.Count == 0) { return null; } @@ -81,7 +69,7 @@ public async Task GenerateAsync(string purpose, UserManager manage AppID = CoreHelpers.U2fAppIdUrl(_globalSettings), }; - var options = _fido2.GetAssertionOptions(allowedCredentials, UserVerificationRequirement.Preferred, exts); + var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred, exts); provider.MetaData["login"] = options; @@ -105,9 +93,6 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - var u2fProvider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - var u2fKeys = LoadU2fKeys(u2fProvider); - if (!provider.MetaData.ContainsKey("login")) { return false; @@ -119,55 +104,29 @@ public async Task ValidateAsync(string purpose, string token, UserManager< var options = AssertionOptions.FromJson(jsonOptions); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); - var u2fCred = u2fKeys.Find(k => k.Item2.KeyHandleBytes.SequenceEqual(clientResponse.Id)); - if (webAuthCred == null && u2fCred == null) + if (webAuthCred == null) { return false; } - var key = webAuthCred != null ? webAuthCred.Item2 : (TwoFactorProvider.BaseMetaData)u2fCred.Item2; - IsUserHandleOwnerOfCredentialIdAsync callback = (args) => Task.FromResult(true); - var res = await _fido2.MakeAssertionAsync(clientResponse, options, key.GetPublicKey(), key.GetSignatureCounter(), callback); + var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback); provider.MetaData.Remove("login"); // Update SignatureCounter - var providers = user.GetTwoFactorProviders(); - - if (webAuthCred != null) - { - webAuthCred.Item2.SignatureCounter = res.Counter; - providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2; - } - providers.Remove(TwoFactorProviderType.WebAuthn); - providers.Add(TwoFactorProviderType.WebAuthn, provider); - - if (u2fCred != null) - { - u2fCred.Item2.Counter = res.Counter; - - providers[TwoFactorProviderType.U2f].MetaData[u2fCred.Item1] = u2fCred.Item2; - providers.Remove(TwoFactorProviderType.U2f); - providers.Add(TwoFactorProviderType.U2f, u2fProvider); - user.SetTwoFactorProviders(providers); - await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); - } + webAuthCred.Item2.SignatureCounter = res.Counter; + var providers = user.GetTwoFactorProviders(); + providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2; user.SetTwoFactorProviders(providers); await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); - if (u2fCred != null) - { - await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); - } - return res.Status == "ok"; + return res.Status == "ok"; } - - private bool HasProperMetaData(TwoFactorProvider provider) { return (provider?.MetaData?.Count ?? 0) > 0; @@ -195,30 +154,5 @@ private bool HasProperMetaData(TwoFactorProvider provider) return keys; } - - private List> LoadU2fKeys(TwoFactorProvider provider) - { - var keys = new List>(); - if (!HasProperMetaData(provider)) - { - return keys; - } - - // Support up to 5 keys - for (var i = 1; i <= 5; i++) - { - var keyName = $"Key{i}"; - if (provider.MetaData.ContainsKey(keyName)) - { - var key = new TwoFactorProvider.U2fMetaData((dynamic)provider.MetaData[keyName]); - if (!key?.Compromised ?? false) - { - keys.Add(new Tuple(keyName, key)); - } - } - } - - return keys; - } } } diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index 11158ef7a038..85417adab632 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -95,12 +95,6 @@ public bool IsUser() TwoFactorProviders); } - // Enable WebAuthn if user has U2f tokens. - if (!_twoFactorProviders.ContainsKey(TwoFactorProviderType.WebAuthn) && _twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) - { - _twoFactorProviders.Add(TwoFactorProviderType.WebAuthn, new TwoFactorProvider { Enabled = true }); - } - return _twoFactorProviders; } catch (JsonSerializationException) diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index b4c639b3eb42..9c245bb0270c 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -15,14 +15,7 @@ public class TwoFactorProvider public bool Enabled { get; set; } public Dictionary MetaData { get; set; } = new Dictionary(); - public abstract class BaseMetaData - { - public abstract uint GetSignatureCounter(); - - public abstract byte[] GetPublicKey(); - } - - public class U2fMetaData: BaseMetaData + public class U2fMetaData { public U2fMetaData() { } @@ -52,16 +45,6 @@ public U2fMetaData(dynamic o) public uint Counter { get; set; } public bool Compromised { get; set; } - public override byte[] GetPublicKey() - { - return CreatePublicKeyFromU2fRegistrationData(KeyHandleBytes, PublicKeyBytes).EncodeToBytes(); - } - - public override uint GetSignatureCounter() - { - return Counter; - } - private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] keyHandleData, byte[] publicKeyData) { var x = new byte[32]; @@ -87,9 +70,25 @@ private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] keyHandl return coseKey; } + + public WebAuthnData ToWebAuthnKey() + { + return new WebAuthnData + { + Name = Name, + Descriptor = new PublicKeyCredentialDescriptor + { + Id = KeyHandleBytes, + Type = PublicKeyCredentialType.PublicKey + }, + PublicKey = CreatePublicKeyFromU2fRegistrationData(KeyHandleBytes, PublicKeyBytes).EncodeToBytes(), + SignatureCounter = Counter, + Migrated = true, + }; + } } - public class WebAuthnData: BaseMetaData + public class WebAuthnData { public WebAuthnData() { } @@ -111,6 +110,7 @@ public WebAuthnData(dynamic o) CredType = o.CredType; RegDate = o.RegDate; AaGuid = o.AaGuid; + Migrated = o.Migrated; } public string Name { get; set; } @@ -122,16 +122,7 @@ public WebAuthnData(dynamic o) public string CredType { get; internal set; } public DateTime RegDate { get; internal set; } public Guid AaGuid { get; internal set; } - - public override uint GetSignatureCounter() - { - return SignatureCounter; - } - - public override byte[] GetPublicKey() - { - return PublicKey; - } + public bool Migrated { get; internal set; } } public static bool RequiresPremium(TwoFactorProviderType type) diff --git a/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs b/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs new file mode 100644 index 000000000000..35d6811543f0 --- /dev/null +++ b/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models; +using DbUp.Engine; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json; + +namespace Bit.Migrator.DbScripts +{ + class ScriptMigrateU2FToWebAuthn : IScript + { + + public string ProvideScript(Func commandFactory) + { + var cmd = commandFactory(); + cmd.CommandText = "SELECT Id, TwoFactorProviders FROM [dbo].[User] WHERE TwoFactorProviders IS NOT NULL"; + + var users = new List(); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + var twoFactorProviders = reader.GetString(1); + + if (string.IsNullOrWhiteSpace(twoFactorProviders)) + { + continue; + } + + var providers = JsonConvert.DeserializeObject>(twoFactorProviders); + + if (!providers.ContainsKey(TwoFactorProviderType.U2f)) + { + continue; + } + + var u2fProvider = providers[TwoFactorProviderType.U2f]; + + if (!u2fProvider.Enabled || !HasProperMetaData(u2fProvider)) + { + continue; + } + + var u2fKeys = LoadKeys(u2fProvider); + var webAuthnKeys = u2fKeys.Select(key => (key.Item1, key.Item2.ToWebAuthnKey())); + + var webAuthnProvider = new TwoFactorProvider + { + Enabled = true, + MetaData = webAuthnKeys.ToDictionary(x => x.Item1, x => (object)x.Item2) + }; + + providers[TwoFactorProviderType.WebAuthn] = webAuthnProvider; + + users.Add(new User + { + Id = id, + TwoFactorProviders = JsonConvert.SerializeObject(providers), + }); + } + } + + foreach (User user in users) + { + var command = commandFactory(); + + command.CommandText = "UPDATE [dbo].[User] SET TwoFactorProviders = @twoFactorProviders WHERE Id = @id"; + var idParameter = command.CreateParameter(); + idParameter.ParameterName = "@id"; + idParameter.Value = user.Id; + + var twoFactorParameter = command.CreateParameter(); + twoFactorParameter.ParameterName = "@twoFactorProviders"; + twoFactorParameter.Value = user.TwoFactorProviders; + + command.Parameters.Add(idParameter); + command.Parameters.Add(twoFactorParameter); + + command.ExecuteNonQuery(); + } + + return ""; + } + + private bool HasProperMetaData(TwoFactorProvider provider) + { + return (provider?.MetaData?.Count ?? 0) > 0; + } + + private List> LoadKeys(TwoFactorProvider provider) + { + var keys = new List>(); + + // Support up to 5 keys + for (var i = 1; i <= 5; i++) + { + var keyName = $"Key{i}"; + if (provider.MetaData.ContainsKey(keyName)) + { + var key = new TwoFactorProvider.U2fMetaData((dynamic)provider.MetaData[keyName]); + if (!key?.Compromised ?? false) + { + keys.Add(new Tuple(keyName, key)); + } + } + } + + return keys; + } + + private class User + { + public Guid Id { get; set; } + public string TwoFactorProviders { get; set; } + } + } +} From a292619187aca62960bef3aecf0a83f9f94b9df2 Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 9 Sep 2020 18:47:59 +0200 Subject: [PATCH 10/20] Minor refactor --- src/Core/Models/TwoFactorProvider.cs | 2 - .../Services/Implementations/UserService.cs | 38 ++++++++----------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 9c245bb0270c..b7fd1464f476 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -95,7 +95,6 @@ public class WebAuthnData public WebAuthnData(dynamic o) { Name = o.Name; - Options = o.Options; try { Descriptor = o.Descriptor; @@ -114,7 +113,6 @@ public WebAuthnData(dynamic o) } public string Name { get; set; } - public string Options { get; set; } public PublicKeyCredentialDescriptor Descriptor { get; internal set; } public byte[] PublicKey { get; internal set; } public byte[] UserHandle { get; internal set; } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index adabc69f9454..a00fb582daec 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -395,15 +395,9 @@ public async Task StartWebAuthnRegistrationAsync(User u var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); - provider.MetaData["pending"] = new TwoFactorProvider.WebAuthnData{ Options = options.ToJson() }; + provider.MetaData["pending"] = options.ToJson(); - - if (providers.ContainsKey(TwoFactorProviderType.WebAuthn)) - { - providers.Remove(TwoFactorProviderType.WebAuthn); - } - - providers.Add(TwoFactorProviderType.WebAuthn, provider); + providers[TwoFactorProviderType.WebAuthn] = provider; user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false); @@ -420,30 +414,28 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri return false; } - var providerData = new TwoFactorProvider.WebAuthnData(provider.MetaData["pending"]); - var options = CredentialCreateOptions.FromJson(providerData.Options); + var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]); // Callback to ensure credential id is unique IsCredentialIdUniqueToUserAsyncDelegate callback = (IsCredentialIdUniqueToUserParams args) => Task.FromResult(true); var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); - providerData.Options = ""; - providerData.Name = name; - providerData.Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId); - providerData.PublicKey = success.Result.PublicKey; - providerData.UserHandle = success.Result.User.Id; - providerData.SignatureCounter = success.Result.Counter; - providerData.CredType = success.Result.CredType; - providerData.RegDate = DateTime.Now; - providerData.AaGuid = success.Result.Aaguid; - - provider.MetaData[keyId] = providerData; provider.MetaData.Remove("pending"); + provider.MetaData[keyId] = new TwoFactorProvider.WebAuthnData + { + Name = name, + Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), + PublicKey = success.Result.PublicKey, + UserHandle = success.Result.User.Id, + SignatureCounter = success.Result.Counter, + CredType = success.Result.CredType, + RegDate = DateTime.Now, + AaGuid = success.Result.Aaguid + }; var providers = user.GetTwoFactorProviders(); - providers.Remove(TwoFactorProviderType.WebAuthn); - providers.Add(TwoFactorProviderType.WebAuthn, provider); + providers[TwoFactorProviderType.WebAuthn] = provider; user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); From 1f2c4e24b7d57ee9ca5ff93b96f50d31383add3b Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 9 Sep 2020 19:20:33 +0200 Subject: [PATCH 11/20] Rename ToWebAuthnKey to ToWebAuthnData --- src/Core/Identity/WebAuthnTokenProvider.cs | 3 +-- src/Core/Models/TwoFactorProvider.cs | 2 +- .../DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs index b46b31625795..8eecd3734558 100644 --- a/src/Core/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -74,8 +74,7 @@ public async Task GenerateAsync(string purpose, UserManager manage provider.MetaData["login"] = options; var providers = user.GetTwoFactorProviders(); - providers.Remove(TwoFactorProviderType.WebAuthn); - providers.Add(TwoFactorProviderType.WebAuthn, provider); + providers[TwoFactorProviderType.WebAuthn] = provider; user.SetTwoFactorProviders(providers); await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index b7fd1464f476..0d5977b041a4 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -71,7 +71,7 @@ private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] keyHandl return coseKey; } - public WebAuthnData ToWebAuthnKey() + public WebAuthnData ToWebAuthnData() { return new WebAuthnData { diff --git a/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs b/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs index 35d6811543f0..d441da6d8e34 100644 --- a/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs +++ b/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs @@ -47,7 +47,7 @@ public string ProvideScript(Func commandFactory) } var u2fKeys = LoadKeys(u2fProvider); - var webAuthnKeys = u2fKeys.Select(key => (key.Item1, key.Item2.ToWebAuthnKey())); + var webAuthnKeys = u2fKeys.Select(key => (key.Item1, key.Item2.ToWebAuthnData())); var webAuthnProvider = new TwoFactorProvider { From e014b862cdf123b1798e974f230b14525f78fef6 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 11 Sep 2020 13:41:11 +0200 Subject: [PATCH 12/20] Add WebAuthn connector to nginx config --- .../DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs | 1 - util/Setup/Templates/NginxConfig.hbs | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs b/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs index d441da6d8e34..5990665873a6 100644 --- a/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs +++ b/util/Migrator/DbScripts/2020-09-09_00-ScriptMigrateU2FToWebAuthn.cs @@ -5,7 +5,6 @@ using Bit.Core.Enums; using Bit.Core.Models; using DbUp.Engine; -using Microsoft.VisualBasic.CompilerServices; using Newtonsoft.Json; namespace Bit.Migrator.DbScripts diff --git a/util/Setup/Templates/NginxConfig.hbs b/util/Setup/Templates/NginxConfig.hbs index 27f5ec3d0a82..33d959d07381 100644 --- a/util/Setup/Templates/NginxConfig.hbs +++ b/util/Setup/Templates/NginxConfig.hbs @@ -92,6 +92,10 @@ server { proxy_pass http://web:5000/u2f-connector.html; } + location = /webauthn-connector.html { + proxy_pass http://web:5000/webauthn-connector.html; + } + location /attachments/ { proxy_pass http://attachments:5000/; } From 5858d0430fcb7d6dfd4f326cf02fe2d6f8953671 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 11 Sep 2020 13:55:41 +0200 Subject: [PATCH 13/20] Remove old u2f routes and code. --- src/Api/Controllers/TwoFactorController.cs | 46 +----- .../Api/Request/TwoFactorRequestModels.cs | 11 +- .../TwoFactor/TwoFactorU2fResponseModel.cs | 59 -------- src/Core/Models/Business/U2fRegistration.cs | 9 -- src/Core/Services/IUserService.cs | 3 - .../Services/Implementations/UserService.cs | 134 ------------------ test/Core.Test/Services/UserServiceTests.cs | 7 +- 7 files changed, 7 insertions(+), 262 deletions(-) delete mode 100644 src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs delete mode 100644 src/Core/Models/Business/U2fRegistration.cs diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index a642f7f6e9f4..1fb26c02d9b8 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -14,8 +14,6 @@ using Bit.Core.Utilities; using Bit.Core.Utilities.Duo; using Fido2NetLib; -using System.Collections.Generic; -using Fido2NetLib.Objects; namespace Bit.Api.Controllers { @@ -254,7 +252,7 @@ public async Task PutWebAuthn([FromBody] TwoFact } [HttpDelete("webauthn")] - public async Task DeleteWebAuthn([FromBody] TwoFactorU2fDeleteRequestModel model) + public async Task DeleteWebAuthn([FromBody] TwoFactorWebAuthnDeleteRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value); @@ -262,48 +260,6 @@ public async Task DeleteWebAuthn([FromBody] TwoF return response; } - [HttpPost("get-u2f")] - public async Task GetU2f([FromBody]TwoFactorRequestModel model) - { - var user = await CheckAsync(model.MasterPasswordHash, true); - var response = new TwoFactorU2fResponseModel(user); - return response; - } - - [HttpPost("get-u2f-challenge")] - public async Task GetU2fChallenge( - [FromBody]TwoFactorRequestModel model) - { - var user = await CheckAsync(model.MasterPasswordHash, true); - var reg = await _userService.StartU2fRegistrationAsync(user); - var challenge = new TwoFactorU2fResponseModel.ChallengeModel(user, reg); - return challenge; - } - - [HttpPut("u2f")] - [HttpPost("u2f")] - public async Task PutU2f([FromBody]TwoFactorU2fRequestModel model) - { - var user = await CheckAsync(model.MasterPasswordHash, true); - var success = await _userService.CompleteU2fRegistrationAsync( - user, model.Id.Value, model.Name, model.DeviceResponse); - if (!success) - { - throw new BadRequestException("Unable to complete U2F key registration."); - } - var response = new TwoFactorU2fResponseModel(user); - return response; - } - - [HttpDelete("u2f")] - public async Task DeleteU2f([FromBody]TwoFactorU2fDeleteRequestModel model) - { - var user = await CheckAsync(model.MasterPasswordHash, true); - await _userService.DeleteU2fKeyAsync(user, model.Id.Value); - var response = new TwoFactorU2fResponseModel(user); - return response; - } - [HttpPost("get-email")] public async Task GetEmail([FromBody]TwoFactorRequestModel model) { diff --git a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs index c8748bd84da2..94c297bba9ae 100644 --- a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs +++ b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs @@ -224,21 +224,14 @@ public User ToUser(User extistingUser) } } - public class TwoFactorWebAuthnRequestModel : TwoFactorU2fDeleteRequestModel + public class TwoFactorWebAuthnRequestModel : TwoFactorWebAuthnDeleteRequestModel { [Required] public AuthenticatorAttestationRawResponse DeviceResponse { get; set; } public string Name { get; set; } } - public class TwoFactorU2fRequestModel : TwoFactorU2fDeleteRequestModel - { - [Required] - public string DeviceResponse { get; set; } - public string Name { get; set; } - } - - public class TwoFactorU2fDeleteRequestModel : TwoFactorRequestModel, IValidatableObject + public class TwoFactorWebAuthnDeleteRequestModel : TwoFactorRequestModel, IValidatableObject { [Required] public int? Id { get; set; } diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs deleted file mode 100644 index 121be3d4be47..000000000000 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using Bit.Core.Models.Table; -using Bit.Core.Models.Business; -using Bit.Core.Enums; -using System.Collections.Generic; -using System.Linq; - -namespace Bit.Core.Models.Api -{ - public class TwoFactorU2fResponseModel : ResponseModel - { - public TwoFactorU2fResponseModel(User user) - : base("twoFactorU2f") - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - Enabled = provider?.Enabled ?? false; - Keys = provider?.MetaData?.Select(k => new KeyModel(k.Key, - new TwoFactorProvider.U2fMetaData((dynamic)k.Value))); - } - - public bool Enabled { get; set; } - public IEnumerable Keys { get; set; } - - public class KeyModel - { - public KeyModel(string id, TwoFactorProvider.U2fMetaData data) - { - Name = data.Name; - Id = Convert.ToInt32(id.Replace("Key", string.Empty)); - Compromised = data.Compromised; - } - - public string Name { get; set; } - public int Id { get; set; } - public bool Compromised { get; set; } - } - - public class ChallengeModel - { - public ChallengeModel(User user, U2fRegistration registration) - { - UserId = user.Id.ToString(); - AppId = registration.AppId; - Challenge = registration.Challenge; - Version = registration.Version; - } - - public string UserId { get; set; } - public string AppId { get; set; } - public string Challenge { get; set; } - public string Version { get; set; } - } - } -} diff --git a/src/Core/Models/Business/U2fRegistration.cs b/src/Core/Models/Business/U2fRegistration.cs deleted file mode 100644 index c27afdc40a59..000000000000 --- a/src/Core/Models/Business/U2fRegistration.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bit.Core.Models.Business -{ - public class U2fRegistration - { - public string AppId { get; set; } - public string Challenge { get; set; } - public string Version { get; set; } - } -} diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 7e01145636ca..2b0d8126bce9 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -27,9 +27,6 @@ public interface IUserService Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); - Task StartU2fRegistrationAsync(User user); - Task DeleteU2fKeyAsync(User user, int id); - Task CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse); Task SendEmailVerificationAsync(User user); Task ConfirmEmailAsync(User user, string token); Task InitiateEmailChangeAsync(User user, string newEmail); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index a00fb582daec..a11895dfe90f 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -11,15 +11,11 @@ using System.Security.Claims; using Bit.Core.Models; using Bit.Core.Models.Business; -using U2fLib = U2F.Core.Crypto.U2F; -using U2F.Core.Models; -using U2F.Core.Utils; using Bit.Core.Exceptions; using Bit.Core.Utilities; using System.IO; using Newtonsoft.Json; using Microsoft.AspNetCore.DataProtection; -using U2F.Core.Exceptions; using Fido2NetLib; using Fido2NetLib.Objects; @@ -34,7 +30,6 @@ public class UserService : UserManager, IUserService, IDisposable private readonly ICipherRepository _cipherRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IU2fRepository _u2fRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; @@ -57,7 +52,6 @@ public class UserService : UserManager, IUserService, IDisposable ICipherRepository cipherRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, - IU2fRepository u2fRepository, IMailService mailService, IPushNotificationService pushService, IUserStore store, @@ -94,7 +88,6 @@ public class UserService : UserManager, IUserService, IDisposable _cipherRepository = cipherRepository; _organizationUserRepository = organizationUserRepository; _organizationRepository = organizationRepository; - _u2fRepository = u2fRepository; _mailService = mailService; _pushService = pushService; _identityOptions = optionsAccessor?.Value ?? new IdentityOptions(); @@ -469,133 +462,6 @@ public async Task DeleteWebAuthnKeyAsync(User user, int id) return true; } - public async Task StartU2fRegistrationAsync(User user) - { - await _u2fRepository.DeleteManyByUserIdAsync(user.Id); - var reg = U2fLib.StartRegistration(CoreHelpers.U2fAppIdUrl(_globalSettings)); - await _u2fRepository.CreateAsync(new U2f - { - AppId = reg.AppId, - Challenge = reg.Challenge, - Version = reg.Version, - UserId = user.Id, - CreationDate = DateTime.UtcNow - }); - - return new U2fRegistration - { - AppId = reg.AppId, - Challenge = reg.Challenge, - Version = reg.Version - }; - } - - public async Task CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse) - { - if (string.IsNullOrWhiteSpace(deviceResponse)) - { - return false; - } - - var challenges = await _u2fRepository.GetManyByUserIdAsync(user.Id); - if (!challenges?.Any() ?? true) - { - return false; - } - - var registerResponse = BaseModel.FromJson(deviceResponse); - - try - { - var challenge = challenges.OrderBy(i => i.Id).Last(i => i.KeyHandle == null); - var startedReg = new StartedRegistration(challenge.Challenge, challenge.AppId); - var reg = U2fLib.FinishRegistration(startedReg, registerResponse); - - await _u2fRepository.DeleteManyByUserIdAsync(user.Id); - - // Add device - var providers = user.GetTwoFactorProviders(); - if (providers == null) - { - providers = new Dictionary(); - } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if (provider == null) - { - provider = new TwoFactorProvider(); - } - if (provider.MetaData == null) - { - provider.MetaData = new Dictionary(); - } - - if (provider.MetaData.Count >= 5) - { - // Can only register up to 5 keys - return false; - } - - var keyId = $"Key{id}"; - if (provider.MetaData.ContainsKey(keyId)) - { - provider.MetaData.Remove(keyId); - } - - provider.Enabled = true; - provider.MetaData.Add(keyId, new TwoFactorProvider.U2fMetaData - { - Name = name, - KeyHandle = reg.KeyHandle == null ? null : Utils.ByteArrayToBase64String(reg.KeyHandle), - PublicKey = reg.PublicKey == null ? null : Utils.ByteArrayToBase64String(reg.PublicKey), - Certificate = reg.AttestationCert == null ? null : Utils.ByteArrayToBase64String(reg.AttestationCert), - Compromised = false, - Counter = reg.Counter - }); - - if (providers.ContainsKey(TwoFactorProviderType.U2f)) - { - providers.Remove(TwoFactorProviderType.U2f); - } - - providers.Add(TwoFactorProviderType.U2f, provider); - user.SetTwoFactorProviders(providers); - await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); - return true; - } - catch (U2fException e) - { - Logger.LogError(e, "Complete U2F registration error."); - return false; - } - } - - public async Task DeleteU2fKeyAsync(User user, int id) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null) - { - return false; - } - - var keyName = $"Key{id}"; - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if (!provider?.MetaData?.ContainsKey(keyName) ?? true) - { - return false; - } - - if (provider.MetaData.Count < 2) - { - return false; - } - - provider.MetaData.Remove(keyName); - providers[TwoFactorProviderType.U2f] = provider; - user.SetTwoFactorProviders(providers); - await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); - return true; - } - public async Task SendEmailVerificationAsync(User user) { if (user.EmailVerified) diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 36cc211cbe67..fe1911fbea1a 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; +using Fido2NetLib; namespace Bit.Core.Test.Services { @@ -20,7 +21,6 @@ public class UserServiceTests private readonly ICipherRepository _cipherRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IU2fRepository _u2fRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushService; private readonly IUserStore _userStore; @@ -39,6 +39,7 @@ public class UserServiceTests private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; private readonly IReferenceEventService _referenceEventService; + private readonly IFido2 _fido2; private readonly CurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -48,7 +49,6 @@ public UserServiceTests() _cipherRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); _organizationRepository = Substitute.For(); - _u2fRepository = Substitute.For(); _mailService = Substitute.For(); _pushService = Substitute.For(); _userStore = Substitute.For>(); @@ -67,6 +67,7 @@ public UserServiceTests() _paymentService = Substitute.For(); _policyRepository = Substitute.For(); _referenceEventService = Substitute.For(); + _fido2 = Substitute.For(); _currentContext = new CurrentContext(); _globalSettings = new GlobalSettings(); @@ -75,7 +76,6 @@ public UserServiceTests() _cipherRepository, _organizationUserRepository, _organizationRepository, - _u2fRepository, _mailService, _pushService, _userStore, @@ -94,6 +94,7 @@ public UserServiceTests() _paymentService, _policyRepository, _referenceEventService, + _fido2, _currentContext, _globalSettings ); From dc230df5862b0fcba6e023c3f7bb68d8e4b52c79 Mon Sep 17 00:00:00 2001 From: Hinton Date: Sun, 13 Sep 2020 12:09:06 +0200 Subject: [PATCH 14/20] Send the migrated flag to clients --- .../Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs index a05a054437fc..6b9f09092c71 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs @@ -34,10 +34,12 @@ public KeyModel(string id, TwoFactorProvider.WebAuthnData data) { Name = data.Name; Id = Convert.ToInt32(id.Replace("Key", string.Empty)); + Migrated = data.Migrated; } public string Name { get; set; } public int Id { get; set; } + public bool Migrated { get; set; } } } } From 891dd47d54b1f29a248f7575d3e1e253164d0d06 Mon Sep 17 00:00:00 2001 From: Hinton Date: Sun, 20 Sep 2020 14:14:40 +0200 Subject: [PATCH 15/20] Fix format --- src/Api/Controllers/TwoFactorController.cs | 8 ++++---- .../Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index 1fb26c02d9b8..e1fe8dd7eb9f 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -220,7 +220,7 @@ public async Task PutDuo([FromBody]UpdateTwoFactorDuo } [HttpPost("get-webauthn")] - public async Task GetWebAuthn([FromBody] TwoFactorRequestModel model) + public async Task GetWebAuthn([FromBody]TwoFactorRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); var response = new TwoFactorWebAuthnResponseModel(user); @@ -228,7 +228,7 @@ public async Task GetWebAuthn([FromBody] TwoFact } [HttpPost("get-webauthn-challenge")] - public async Task GetWebAuthnChallenge([FromBody] TwoFactorRequestModel model) + public async Task GetWebAuthnChallenge([FromBody]TwoFactorRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); var reg = await _userService.StartWebAuthnRegistrationAsync(user); @@ -237,7 +237,7 @@ public async Task GetWebAuthnChallenge([FromBody] TwoFa [HttpPut("webauthn")] [HttpPost("webauthn")] - public async Task PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) + public async Task PutWebAuthn([FromBody]TwoFactorWebAuthnRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); @@ -252,7 +252,7 @@ public async Task PutWebAuthn([FromBody] TwoFact } [HttpDelete("webauthn")] - public async Task DeleteWebAuthn([FromBody] TwoFactorWebAuthnDeleteRequestModel model) + public async Task DeleteWebAuthn([FromBody]TwoFactorWebAuthnDeleteRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value); diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs index 6b9f09092c71..3c4da9e05c78 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs @@ -4,7 +4,6 @@ using Bit.Core.Enums; using System.Collections.Generic; using System.Linq; -using Fido2NetLib; namespace Bit.Core.Models.Api { From 210f9d5c9ecd5ab5bc0bc4d68501b34e96948082 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 18 Feb 2021 20:03:58 +0100 Subject: [PATCH 16/20] Remove unessesary call to setTwoFactorProviders --- src/Core/Services/Implementations/UserService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index b32a9c20f2ec..04e421310821 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -399,7 +399,6 @@ public async Task StartWebAuthnRegistrationAsync(User u provider.MetaData["pending"] = options.ToJson(); providers[TwoFactorProviderType.WebAuthn] = provider; - user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false); return options; @@ -437,7 +436,6 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri var providers = user.GetTwoFactorProviders(); providers[TwoFactorProviderType.WebAuthn] = provider; - user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); return true; From 603e0c6019def43e7154aabc26718b4a4b72bd98 Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 3 Mar 2021 22:15:12 +0100 Subject: [PATCH 17/20] Accidently removed `user.SetTwoFactorProviders`. --- src/Core/Services/Implementations/UserService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 04e421310821..bd74912991c1 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -397,8 +397,8 @@ public async Task StartWebAuthnRegistrationAsync(User u var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); provider.MetaData["pending"] = options.ToJson(); - providers[TwoFactorProviderType.WebAuthn] = provider; + user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false); return options; @@ -417,7 +417,7 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]); // Callback to ensure credential id is unique - IsCredentialIdUniqueToUserAsyncDelegate callback = (IsCredentialIdUniqueToUserParams args) => Task.FromResult(true); + IsCredentialIdUniqueToUserAsyncDelegate callback = args => Task.FromResult(true); var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); @@ -436,6 +436,7 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri var providers = user.GetTwoFactorProviders(); providers[TwoFactorProviderType.WebAuthn] = provider; + user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn); return true; From b367db17d06775738cf81e32b72d04c807661d90 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 4 Mar 2021 21:18:19 +0100 Subject: [PATCH 18/20] Add webauthn-fallback-connector to nginx config --- src/Core/Services/Implementations/UserService.cs | 3 ++- util/Setup/Templates/NginxConfig.hbs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index bd74912991c1..4a7401e2d561 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -416,7 +416,8 @@ public async Task CompleteWebAuthRegistrationAsync(User user, int id, stri var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]); - // Callback to ensure credential id is unique + // Callback to ensure credential id is unique. Always return true since we don't care if another + // account uses the same 2fa key. IsCredentialIdUniqueToUserAsyncDelegate callback = args => Task.FromResult(true); var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); diff --git a/util/Setup/Templates/NginxConfig.hbs b/util/Setup/Templates/NginxConfig.hbs index d8d89727f7c1..86af0d1c1319 100644 --- a/util/Setup/Templates/NginxConfig.hbs +++ b/util/Setup/Templates/NginxConfig.hbs @@ -94,8 +94,12 @@ server { location = /webauthn-connector.html { proxy_pass http://web:5000/webauthn-connector.html; - } - + } + + location = /webauthn-fallback-connector.html { + proxy_pass http://web:5000/webauthn-fallback-connector.html; + } + location = /sso-connector.html { proxy_pass http://web:5000/sso-connector.html; } From f6ae381bd18b078b9cadb17b5e7e9f149147fe6b Mon Sep 17 00:00:00 2001 From: Hinton Date: Sat, 6 Mar 2021 23:07:14 +0100 Subject: [PATCH 19/20] Set AttestationConveyancePreference to None to resolve TouchId not working --- src/Core/Services/Implementations/UserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 4a7401e2d561..84bae1875a04 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -394,7 +394,7 @@ public async Task StartWebAuthnRegistrationAsync(User u .Select(k => new TwoFactorProvider.WebAuthnData((dynamic)k.Value).Descriptor) .ToList(); - var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.Direct); + var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.None); provider.MetaData["pending"] = options.ToJson(); providers[TwoFactorProviderType.WebAuthn] = provider; From 6494a057f1b1ebb5359de1853470b9cdd69a558e Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 18 Mar 2021 19:43:07 +0100 Subject: [PATCH 20/20] Fix review comments --- src/Core/Identity/WebAuthnTokenProvider.cs | 2 +- src/Core/Models/TwoFactorProvider.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/Identity/WebAuthnTokenProvider.cs b/src/Core/Identity/WebAuthnTokenProvider.cs index e8e271250fb4..ca4490de9968 100644 --- a/src/Core/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Identity/WebAuthnTokenProvider.cs @@ -129,7 +129,7 @@ public async Task ValidateAsync(string purpose, string token, UserManager< private bool HasProperMetaData(TwoFactorProvider provider) { - return (provider?.MetaData?.Count ?? 0) > 0; + return provider?.MetaData?.Any() ?? false; } private List> LoadKeys(TwoFactorProvider provider) diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 0d5977b041a4..79bb9d448bc4 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -98,7 +98,8 @@ public WebAuthnData(dynamic o) try { Descriptor = o.Descriptor; - } catch + } + catch { // Handle newtonsoft parsing Descriptor = JsonConvert.DeserializeObject(o.Descriptor.ToString());