Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebAuthn #903

Merged
merged 23 commits into from Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1850c23
WIP
Hinton Aug 14, 2020
a6a3a7e
Registration mostly done
Hinton Aug 19, 2020
abd1468
Basic registration and login implemented
Hinton Aug 23, 2020
2d50a1d
Cleanup hardcoded urls, fix delete, prevent user from registering sam…
Hinton Aug 29, 2020
2d9d3da
Add support for old u2f tokens
Hinton Sep 1, 2020
1ffa1ad
Cleanup WebAuthnTokenProvider, set SignatureCounter
Hinton Sep 1, 2020
a4e6965
Cleanup UserService
Hinton Sep 1, 2020
750bdaf
Review comments
Hinton Sep 2, 2020
c16143f
Add migration for u2f data
Hinton Sep 9, 2020
a292619
Minor refactor
Hinton Sep 9, 2020
1f2c4e2
Rename ToWebAuthnKey to ToWebAuthnData
Hinton Sep 9, 2020
e014b86
Add WebAuthn connector to nginx config
Hinton Sep 11, 2020
5858d04
Remove old u2f routes and code.
Hinton Sep 11, 2020
66b1661
Merge branch 'master' of https://github.com/bitwarden/server into fea…
Hinton Sep 11, 2020
dc230df
Send the migrated flag to clients
Hinton Sep 13, 2020
891dd47
Fix format
Hinton Sep 20, 2020
be7ef84
Merge branch 'master' of https://github.com/bitwarden/server into fea…
Hinton Feb 18, 2021
210f9d5
Remove unessesary call to setTwoFactorProviders
Hinton Feb 18, 2021
603e0c6
Accidently removed `user.SetTwoFactorProviders`.
Hinton Mar 3, 2021
b367db1
Add webauthn-fallback-connector to nginx config
Hinton Mar 4, 2021
f6ae381
Set AttestationConveyancePreference to None to resolve TouchId not wo…
Hinton Mar 6, 2021
e440df9
Merge branch 'master' of https://github.com/bitwarden/server into fea…
Hinton Mar 17, 2021
6494a05
Fix review comments
Hinton Mar 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Admin/Startup.cs
Expand Up @@ -66,6 +66,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 =>
{
Expand Down
38 changes: 19 additions & 19 deletions src/Api/Controllers/TwoFactorController.cs
Expand Up @@ -14,6 +14,7 @@
using Bit.Core.Utilities;
using Bit.Core.Utilities.Duo;
using Bit.Core.Settings;
using Fido2NetLib;

namespace Bit.Api.Controllers
{
Expand Down Expand Up @@ -219,45 +220,44 @@ public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody]UpdateTwoFactorDuo
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;
}

Expand Down
10 changes: 10 additions & 0 deletions src/Api/Startup.cs
Expand Up @@ -18,6 +18,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System;

namespace Bit.Api
{
Expand Down Expand Up @@ -112,6 +113,15 @@ public void ConfigureServices(IServiceCollection services)
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 =>
{
Expand Down
1 change: 1 addition & 0 deletions src/Core/Core.csproj
Expand Up @@ -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" />
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Enums/TwoFactorProviderType.cs
Expand Up @@ -8,6 +8,7 @@ public enum TwoFactorProviderType : byte
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6
OrganizationDuo = 6,
WebAuthn = 7,
}
}
158 changes: 158 additions & 0 deletions src/Core/Identity/WebAuthnTokenProvider.cs
@@ -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);

Hinton marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
}
7 changes: 7 additions & 0 deletions src/Core/IdentityServer/BaseRequestValidator.cs
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -367,6 +368,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)))
Expand Down Expand Up @@ -394,6 +396,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)))
Expand Down Expand Up @@ -421,6 +424,10 @@ private Device GetDeviceFromRequest(ValidatedRequest request)
["Challenges"] = tokens != null && tokens.Length > 1 ? tokens[1] : null
};
}
else if (type == TwoFactorProviderType.WebAuthn)
{
return JsonSerializer.Deserialize<Dictionary<string, object>>(token);
Hinton marked this conversation as resolved.
Show resolved Hide resolved
}
else if (type == TwoFactorProviderType.Email)
{
return new Dictionary<string, object>
Expand Down
7 changes: 4 additions & 3 deletions 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;
Expand Down Expand Up @@ -223,14 +224,14 @@ public User ToUser(User extistingUser)
}
}

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; }
Expand Down