mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
WebAuthn (#903)
This commit is contained in:
parent
905b4b06da
commit
07f37d1f74
@ -66,6 +66,15 @@ namespace Bit.Admin
|
||||
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 =>
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Duo;
|
||||
using Bit.Core.Settings;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Api.Controllers
|
||||
{
|
||||
@ -219,45 +220,44 @@ namespace Bit.Api.Controllers
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("get-u2f")]
|
||||
public async Task<TwoFactorU2fResponseModel> GetU2f([FromBody]TwoFactorRequestModel model)
|
||||
[HttpPost("get-webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody]TwoFactorRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model.MasterPasswordHash, true);
|
||||
var response = new TwoFactorU2fResponseModel(user);
|
||||
var response = new TwoFactorWebAuthnResponseModel(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("get-u2f-challenge")]
|
||||
public async Task<TwoFactorU2fResponseModel.ChallengeModel> GetU2fChallenge(
|
||||
[FromBody]TwoFactorRequestModel model)
|
||||
[HttpPost("get-webauthn-challenge")]
|
||||
public async Task<CredentialCreateOptions> GetWebAuthnChallenge([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;
|
||||
var reg = await _userService.StartWebAuthnRegistrationAsync(user);
|
||||
return reg;
|
||||
}
|
||||
|
||||
[HttpPut("u2f")]
|
||||
[HttpPost("u2f")]
|
||||
public async Task<TwoFactorU2fResponseModel> PutU2f([FromBody]TwoFactorU2fRequestModel model)
|
||||
[HttpPut("webauthn")]
|
||||
[HttpPost("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody]TwoFactorWebAuthnRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model.MasterPasswordHash, true);
|
||||
var success = await _userService.CompleteU2fRegistrationAsync(
|
||||
|
||||
var success = await _userService.CompleteWebAuthRegistrationAsync(
|
||||
user, model.Id.Value, model.Name, model.DeviceResponse);
|
||||
if (!success)
|
||||
{
|
||||
throw new BadRequestException("Unable to complete U2F key registration.");
|
||||
throw new BadRequestException("Unable to complete WebAuthn registration.");
|
||||
}
|
||||
var response = new TwoFactorU2fResponseModel(user);
|
||||
var response = new TwoFactorWebAuthnResponseModel(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpDelete("u2f")]
|
||||
public async Task<TwoFactorU2fResponseModel> DeleteU2f([FromBody]TwoFactorU2fDeleteRequestModel model)
|
||||
[HttpDelete("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn([FromBody]TwoFactorWebAuthnDeleteRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model.MasterPasswordHash, true);
|
||||
await _userService.DeleteU2fKeyAsync(user, model.Id.Value);
|
||||
var response = new TwoFactorU2fResponseModel(user);
|
||||
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
|
||||
var response = new TwoFactorWebAuthnResponseModel(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Bit.Api
|
||||
{
|
||||
@ -112,6 +113,15 @@ namespace Bit.Api
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddCoreLocalizationServices();
|
||||
|
||||
// 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 =>
|
||||
{
|
||||
|
@ -26,6 +26,7 @@
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.3.103.15" />
|
||||
<PackageReference Include="Azure.Storage.Queues" Version="12.3.2" />
|
||||
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="1.1.0" />
|
||||
<PackageReference Include="Handlebars.Net" Version="1.10.1" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="MailKit" Version="2.8.0" />
|
||||
|
@ -8,6 +8,7 @@
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6
|
||||
OrganizationDuo = 6,
|
||||
WebAuthn = 7,
|
||||
}
|
||||
}
|
||||
|
158
src/Core/Identity/WebAuthnTokenProvider.cs
Normal file
158
src/Core/Identity/WebAuthnTokenProvider.cs
Normal file
@ -0,0 +1,158 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Fido2NetLib.Objects;
|
||||
using Fido2NetLib;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Identity
|
||||
{
|
||||
public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public WebAuthnTokenProvider(IServiceProvider serviceProvider, IFido2 fido2, GlobalSettings globalSettings)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_fido2 = fido2;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
if (!HasProperMetaData(webAuthnProvider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
var keys = LoadKeys(provider);
|
||||
var existingCredentials = keys.Select(key => key.Item2.Descriptor).ToList();
|
||||
|
||||
if (existingCredentials.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var exts = new AuthenticationExtensionsClientInputs()
|
||||
{
|
||||
UserVerificationIndex = true,
|
||||
UserVerificationMethod = true,
|
||||
AppID = CoreHelpers.U2fAppIdUrl(_globalSettings),
|
||||
};
|
||||
|
||||
var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred, exts);
|
||||
|
||||
provider.MetaData["login"] = options;
|
||||
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
providers[TwoFactorProviderType.WebAuthn] = provider;
|
||||
user.SetTwoFactorProviders(providers);
|
||||
await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
|
||||
|
||||
return options.ToJson();
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)) || string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
var keys = LoadKeys(provider);
|
||||
|
||||
if (!provider.MetaData.ContainsKey("login"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var clientResponse = JsonConvert.DeserializeObject<AuthenticatorAssertionRawResponse>(token);
|
||||
|
||||
var jsonOptions = provider.MetaData["login"].ToString();
|
||||
var options = AssertionOptions.FromJson(jsonOptions);
|
||||
|
||||
var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id));
|
||||
|
||||
if (webAuthCred == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IsUserHandleOwnerOfCredentialIdAsync callback = (args) => Task.FromResult(true);
|
||||
|
||||
var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback);
|
||||
|
||||
provider.MetaData.Remove("login");
|
||||
|
||||
// Update SignatureCounter
|
||||
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);
|
||||
|
||||
return res.Status == "ok";
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData?.Any() ?? false;
|
||||
}
|
||||
|
||||
private List<Tuple<string, TwoFactorProvider.WebAuthnData>> LoadKeys(TwoFactorProvider provider)
|
||||
{
|
||||
var keys = new List<Tuple<string, TwoFactorProvider.WebAuthnData>>();
|
||||
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<string, TwoFactorProvider.WebAuthnData>(keyName, key));
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Services;
|
||||
using System.Linq;
|
||||
@ -367,6 +368,7 @@ namespace Bit.Core.IdentityServer
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
case TwoFactorProviderType.U2f:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Remember:
|
||||
if (type != TwoFactorProviderType.Remember &&
|
||||
!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
||||
@ -394,6 +396,7 @@ namespace Bit.Core.IdentityServer
|
||||
{
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.U2f:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Email:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
if (!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
||||
@ -421,6 +424,10 @@ namespace Bit.Core.IdentityServer
|
||||
["Challenges"] = tokens != null && tokens.Length > 1 ? tokens[1] : null
|
||||
};
|
||||
}
|
||||
else if (type == TwoFactorProviderType.WebAuthn)
|
||||
{
|
||||
return JsonSerializer.Deserialize<Dictionary<string, object>>(token);
|
||||
}
|
||||
else if (type == TwoFactorProviderType.Email)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
|
@ -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,14 +224,14 @@ namespace Bit.Core.Models.Api
|
||||
}
|
||||
}
|
||||
|
||||
public class TwoFactorU2fRequestModel : TwoFactorU2fDeleteRequestModel
|
||||
public class TwoFactorWebAuthnRequestModel : TwoFactorWebAuthnDeleteRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string DeviceResponse { get; set; }
|
||||
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class TwoFactorU2fDeleteRequestModel : TwoFactorRequestModel, IValidatableObject
|
||||
public class TwoFactorWebAuthnDeleteRequestModel : TwoFactorRequestModel, IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public int? Id { get; set; }
|
||||
|
@ -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<KeyModel> 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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
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 TwoFactorWebAuthnResponseModel : ResponseModel
|
||||
{
|
||||
public TwoFactorWebAuthnResponseModel(User user)
|
||||
: base("twoFactorWebAuthn")
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
Enabled = provider?.Enabled ?? false;
|
||||
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; }
|
||||
public IEnumerable<KeyModel> Keys { get; set; }
|
||||
|
||||
public class KeyModel
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,6 +1,11 @@
|
||||
using Bit.Core.Enums;
|
||||
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
|
||||
@ -39,6 +44,84 @@ namespace Bit.Core.Models
|
||||
string.IsNullOrWhiteSpace(Certificate) ? null : Utils.Base64StringToByteArray(Certificate);
|
||||
public uint Counter { get; set; }
|
||||
public bool Compromised { get; set; }
|
||||
|
||||
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 WebAuthnData ToWebAuthnData()
|
||||
{
|
||||
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
|
||||
{
|
||||
public WebAuthnData() { }
|
||||
|
||||
public WebAuthnData(dynamic o)
|
||||
{
|
||||
Name = o.Name;
|
||||
try
|
||||
{
|
||||
Descriptor = o.Descriptor;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Handle newtonsoft parsing
|
||||
Descriptor = JsonConvert.DeserializeObject<PublicKeyCredentialDescriptor>(o.Descriptor.ToString());
|
||||
}
|
||||
PublicKey = o.PublicKey;
|
||||
UserHandle = o.UserHandle;
|
||||
SignatureCounter = o.SignatureCounter;
|
||||
CredType = o.CredType;
|
||||
RegDate = o.RegDate;
|
||||
AaGuid = o.AaGuid;
|
||||
Migrated = o.Migrated;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public PublicKeyCredentialDescriptor Descriptor { get; internal set; }
|
||||
public byte[] PublicKey { get; internal set; }
|
||||
public byte[] UserHandle { 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 bool Migrated { get; internal set; }
|
||||
}
|
||||
|
||||
public static bool RequiresPremium(TwoFactorProviderType type)
|
||||
|
@ -7,6 +7,7 @@ using System.Security.Claims;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Business;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -23,9 +24,9 @@ namespace Bit.Core.Services
|
||||
Task SendMasterPasswordHintAsync(string email);
|
||||
Task SendTwoFactorEmailAsync(User user);
|
||||
Task<bool> VerifyTwoFactorEmailAsync(User user, string token);
|
||||
Task<U2fRegistration> StartU2fRegistrationAsync(User user);
|
||||
Task<bool> DeleteU2fKeyAsync(User user, int id);
|
||||
Task<bool> CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse);
|
||||
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
|
||||
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
|
||||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
||||
Task SendEmailVerificationAsync(User user);
|
||||
Task<IdentityResult> ConfirmEmailAsync(User user, string token);
|
||||
Task InitiateEmailChangeAsync(User user, string newEmail);
|
||||
@ -38,7 +39,7 @@ namespace Bit.Core.Services
|
||||
Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
|
||||
IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||
Task<IdentityResult> 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<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode,
|
||||
|
@ -21,7 +21,8 @@ using Bit.Core.Settings;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using U2F.Core.Exceptions;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -34,7 +35,6 @@ namespace Bit.Core.Services
|
||||
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;
|
||||
@ -48,6 +48,7 @@ namespace Bit.Core.Services
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IDataProtector _organizationServiceDataProtector;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
@ -57,7 +58,6 @@ namespace Bit.Core.Services
|
||||
ICipherRepository cipherRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IU2fRepository u2fRepository,
|
||||
IMailService mailService,
|
||||
IPushNotificationService pushService,
|
||||
IUserStore<User> store,
|
||||
@ -76,6 +76,7 @@ namespace Bit.Core.Services
|
||||
IPaymentService paymentService,
|
||||
IPolicyRepository policyRepository,
|
||||
IReferenceEventService referenceEventService,
|
||||
IFido2 fido2,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IOrganizationService organizationService)
|
||||
@ -94,7 +95,6 @@ namespace Bit.Core.Services
|
||||
_cipherRepository = cipherRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_u2fRepository = u2fRepository;
|
||||
_mailService = mailService;
|
||||
_pushService = pushService;
|
||||
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
|
||||
@ -109,6 +109,7 @@ namespace Bit.Core.Services
|
||||
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
|
||||
"OrganizationServiceDataProtector");
|
||||
_referenceEventService = referenceEventService;
|
||||
_fido2 = fido2;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationService = organizationService;
|
||||
@ -362,107 +363,88 @@ namespace Bit.Core.Services
|
||||
"2faEmail:" + email, token);
|
||||
}
|
||||
|
||||
public async Task<U2fRegistration> StartU2fRegistrationAsync(User user)
|
||||
public async Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user)
|
||||
{
|
||||
await _u2fRepository.DeleteManyByUserIdAsync(user.Id);
|
||||
var reg = U2fLib.StartRegistration(CoreHelpers.U2fAppIdUrl(_globalSettings));
|
||||
await _u2fRepository.CreateAsync(new U2f
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
if (providers == null)
|
||||
{
|
||||
AppId = reg.AppId,
|
||||
Challenge = reg.Challenge,
|
||||
Version = reg.Version,
|
||||
UserId = user.Id,
|
||||
CreationDate = DateTime.UtcNow
|
||||
});
|
||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
}
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
if (provider == null)
|
||||
{
|
||||
provider = new TwoFactorProvider
|
||||
{
|
||||
Enabled = false
|
||||
};
|
||||
}
|
||||
if (provider.MetaData == null)
|
||||
{
|
||||
provider.MetaData = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
return new U2fRegistration
|
||||
var fidoUser = new Fido2User
|
||||
{
|
||||
AppId = reg.AppId,
|
||||
Challenge = reg.Challenge,
|
||||
Version = reg.Version
|
||||
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)
|
||||
.ToList();
|
||||
|
||||
var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.None);
|
||||
|
||||
provider.MetaData["pending"] = options.ToJson();
|
||||
providers[TwoFactorProviderType.WebAuthn] = provider;
|
||||
user.SetTwoFactorProviders(providers);
|
||||
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public async Task<bool> CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse)
|
||||
public async Task<bool> CompleteWebAuthRegistrationAsync(User user, int id, string name, AuthenticatorAttestationRawResponse attestationResponse)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceResponse))
|
||||
var keyId = $"Key{id}";
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
if (!provider?.MetaData?.ContainsKey("pending") ?? true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var challenges = await _u2fRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (!challenges?.Any() ?? true)
|
||||
var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]);
|
||||
|
||||
// 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);
|
||||
|
||||
provider.MetaData.Remove("pending");
|
||||
provider.MetaData[keyId] = new TwoFactorProvider.WebAuthnData
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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 registerResponse = BaseModel.FromJson<RegisterResponse>(deviceResponse);
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
providers[TwoFactorProviderType.WebAuthn] = provider;
|
||||
user.SetTwoFactorProviders(providers);
|
||||
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
|
||||
|
||||
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<TwoFactorProviderType, TwoFactorProvider>();
|
||||
}
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f);
|
||||
if (provider == null)
|
||||
{
|
||||
provider = new TwoFactorProvider();
|
||||
}
|
||||
if (provider.MetaData == null)
|
||||
{
|
||||
provider.MetaData = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteU2fKeyAsync(User user, int id)
|
||||
public async Task<bool> DeleteWebAuthnKeyAsync(User user, int id)
|
||||
{
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
if (providers == null)
|
||||
@ -471,7 +453,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
var keyName = $"Key{id}";
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f);
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
if (!provider?.MetaData?.ContainsKey(keyName) ?? true)
|
||||
{
|
||||
return false;
|
||||
@ -483,9 +465,9 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
provider.MetaData.Remove(keyName);
|
||||
providers[TwoFactorProviderType.U2f] = provider;
|
||||
providers[TwoFactorProviderType.WebAuthn] = provider;
|
||||
user.SetTwoFactorProviders(providers);
|
||||
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f);
|
||||
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -703,9 +685,9 @@ namespace Bit.Core.Services
|
||||
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);
|
||||
}
|
||||
@ -1158,7 +1140,7 @@ namespace Bit.Core.Services
|
||||
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)
|
||||
@ -1166,7 +1148,10 @@ namespace Bit.Core.Services
|
||||
return;
|
||||
}
|
||||
|
||||
providers[type].Enabled = true;
|
||||
if (setEnabled)
|
||||
{
|
||||
providers[type].Enabled = true;
|
||||
}
|
||||
user.SetTwoFactorProviders(providers);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode))
|
||||
|
@ -294,7 +294,9 @@ namespace Bit.Core.Utilities
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.U2f))
|
||||
.AddTokenProvider<TwoFactorRememberTokenProvider>(
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember))
|
||||
.AddTokenProvider<EmailTokenProvider<User>>(TokenOptions.DefaultEmailProvider);
|
||||
.AddTokenProvider<EmailTokenProvider<User>>(TokenOptions.DefaultEmailProvider)
|
||||
.AddTokenProvider<WebAuthnTokenProvider>(
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.WebAuthn));
|
||||
|
||||
return identityBuilder;
|
||||
}
|
||||
|
@ -56,6 +56,15 @@ namespace Bit.Identity
|
||||
// Caching
|
||||
services.AddMemoryCache();
|
||||
|
||||
// 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();
|
||||
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Fido2NetLib;
|
||||
using Bit.Core.Context;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
@ -22,7 +23,6 @@ namespace Bit.Core.Test.Services
|
||||
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<User> _userStore;
|
||||
@ -41,6 +41,7 @@ namespace Bit.Core.Test.Services
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly CurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
@ -51,7 +52,6 @@ namespace Bit.Core.Test.Services
|
||||
_cipherRepository = Substitute.For<ICipherRepository>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_u2fRepository = Substitute.For<IU2fRepository>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_pushService = Substitute.For<IPushNotificationService>();
|
||||
_userStore = Substitute.For<IUserStore<User>>();
|
||||
@ -70,6 +70,7 @@ namespace Bit.Core.Test.Services
|
||||
_paymentService = Substitute.For<IPaymentService>();
|
||||
_policyRepository = Substitute.For<IPolicyRepository>();
|
||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
_fido2 = Substitute.For<IFido2>();
|
||||
_currentContext = new CurrentContext();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_organizationService = Substitute.For<IOrganizationService>();
|
||||
@ -79,7 +80,6 @@ namespace Bit.Core.Test.Services
|
||||
_cipherRepository,
|
||||
_organizationUserRepository,
|
||||
_organizationRepository,
|
||||
_u2fRepository,
|
||||
_mailService,
|
||||
_pushService,
|
||||
_userStore,
|
||||
@ -98,6 +98,7 @@ namespace Bit.Core.Test.Services
|
||||
_paymentService,
|
||||
_policyRepository,
|
||||
_referenceEventService,
|
||||
_fido2,
|
||||
_currentContext,
|
||||
_globalSettings,
|
||||
_organizationService
|
||||
|
@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using DbUp.Engine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Migrator.DbScripts
|
||||
{
|
||||
class ScriptMigrateU2FToWebAuthn : IScript
|
||||
{
|
||||
|
||||
public string ProvideScript(Func<IDbCommand> commandFactory)
|
||||
{
|
||||
var cmd = commandFactory();
|
||||
cmd.CommandText = "SELECT Id, TwoFactorProviders FROM [dbo].[User] WHERE TwoFactorProviders IS NOT NULL";
|
||||
|
||||
var users = new List<object>();
|
||||
|
||||
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<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(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.ToWebAuthnData()));
|
||||
|
||||
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<Tuple<string, TwoFactorProvider.U2fMetaData>> LoadKeys(TwoFactorProvider provider)
|
||||
{
|
||||
var keys = new List<Tuple<string, TwoFactorProvider.U2fMetaData>>();
|
||||
|
||||
// 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<string, TwoFactorProvider.U2fMetaData>(keyName, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
private class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string TwoFactorProviders { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -92,6 +92,14 @@ server {
|
||||
proxy_pass http://web:5000/u2f-connector.html;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user