1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

initial commit of source

This commit is contained in:
Kyle Spearrin
2015-12-08 22:57:38 -05:00
commit 437b971003
87 changed files with 3819 additions and 0 deletions

18
src/Core/Core.xproj Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>3973d21b-a692-4b60-9b70-3631c057423a</ProjectGuid>
<RootNamespace>Bit.Core</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core
{
public class CurrentContext
{
public virtual User User { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using Newtonsoft.Json;
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public abstract class Cipher : IDataObject
{
internal static string TypeValue = "cipher";
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("type")]
public string Type { get; private set; } = TypeValue;
public abstract CipherType CipherType { get; protected set; }
public string UserId { get; set; }
public string Name { get; set; }
public bool Dirty { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,9 @@
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public class Folder : Cipher, IDataObject
{
public override CipherType CipherType { get; protected set; } = CipherType.Folder;
}
}

10
src/Core/Domains/Role.cs Normal file
View File

@ -0,0 +1,10 @@
namespace Bit.Core.Domains
{
/// <summary>
/// This class is not used. It is implemented to make the Identity provider happy.
/// </summary>
public class Role
{
public string Name { get; set; }
}
}

16
src/Core/Domains/Site.cs Normal file
View File

@ -0,0 +1,16 @@
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public class Site : Cipher, IDataObject
{
public override CipherType CipherType { get; protected set; } = CipherType.Site;
public string FolderId { get; set; }
public string Uri { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Notes { get; set; }
}
}

30
src/Core/Domains/User.cs Normal file
View File

@ -0,0 +1,30 @@
using System;
using Newtonsoft.Json;
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public class User : IDataObject
{
internal static string TypeValue = "user";
[JsonProperty("id")]
public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("type")]
public string Type { get; private set; } = TypeValue;
public string Name { get; set; }
public string Email { get; set; }
public string MasterPassword { get; set; }
public string MasterPasswordHint { get; set; }
public string Culture { get; set; }
public string SecurityStamp { get; set; }
public string OldEmail { get; set; }
public string OldMasterPassword { get; set; }
public bool TwoFactorEnabled { get; set; }
public TwoFactorProvider? TwoFactorProvider { get; set; }
public string AuthenticatorKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum CipherType
{
Folder = 0,
Site = 1
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums
{
public enum TwoFactorProvider
{
Authenticator = 0
}
}

View File

@ -0,0 +1,30 @@
using System;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Bit.Core.Exceptions
{
public class BadRequestException : Exception
{
public BadRequestException(string message) : this(string.Empty, message) { }
public BadRequestException(string key, string errorMessage)
: base("The model state is invalid.")
{
ModelState = new ModelStateDictionary();
ModelState.AddModelError(key, errorMessage);
}
public BadRequestException(ModelStateDictionary modelState)
: base("The model state is invalid.")
{
if(modelState.IsValid || modelState.ErrorCount == 0)
{
return;
}
ModelState = modelState;
}
public ModelStateDictionary ModelState { get; set; }
}
}

View File

@ -0,0 +1,6 @@
using System;
namespace Bit.Core.Exceptions
{
public class NotFoundException : Exception { }
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
namespace Bit.Core
{
public class GlobalSettings
{
public string SiteName { get; set; }
public string BaseVaultUri { get; set; }
public virtual DocumentDBSettings DocumentDB { get; set; } = new DocumentDBSettings();
public virtual MailSettings Mail { get; set; } = new MailSettings();
public class DocumentDBSettings
{
public string Uri { get; set; }
public string Key { get; set; }
public string DatabaseId { get; set; }
public string CollectionIdPrefix { get; set; }
public int NumberOfCollections { get; set; }
}
public class MailSettings
{
public string APIKey { get; set; }
public string ReplyToEmail { get; set; }
}
}
}

12
src/Core/IDataObject.cs Normal file
View File

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Bit.Core
{
public interface IDataObject
{
[JsonProperty("id")]
string Id { get; set; }
[JsonProperty("type")]
string Type { get; }
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;
using Base32;
using Microsoft.AspNet.Identity;
using Bit.Core.Domains;
using Bit.Core.Enums;
using OtpSharp;
namespace Bit.Core.Identity
{
public class AuthenticatorTokenProvider : IUserTokenProvider<User>
{
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{
var canGenerate = user.TwoFactorEnabled
&& user.TwoFactorProvider.HasValue
&& user.TwoFactorProvider.Value == TwoFactorProvider.Authenticator
&& !string.IsNullOrWhiteSpace(user.AuthenticatorKey);
return Task.FromResult(canGenerate);
}
public Task<string> GetUserModifierAsync(string purpose, UserManager<User> manager, User user)
{
return Task.FromResult<string>(null);
}
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
{
return Task.FromResult<string>(null);
}
public Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
{
var otp = new Totp(Base32Encoder.Decode(user.AuthenticatorKey));
long timeStepMatched;
var valid = otp.VerifyTotp(token, out timeStepMatched, new VerificationWindow(2, 2));
return Task.FromResult(valid);
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.OptionsModel;
using Bit.Core.Domains;
using Microsoft.AspNet.Authentication.JwtBearer;
namespace Bit.Core.Identity
{
public static class JwtBearerBuilderExtensions
{
public static IApplicationBuilder UseJwtBearerIdentity(this IApplicationBuilder app)
{
if(app == null)
{
throw new ArgumentNullException(nameof(app));
}
var marker = app.ApplicationServices.GetService<IdentityMarkerService>();
if(marker == null)
{
throw new InvalidOperationException("Must Call AddJwtBearerIdentity");
}
var jwtOptions = app.ApplicationServices.GetRequiredService<IOptions<JwtBearerIdentityOptions>>().Value;
var jwtSignInManager = app.ApplicationServices.GetRequiredService<JwtBearerSignInManager>();
app.UseJwtBearerAuthentication(options =>
{
// Basic settings - signing key to validate with, audience and issuer.
//options.TokenValidationParameters.IssuerSigningKey = key;
options.TokenValidationParameters.ValidAudience = jwtOptions.Audience;
options.TokenValidationParameters.ValidIssuer = jwtOptions.Issuer;
options.TokenValidationParameters.RequireExpirationTime = true;
options.TokenValidationParameters.RequireSignedTokens = false;
// When receiving a token, check that we've signed it.
options.TokenValidationParameters.ValidateSignature = false;
//// When receiving a token, check that it is still valid.
options.TokenValidationParameters.ValidateLifetime = true;
// This defines the maximum allowable clock skew - i.e. provides a tolerance on the token expiry time
// when validating the lifetime. As we're creating the tokens locally and validating them on the same
// machines which should have synchronised time, this can be set to zero. Where external tokens are
// used, some leeway here could be useful.
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(0);
options.Events = new JwtBearerEvents
{
OnValidatedToken = JwtBearerEventImplementations.ValidatedTokenAsync,
OnAuthenticationFailed = JwtBearerEventImplementations.AuthenticationFailedAsync
};
});
return app;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using System.IdentityModel.Tokens;
using Bit.Core.Repositories;
using Microsoft.AspNet.Authentication;
using Microsoft.AspNet.Http.Authentication;
namespace Bit.Core.Identity
{
public static class JwtBearerEventImplementations
{
public async static Task ValidatedTokenAsync(ValidatedTokenContext context)
{
if(context.HttpContext.RequestServices == null)
{
throw new InvalidOperationException("RequestServices is null");
}
var userRepository = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
var manager = context.HttpContext.RequestServices.GetRequiredService<JwtBearerSignInManager>();
var userId = context.AuthenticationTicket.Principal.GetUserId();
var user = await userRepository.GetByIdAsync(userId);
// validate security token
if(!await manager.ValidateSecurityStampAsync(user, context.AuthenticationTicket.Principal))
{
throw new SecurityTokenValidationException("Bad security stamp.");
}
// register the current context user
var currentContext = context.HttpContext.RequestServices.GetRequiredService<CurrentContext>();
currentContext.User = user;
}
public static Task AuthenticationFailedAsync(AuthenticationFailedContext context)
{
if(!context.HttpContext.User.Identity.IsAuthenticated)
{
context.State = EventResultState.HandledResponse;
context.AuthenticationTicket = new AuthenticationTicket(context.HttpContext.User, new AuthenticationProperties(), context.Options.AuthenticationScheme);
}
return Task.FromResult<object>(null);
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.IdentityModel.Tokens;
namespace Bit.Core.Identity
{
public class JwtBearerIdentityOptions
{
public string Audience { get; set; }
public string Issuer { get; set; }
public SigningCredentials SigningCredentials { get; set; }
public TimeSpan? TokenLifetime { get; set; }
public TimeSpan? TwoFactorTokenLifetime { get; set; }
public string AuthenticationMethod { get; set; } = "Application";
public string TwoFactorAuthenticationMethod { get; set; } = "TwoFactor";
}
}

View File

@ -0,0 +1,54 @@
using System;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Core.Domains;
namespace Bit.Core.Identity
{
public static class JwtBearerIdentityServiceCollectionExtensions
{
public static IdentityBuilder AddJwtBearerIdentit(
this IServiceCollection services)
{
return services.AddJwtBearerIdentity(setupAction: null, jwtBearerSetupAction: null);
}
public static IdentityBuilder AddJwtBearerIdentity(
this IServiceCollection services,
Action<IdentityOptions> setupAction,
Action<JwtBearerIdentityOptions> jwtBearerSetupAction)
{
// Services used by identity
services.AddOptions();
services.AddAuthentication();
// Identity services
services.TryAddSingleton<IdentityMarkerService>();
services.TryAddScoped<IUserValidator<User>, UserValidator<User>>();
services.TryAddScoped<IPasswordValidator<User>, PasswordValidator<User>>();
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<Role>, RoleValidator<Role>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<User>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<User>, UserClaimsPrincipalFactory<User, Role>>();
services.TryAddScoped<UserManager<User>, UserManager<User>>();
services.TryAddScoped<JwtBearerSignInManager, JwtBearerSignInManager>();
services.TryAddScoped<RoleManager<Role>, RoleManager<Role>>();
if(setupAction != null)
{
services.Configure(setupAction);
}
if(jwtBearerSetupAction != null)
{
services.Configure(jwtBearerSetupAction);
}
return new IdentityBuilder(typeof(User), typeof(Role), services);
}
}
}

View File

@ -0,0 +1,160 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Authentication.JwtBearer;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.OptionsModel;
using Bit.Core.Domains;
namespace Bit.Core.Identity
{
public class JwtBearerSignInManager
{
public JwtBearerSignInManager(
UserManager<User> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<User> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
IOptions<JwtBearerIdentityOptions> jwtIdentityOptionsAccessor,
IOptions<JwtBearerOptions> jwtOptionsAccessor,
ILogger<JwtBearerSignInManager> logger)
{
UserManager = userManager;
Context = contextAccessor.HttpContext;
ClaimsFactory = claimsFactory;
IdentityOptions = optionsAccessor?.Value ?? new IdentityOptions();
JwtIdentityOptions = jwtIdentityOptionsAccessor?.Value ?? new JwtBearerIdentityOptions();
JwtBearerOptions = jwtOptionsAccessor?.Value ?? new JwtBearerOptions();
}
internal UserManager<User> UserManager { get; set; }
internal HttpContext Context { get; set; }
internal IUserClaimsPrincipalFactory<User> ClaimsFactory { get; set; }
internal IdentityOptions IdentityOptions { get; set; }
internal JwtBearerIdentityOptions JwtIdentityOptions { get; set; }
internal JwtBearerOptions JwtBearerOptions { get; set; }
public async Task<ClaimsPrincipal> CreateUserPrincipalAsync(User user) => await ClaimsFactory.CreateAsync(user);
public Task<bool> ValidateSecurityStampAsync(User user, ClaimsPrincipal principal)
{
if(user != null && UserManager.SupportsUserSecurityStamp)
{
var securityStamp = principal.FindFirstValue(IdentityOptions.ClaimsIdentity.SecurityStampClaimType);
if(securityStamp == user.SecurityStamp)
{
return Task.FromResult(true);
}
}
return Task.FromResult(false);
}
public async Task<JwtBearerSignInResult> PasswordSignInAsync(User user, string password)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(await UserManager.CheckPasswordAsync(user, password))
{
return await SignInOrTwoFactorAsync(user);
}
return JwtBearerSignInResult.Failed;
}
public async Task<JwtBearerSignInResult> PasswordSignInAsync(string userName, string password)
{
var user = await UserManager.FindByNameAsync(userName);
if(user == null)
{
return JwtBearerSignInResult.Failed;
}
return await PasswordSignInAsync(user, password);
}
public async Task<JwtBearerSignInResult> TwoFactorSignInAsync(User user, string provider, string code)
{
if(user == null)
{
return JwtBearerSignInResult.Failed;
}
if(await UserManager.VerifyTwoFactorTokenAsync(user, provider, code))
{
var token = await SignInAsync(user, false);
var success = JwtBearerSignInResult.Success;
success.Token = token;
success.User = user;
return success;
}
return JwtBearerSignInResult.Failed;
}
private async Task<string> SignInAsync(User user, bool twoFactor)
{
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
DateTime? tokenExpiration = null;
var userPrincipal = await CreateUserPrincipalAsync(user);
if(twoFactor)
{
userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, JwtIdentityOptions.TwoFactorAuthenticationMethod));
if(JwtIdentityOptions.TwoFactorTokenLifetime.HasValue)
{
tokenExpiration = DateTime.UtcNow.Add(JwtIdentityOptions.TwoFactorTokenLifetime.Value);
}
}
else
{
userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, JwtIdentityOptions.AuthenticationMethod));
if(JwtIdentityOptions.TokenLifetime.HasValue)
{
tokenExpiration = DateTime.UtcNow.Add(JwtIdentityOptions.TokenLifetime.Value);
}
}
var securityToken = handler.CreateToken(
issuer: JwtIdentityOptions.Issuer,
audience: JwtIdentityOptions.Audience,
signingCredentials: JwtIdentityOptions.SigningCredentials,
subject: userPrincipal.Identities.First(),
expires: tokenExpiration);
return handler.WriteToken(securityToken);
}
private async Task<JwtBearerSignInResult> SignInOrTwoFactorAsync(User user)
{
if(UserManager.SupportsUserTwoFactor &&
await UserManager.GetTwoFactorEnabledAsync(user) &&
(await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0)
{
var twoFactorToken = await SignInAsync(user, true);
var twoFactorResult = JwtBearerSignInResult.TwoFactorRequired;
twoFactorResult.Token = twoFactorToken;
twoFactorResult.User = user;
return twoFactorResult;
}
var token = await SignInAsync(user, false);
var result = JwtBearerSignInResult.Success;
result.Token = token;
result.User = user;
return result;
}
}
}

View File

@ -0,0 +1,34 @@
using Bit.Core.Domains;
namespace Bit.Core.Identity
{
public class JwtBearerSignInResult
{
private static readonly JwtBearerSignInResult _success = new JwtBearerSignInResult { Succeeded = true };
private static readonly JwtBearerSignInResult _failed = new JwtBearerSignInResult();
private static readonly JwtBearerSignInResult _lockedOut = new JwtBearerSignInResult { IsLockedOut = true };
private static readonly JwtBearerSignInResult _notAllowed = new JwtBearerSignInResult { IsNotAllowed = true };
private static readonly JwtBearerSignInResult _twoFactorRequired = new JwtBearerSignInResult { RequiresTwoFactor = true };
public bool Succeeded { get; protected set; }
public bool IsLockedOut { get; protected set; }
public bool IsNotAllowed { get; protected set; }
public bool RequiresTwoFactor { get; protected set; }
public string Token { get; set; }
public User User { get; set; }
public static JwtBearerSignInResult Success => _success;
public static JwtBearerSignInResult Failed => _failed;
public static JwtBearerSignInResult LockedOut => _lockedOut;
public static JwtBearerSignInResult NotAllowed => _notAllowed;
public static JwtBearerSignInResult TwoFactorRequired => _twoFactorRequired;
public override string ToString()
{
return IsLockedOut ? "Lockedout" :
IsNotAllowed ? "NotAllowed" :
RequiresTwoFactor ? "RequiresTwoFactor" :
Succeeded ? "Succeeded" : "Failed";
}
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNet.Identity;
namespace Bit.Core.Identity
{
public class LowerInvariantLookupNormalizer : ILookupNormalizer
{
public string Normalize(string key)
{
return key?.Normalize().ToLowerInvariant();
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Bit.Core.Domains;
namespace Bit.Core.Identity
{
public class RoleStore : IRoleStore<Role>
{
public void Dispose() { }
public Task<IdentityResult> CreateAsync(Role role, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<IdentityResult> DeleteAsync(Role role, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<Role> FindByIdAsync(string roleId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<Role> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<string> GetNormalizedRoleNameAsync(Role role, CancellationToken cancellationToken)
{
return Task.FromResult(role.Name);
}
public Task<string> GetRoleIdAsync(Role role, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<string> GetRoleNameAsync(Role role, CancellationToken cancellationToken)
{
return Task.FromResult(role.Name);
}
public Task SetNormalizedRoleNameAsync(Role role, string normalizedName, CancellationToken cancellationToken)
{
return Task.FromResult<object>(null);
}
public Task SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken)
{
role.Name = roleName;
return Task.FromResult<object>(null);
}
public Task<IdentityResult> UpdateAsync(Role role, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Bit.Core.Domains;
using Bit.Core.Repositories;
namespace Bit.Core.Identity
{
public class UserStore :
IUserStore<User>,
IUserPasswordStore<User>,
IUserEmailStore<User>,
IUserTwoFactorStore<User>,
IUserSecurityStampStore<User>
{
private readonly IUserRepository _userRepository;
public UserStore(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public void Dispose() { }
public async Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
await _userRepository.CreateAsync(user);
return IdentityResult.Success;
}
public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
await _userRepository.DeleteAsync(user);
return IdentityResult.Success;
}
public async Task<User> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{
return await _userRepository.GetByEmailAsync(normalizedEmail);
}
public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{
return await _userRepository.GetByIdAsync(userId);
}
public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
{
return await _userRepository.GetByEmailAsync(normalizedUserName);
}
public Task<string> GetEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.Email);
}
public Task<bool> GetEmailConfirmedAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(true); // all emails are confirmed
}
public Task<string> GetNormalizedEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.Email);
}
public Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.Email);
}
public Task<string> GetPasswordHashAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.MasterPassword);
}
public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.Id);
}
public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.Email);
}
public Task<bool> HasPasswordAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(!string.IsNullOrWhiteSpace(user.MasterPassword));
}
public Task SetEmailAsync(User user, string email, CancellationToken cancellationToken = default(CancellationToken))
{
user.Email = email;
return Task.FromResult<object>(null);
}
public Task SetEmailConfirmedAsync(User user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
// do nothing
return Task.FromResult<object>(null);
}
public Task SetNormalizedEmailAsync(User user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{
user.Email = normalizedEmail;
return Task.FromResult<object>(null);
}
public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
user.Email = normalizedName;
return Task.FromResult<object>(null);
}
public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
{
user.MasterPassword = passwordHash;
return Task.FromResult<object>(null);
}
public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken = default(CancellationToken))
{
user.Email = userName;
return Task.FromResult<object>(null);
}
public async Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{
await _userRepository.ReplaceAsync(user);
return IdentityResult.Success;
}
public Task SetTwoFactorEnabledAsync(User user, bool enabled, CancellationToken cancellationToken)
{
user.TwoFactorEnabled = enabled;
return Task.FromResult<object>(null);
}
public Task<bool> GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken)
{
return Task.FromResult(user.TwoFactorEnabled && user.TwoFactorProvider.HasValue);
}
public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken)
{
user.SecurityStamp = stamp;
return Task.FromResult<object>(null);
}
public Task<string> GetSecurityStampAsync(User user, CancellationToken cancellationToken)
{
return Task.FromResult(user.SecurityStamp);
}
}
}

View File

@ -0,0 +1,23 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Bit.Core")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("bitwarden")]
[assembly: AssemblyProduct("bitwarden")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("3973d21b-a692-4b60-9b70-3631c057423a")]

View File

@ -0,0 +1,67 @@
using System;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB
{
public abstract class BaseRepository<T> where T : IDataObject
{
public BaseRepository(DocumentClient client, string databaseId, string documentType = null)
{
Client = client;
DatabaseId = databaseId;
DatabaseUri = UriFactory.CreateDatabaseUri(databaseId);
PartitionResolver = client.PartitionResolvers[DatabaseUri.OriginalString];
if(string.IsNullOrWhiteSpace(documentType))
{
DocumentType = typeof(T).Name.ToLower();
}
else
{
DocumentType = documentType;
}
}
protected DocumentClient Client { get; private set; }
protected string DatabaseId { get; private set; }
protected Uri DatabaseUri { get; private set; }
protected IPartitionResolver PartitionResolver { get; private set; }
protected string DocumentType { get; private set; }
protected string ResolveSprocIdLink(T obj, string sprocId)
{
return string.Format("{0}/sprocs/{1}", ResolveCollectionIdLink(obj), sprocId);
}
protected string ResolveSprocIdLink(string partitionKey, string sprocId)
{
return string.Format("{0}/sprocs/{1}", ResolveCollectionIdLink(partitionKey), sprocId);
}
protected string ResolveDocumentIdLink(T obj)
{
return string.Format("{0}/docs/{1}", ResolveCollectionIdLink(obj), obj.Id);
}
protected string ResolveDocumentIdLink(string id)
{
return ResolveDocumentIdLink(id, id);
}
protected string ResolveDocumentIdLink(string partitionKey, string id)
{
return string.Format("{0}/docs/{1}", ResolveCollectionIdLink(partitionKey), id);
}
protected string ResolveCollectionIdLink(T obj)
{
var partitionKey = PartitionResolver.GetPartitionKey(obj);
return ResolveCollectionIdLink(partitionKey);
}
protected string ResolveCollectionIdLink(object partitionKey)
{
return PartitionResolver.ResolveForCreate(partitionKey);
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains;
namespace Bit.Core.Repositories.DocumentDB
{
public class CipherRepository : BaseRepository<Cipher>, ICipherRepository
{
public CipherRepository(DocumentClient client, string databaseId, string documentType = null)
: base(client, databaseId, documentType)
{ }
public async Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers)
{
// Make sure we are dealing with cipher types since we accept any via dynamic.
var cleanedCiphers = ciphers.Where(c => c is Cipher);
if(cleanedCiphers.Count() == 0)
{
return;
}
var userId = ((Cipher)cleanedCiphers.First()).UserId;
StoredProcedureResponse<int> sprocResponse = await Client.ExecuteStoredProcedureAsync<int>(
ResolveSprocIdLink(userId, "bulkUpdateDirtyCiphers"),
// Do sets of 50. Recursion will handle the rest below.
cleanedCiphers.Take(50),
userId,
Cipher.TypeValue);
var replacedCount = sprocResponse.Response;
if(replacedCount != cleanedCiphers.Count())
{
await UpdateDirtyCiphersAsync(cleanedCiphers.Skip(replacedCount));
}
}
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains;
using Bit.Core.Enums;
namespace Bit.Core.Repositories.DocumentDB
{
public class FolderRepository : Repository<Folder>, IFolderRepository
{
public FolderRepository(DocumentClient client, string databaseId)
: base(client, databaseId)
{ }
public async Task<Folder> GetByIdAsync(string id, string userId)
{
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id));
if(doc?.Resource == null)
{
return null;
}
var folder = (Folder)((dynamic)doc.Resource);
if(folder.UserId != userId)
{
return null;
}
return folder;
}
public Task<ICollection<Folder>> GetManyByUserIdAsync(string userId)
{
var docs = Client.CreateDocumentQuery<Folder>(DatabaseUri, null, userId)
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId).AsEnumerable();
return Task.FromResult<ICollection<Folder>>(docs.ToList());
}
public Task<ICollection<Folder>> GetManyByUserIdAsync(string userId, bool dirty)
{
var docs = Client.CreateDocumentQuery<Folder>(DatabaseUri, null, userId)
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId && d.Dirty == dirty).AsEnumerable();
return Task.FromResult<ICollection<Folder>>(docs.ToList());
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB
{
public abstract class Repository<T> : BaseRepository<T>, IRepository<T> where T : IDataObject
{
public Repository(DocumentClient client, string databaseId, string documentType = null)
: base(client, databaseId, documentType)
{ }
public virtual Task<T> GetByIdAsync(string id)
{
// NOTE: Not an ideal condition, scanning all collections.
// Override this method if you can implement a direct partition lookup based on the id.
// Use the inherited GetByPartitionIdAsync method to implement your override.
var docs = Client.CreateDocumentQuery<T>(DatabaseUri, new FeedOptions { MaxItemCount = 1 })
.Where(d => d.Id == id).AsEnumerable();
return Task.FromResult(docs.FirstOrDefault());
}
public virtual async Task CreateAsync(T obj)
{
var result = await Client.CreateDocumentAsync(DatabaseUri, obj);
obj.Id = result.Resource.Id;
}
public virtual async Task ReplaceAsync(T obj)
{
await Client.ReplaceDocumentAsync(ResolveDocumentIdLink(obj), obj);
}
public virtual async Task UpsertAsync(T obj)
{
await Client.UpsertDocumentAsync(ResolveDocumentIdLink(obj), obj);
}
public virtual async Task DeleteAsync(T obj)
{
await Client.DeleteDocumentAsync(ResolveDocumentIdLink(obj));
}
public virtual async Task DeleteByIdAsync(string id)
{
// NOTE: Not an ideal condition, scanning all collections.
// Override this method if you can implement a direct partition lookup based on the id.
// Use the inherited DeleteByPartitionIdAsync method to implement your override.
var docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 })
.Where(d => d.Id == id).AsEnumerable();
if(docs.Count() > 0)
{
await Client.DeleteDocumentAsync(docs.First().SelfLink);
}
}
protected async Task<T> GetByPartitionIdAsync(string id)
{
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(id));
if(doc?.Resource == null)
{
return default(T);
}
return (T)((dynamic)doc.Resource);
}
protected async Task DeleteByPartitionIdAsync(string id)
{
await Client.DeleteDocumentAsync(ResolveDocumentIdLink(id));
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains;
using Bit.Core.Enums;
namespace Bit.Core.Repositories.DocumentDB
{
public class SiteRepository : Repository<Site>, ISiteRepository
{
public SiteRepository(DocumentClient client, string databaseId)
: base(client, databaseId)
{ }
public async Task<Site> GetByIdAsync(string id, string userId)
{
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id));
if(doc?.Resource == null)
{
return null;
}
var site = (Site)((dynamic)doc.Resource);
if(site.UserId != userId)
{
return null;
}
return site;
}
public Task<ICollection<Site>> GetManyByUserIdAsync(string userId)
{
var docs = Client.CreateDocumentQuery<Site>(DatabaseUri, null, userId)
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId).AsEnumerable();
return Task.FromResult<ICollection<Site>>(docs.ToList());
}
public Task<ICollection<Site>> GetManyByUserIdAsync(string userId, bool dirty)
{
var docs = Client.CreateDocumentQuery<Site>(DatabaseUri, null, userId)
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId && d.Dirty == dirty).AsEnumerable();
return Task.FromResult<ICollection<Site>>(docs.ToList());
}
}
}

View File

@ -0,0 +1,87 @@
// Update an array of dirty ciphers for a user.
function bulkUpdateDirtyCiphers(ciphers, userId) {
var context = getContext();
var collection = context.getCollection();
var collectionLink = collection.getSelfLink();
var response = context.getResponse();
var count = 0;
// Validate input.
if (!ciphers) {
throw new Error("The ciphers array is undefined or null.");
}
var ciphersLength = ciphers.length;
if (ciphersLength == 0) {
response.setBody(0);
return;
}
queryAndReplace(ciphers[count]);
function queryAndReplace(cipher, continuation) {
var query = {
query: "SELECT * FROM root r WHERE r.id = @id AND r.UserId = @userId AND r.type = 'cipher' AND r.Dirty = true",
parameters: [{ name: '@id', value: cipher.id }, { name: '@userId', value: userId }]
};
var requestOptions = { continuation: continuation };
var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) {
if (err) throw err;
if (documents.length > 0) {
replace(documents[0], cipher);
}
else if (responseOptions.continuation) {
// try again
queryAndReplace(cipher, responseOptions.continuation);
}
else {
// doc not found, skip it
next();
}
});
if (!accepted) {
response.setBody(count);
}
}
function replace(doc, placementCipher) {
// site
if (doc.CipherType == 1) {
doc.Username = placementCipher.Username;
doc.Password = placementCipher.Password;
doc.Notes = placementCipher.Notes;
doc.Uri = placementCipher.Uri;
}
doc.Name = placementCipher.Name;
doc.RevisionDate = placementCipher.RevisionDate;
// no longer dirty
doc.Dirty = false;
var accepted = collection.replaceDocument(doc._self, doc, function (err) {
if (err) throw err;
next();
});
if (!accepted) {
response.setBody(count);
}
}
function next() {
count++;
if (count >= ciphersLength) {
response.setBody(count);
}
else {
queryAndReplace(ciphers[count]);
}
}
}

View File

@ -0,0 +1,107 @@
// Replace user document and mark all related ciphers as dirty.
function replaceUserAndDirtyCiphers(user) {
var context = getContext();
var collection = context.getCollection();
var collectionLink = collection.getSelfLink();
var response = context.getResponse();
// Validate input.
if (!user) {
throw new Error('The user is undefined or null.');
}
getUser(function (userDoc) {
replaceUser(userDoc, function (replacedDoc) {
queryAndDirtyCiphers(function () {
response.setBody(replacedDoc);
});
});
});
function getUser(callback, continuation) {
var query = {
query: 'SELECT * FROM root r WHERE r.id = @id',
parameters: [{ name: '@id', value: user.id }]
};
var requestOptions = { continuation: continuation };
var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) {
if (err) throw err;
if (documents.length > 0) {
callback(documents[0]);
}
else if (responseOptions.continuation) {
getUser(responseOptions.continuation);
}
else {
throw new Error('User not found.');
}
});
if (!accepted) {
throw new Error('The stored procedure timed out.');
}
}
function replaceUser(userDoc, callback) {
var accepted = collection.replaceDocument(userDoc._self, user, {}, function (err, replacedDoc) {
if (err) throw err;
callback(replacedDoc);
});
if (!accepted) {
throw new Error('The stored procedure timed out.');
}
}
function queryAndDirtyCiphers(callback, continuation) {
var query = {
query: 'SELECT * FROM root r WHERE r.type = @type AND r.UserId = @userId',
parameters: [{ name: '@type', value: 'cipher' }, { name: '@userId', value: user.id }]
};
var requestOptions = { continuation: continuation };
var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) {
if (err) throw err;
if (documents.length > 0) {
dirtyCiphers(documents, callback);
}
else if (responseOptions.continuation) {
queryAndDirtyCiphers(callback, responseOptions.continuation);
}
else {
callback();
}
});
if (!accepted) {
throw new Error('The stored procedure timed out.');
}
}
function dirtyCiphers(documents, callback) {
if (documents.length > 0) {
// dirty the cipher
documents[0].Dirty = true;
var requestOptions = { etag: documents[0]._etag };
var accepted = collection.replaceDocument(documents[0]._self, documents[0], requestOptions, function (err) {
if (err) throw err;
documents.shift();
dirtyCiphers(documents, callback);
});
if (!accepted) {
throw new Error('The stored procedure timed out.');
}
}
else {
callback();
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB
{
public class UserRepository : Repository<Domains.User>, IUserRepository
{
public UserRepository(DocumentClient client, string databaseId)
: base(client, databaseId)
{ }
public override async Task<Domains.User> GetByIdAsync(string id)
{
return await GetByPartitionIdAsync(id);
}
public Task<Domains.User> GetByEmailAsync(string email)
{
var docs = Client.CreateDocumentQuery<Domains.User>(DatabaseUri, new FeedOptions { MaxItemCount = 1 })
.Where(d => d.Type == Domains.User.TypeValue && d.Email == email).AsEnumerable();
return Task.FromResult(docs.FirstOrDefault());
}
public async Task ReplaceAndDirtyCiphersAsync(Domains.User user)
{
await Client.ExecuteStoredProcedureAsync<Domains.User>(ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"), user);
}
public override async Task DeleteByIdAsync(string id)
{
await DeleteByPartitionIdAsync(id);
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB.Utilities
{
public class DocumentClientHelpers
{
public static DocumentClient InitClient(GlobalSettings.DocumentDBSettings settings)
{
var client = new DocumentClient(
new Uri(settings.Uri),
settings.Key,
new ConnectionPolicy
{
ConnectionMode = ConnectionMode.Direct,
ConnectionProtocol = Protocol.Tcp
});
var hashResolver = new ManagedHashPartitionResolver(
GetPartitionKeyExtractor(),
settings.DatabaseId,
settings.CollectionIdPrefix,
settings.NumberOfCollections,
null);
client.PartitionResolvers[UriFactory.CreateDatabaseUri(settings.DatabaseId).OriginalString] = hashResolver;
client.OpenAsync().Wait();
return client;
}
private static Func<object, string> GetPartitionKeyExtractor()
{
return doc =>
{
if(doc is Domains.User)
{
return ((Domains.User)doc).Id;
}
if(doc is Domains.Cipher)
{
return ((Domains.Cipher)doc).UserId;
}
throw new InvalidOperationException("Document type is not resolvable for the partition key extractor.");
};
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Partitioning;
namespace Bit.Core.Repositories.DocumentDB.Utilities
{
public class ManagedHashPartitionResolver : HashPartitionResolver
{
public ManagedHashPartitionResolver(
Func<object, string> partitionKeyExtractor,
string databaseId,
string collectionIdPrefix,
int numberOfCollections,
IHashGenerator hashGenerator = null)
: base(
partitionKeyExtractor,
GetCollectionIds(databaseId, collectionIdPrefix, numberOfCollections),
128,
hashGenerator)
{ }
private static List<string> GetCollectionIds(string databaseId, string collectionIdPrefix, int numberOfCollections)
{
var collections = new List<string>();
for(int i = 0; i < numberOfCollections; i++)
{
var collectionIdUri = UriFactory.CreateDocumentCollectionUri(databaseId, string.Concat(collectionIdPrefix, i));
collections.Add(collectionIdUri.OriginalString);
}
return collections;
}
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Bit.Core.Repositories
{
public interface ICipherRepository
{
Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers);
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories
{
public interface IFolderRepository : IRepository<Folder>
{
Task<Folder> GetByIdAsync(string id, string userId);
Task<ICollection<Folder>> GetManyByUserIdAsync(string userId);
Task<ICollection<Folder>> GetManyByUserIdAsync(string userId, bool dirty);
}
}

View File

@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace Bit.Core.Repositories
{
public interface IRepository<T> where T : IDataObject
{
Task<T> GetByIdAsync(string id);
Task CreateAsync(T obj);
Task ReplaceAsync(T obj);
Task UpsertAsync(T obj);
Task DeleteByIdAsync(string id);
Task DeleteAsync(T obj);
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories
{
public interface ISiteRepository : IRepository<Site>
{
Task<Site> GetByIdAsync(string id, string userId);
Task<ICollection<Site>> GetManyByUserIdAsync(string userId);
Task<ICollection<Site>> GetManyByUserIdAsync(string userId, bool dirty);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories
{
public interface IUserRepository : IRepository<User>
{
Task<User> GetByEmailAsync(string email);
Task ReplaceAndDirtyCiphersAsync(User user);
}
}

View File

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Services
{
public interface IMailService
{
Task SendAlreadyRegisteredEmailAsync(string registrantEmailAddress);
Task SendRegisterEmailAsync(string registrantEmailAddress, string token);
Task SendWelcomeEmailAsync(User user);
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Bit.Core.Domains;
namespace Bit.Core.Services
{
public interface IUserService
{
Task<User> GetUserByIdAsync(string userId);
Task SaveUserAsync(User user);
Task InitiateRegistrationAsync(string email);
Task<IdentityResult> RegisterUserAsync(string token, User user, string masterPassword);
Task SendMasterPasswordHintAsync(string email);
Task InitiateEmailChangeAsync(User user, string newEmail);
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<dynamic> ciphers);
Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers);
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider);
}
}

View File

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Net.Mail;
using System.Threading.Tasks;
using Bit.Core.Domains;
using SendGrid;
namespace Bit.Core.Services
{
public class MailService : IMailService
{
private const string AlreadyRegisteredTemplateId = "8af9cd2b-e4dd-497a-bcc6-1d5b317ff811";
private const string RegisterTemplateId = "7382e1f9-50c7-428d-aa06-bf584f03cd6a";
private const string WelcomeTemplateId = "d24aa21e-5ead-45d8-a14e-f96ba7ec63ff";
private const string ChangeEmailAlreadyExistsTemplateId = "b28bc69e-9592-4320-b274-bfb955667add";
private const string ChangeEmailTemplateId = "b8d17dd7-c883-4b47-8170-5b845d487929";
private const string NoMasterPasswordHint = "d5d13bba-3f67-4899-9995-514c1bd6dae7";
private const string MasterPasswordHint = "804a9897-1284-42e8-8aed-ab318c378b71";
private const string AdministrativeCategoryName = "Administrative";
private const string MarketingCategoryName = "Marketing";
private readonly GlobalSettings _globalSettings;
private readonly Web _web;
public MailService(GlobalSettings globalSettings)
{
_globalSettings = globalSettings;
_web = new Web(_globalSettings.Mail.APIKey);
}
public async Task SendAlreadyRegisteredEmailAsync(string registrantEmailAddress)
{
var message = CreateDefaultMessage(AlreadyRegisteredTemplateId);
message.Subject = "Your Registration";
message.AddTo(registrantEmailAddress);
message.AddSubstitution("{{email}}", new List<string> { registrantEmailAddress });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Already Registered" });
await _web.DeliverAsync(message);
}
public async Task SendRegisterEmailAsync(string registrantEmailAddress, string token)
{
var message = CreateDefaultMessage(RegisterTemplateId);
message.Subject = "Complete Your Registration";
message.AddTo(registrantEmailAddress);
message.AddSubstitution("{{token}}", new List<string> { Uri.EscapeDataString(token) });
message.AddSubstitution("{{email}}", new List<string> { Uri.EscapeDataString(registrantEmailAddress) });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Register" });
message.DisableBypassListManagement();
await _web.DeliverAsync(message);
}
public async Task SendWelcomeEmailAsync(User user)
{
var message = CreateDefaultMessage(WelcomeTemplateId);
message.Subject = "Welcome";
message.AddTo(user.Email);
message.SetCategories(new List<string> { AdministrativeCategoryName, "Welcome" });
await _web.DeliverAsync(message);
}
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
{
var message = CreateDefaultMessage(ChangeEmailAlreadyExistsTemplateId);
message.Subject = "Your Email Change";
message.AddTo(toEmail);
message.AddSubstitution("{{fromEmail}}", new List<string> { fromEmail });
message.AddSubstitution("{{toEmail}}", new List<string> { toEmail });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Change Email Alrady Exists" });
await _web.DeliverAsync(message);
}
public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token)
{
var message = CreateDefaultMessage(ChangeEmailTemplateId);
message.Subject = "Change Your Email";
message.AddTo(newEmailAddress);
message.AddSubstitution("{{token}}", new List<string> { Uri.EscapeDataString(token) });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Change Email" });
message.DisableBypassListManagement();
await _web.DeliverAsync(message);
}
public async Task SendNoMasterPasswordHintEmailAsync(string email)
{
var message = CreateDefaultMessage(NoMasterPasswordHint);
message.Subject = "Your Master Password Hint";
message.AddTo(email);
message.SetCategories(new List<string> { AdministrativeCategoryName, "No Master Password Hint" });
await _web.DeliverAsync(message);
}
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)
{
var message = CreateDefaultMessage(MasterPasswordHint);
message.Subject = "Your Master Password Hint";
message.AddTo(email);
message.AddSubstitution("{{hint}}", new List<string> { hint });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Master Password Hint" });
await _web.DeliverAsync(message);
}
private SendGridMessage CreateDefaultMessage(string templateId)
{
var message = new SendGridMessage
{
From = new MailAddress(_globalSettings.Mail.ReplyToEmail, _globalSettings.SiteName),
Html = " ",
Text = " "
};
if(!string.IsNullOrWhiteSpace(templateId))
{
message.EnableTemplateEngine(templateId);
}
message.AddSubstitution("{{siteName}}", new List<string> { _globalSettings.SiteName });
message.AddSubstitution("{{baseVaultUri}}", new List<string> { _globalSettings.BaseVaultUri });
return message;
}
}
}

View File

@ -0,0 +1,308 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.DataProtection;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.OptionsModel;
using Bit.Core.Domains;
using Bit.Core.Repositories;
using OtpSharp;
using Base32;
using System.Linq;
namespace Bit.Core.Services
{
public class UserService : UserManager<User>, IUserService, IDisposable
{
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IMailService _mailService;
private readonly ITimeLimitedDataProtector _registrationEmailDataProtector;
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IdentityOptions _identityOptions;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
public UserService(
IUserRepository userRepository,
ICipherRepository cipherRepository,
IMailService mailService,
IDataProtectionProvider dataProtectionProvider,
IUserStore<User> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<User> passwordHasher,
IEnumerable<IUserValidator<User>> userValidators,
IEnumerable<IPasswordValidator<User>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<User>> logger,
IHttpContextAccessor contextAccessor)
: base(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger,
contextAccessor)
{
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_mailService = mailService;
_registrationEmailDataProtector = dataProtectionProvider.CreateProtector("RegistrationEmail").ToTimeLimitedDataProtector();
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
_identityErrorDescriber = errors;
_passwordHasher = passwordHasher;
_passwordValidators = passwordValidators;
}
public async Task<User> GetUserByIdAsync(string userId)
{
return await _userRepository.GetByIdAsync(userId);
}
public async Task SaveUserAsync(User user)
{
if(string.IsNullOrWhiteSpace(user.Id))
{
throw new ApplicationException("Use register method to create a new user.");
}
await _userRepository.ReplaceAsync(user);
}
public async Task InitiateRegistrationAsync(string email)
{
var existingUser = await _userRepository.GetByEmailAsync(email);
if(existingUser != null)
{
await _mailService.SendAlreadyRegisteredEmailAsync(email);
return;
}
var token = _registrationEmailDataProtector.Protect(email, TimeSpan.FromDays(5));
await _mailService.SendRegisterEmailAsync(email, token);
}
public async Task<IdentityResult> RegisterUserAsync(string token, User user, string masterPassword)
{
try
{
var tokenEmail = _registrationEmailDataProtector.Unprotect(token);
if(tokenEmail != user.Email)
{
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
}
}
catch
{
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
}
var result = await base.CreateAsync(user, masterPassword);
if(result == IdentityResult.Success)
{
await _mailService.SendWelcomeEmailAsync(user);
}
return result;
}
public async Task SendMasterPasswordHintAsync(string email)
{
var user = await _userRepository.GetByEmailAsync(email);
if(user == null)
{
// No user exists. Do we want to send an email telling them this in the future?
return;
}
if(string.IsNullOrWhiteSpace(user.MasterPasswordHint))
{
await _mailService.SendNoMasterPasswordHintEmailAsync(email);
}
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
}
public async Task InitiateEmailChangeAsync(User user, string newEmail)
{
var existingUser = await _userRepository.GetByEmailAsync(newEmail);
if(existingUser != null)
{
await _mailService.SendChangeEmailAlreadyExistsEmailAsync(user.Email, newEmail);
return;
}
var token = await base.GenerateChangeEmailTokenAsync(user, newEmail);
await _mailService.SendChangeEmailEmailAsync(newEmail, token);
}
public async Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<dynamic> ciphers)
{
var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword);
if(verifyPasswordResult == PasswordVerificationResult.Failed)
{
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
if(!await base.VerifyUserTokenAsync(user, _identityOptions.Tokens.ChangeEmailTokenProvider, GetChangeEmailTokenPurpose(newEmail), token))
{
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
}
var existingUser = await _userRepository.GetByEmailAsync(newEmail);
if(existingUser != null)
{
return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail));
}
user.OldEmail = user.Email;
user.OldMasterPassword = user.MasterPassword;
user.Email = newEmail;
user.MasterPassword = _passwordHasher.HashPassword(user, newMasterPassword);
user.SecurityStamp = Guid.NewGuid().ToString();
await _userRepository.ReplaceAndDirtyCiphersAsync(user);
await _cipherRepository.UpdateDirtyCiphersAsync(ciphers);
// TODO: what if something fails? rollback?
return IdentityResult.Success;
}
public override Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash)
{
throw new NotImplementedException();
}
public async Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(await base.CheckPasswordAsync(user, currentMasterPasswordHash))
{
var result = await UpdatePasswordHash(user, newMasterPasswordHash);
if(!result.Succeeded)
{
return result;
}
await _userRepository.ReplaceAndDirtyCiphersAsync(user);
await _cipherRepository.UpdateDirtyCiphersAsync(ciphers);
// TODO: what if something fails? rollback?
return IdentityResult.Success;
}
Logger.LogWarning("Change password failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(await base.CheckPasswordAsync(user, masterPasswordHash))
{
var result = await base.UpdateSecurityStampAsync(user);
if(!result.Succeeded)
{
return result;
}
await _userRepository.ReplaceAndDirtyCiphersAsync(user);
return IdentityResult.Success;
}
Logger.LogWarning("Refresh security stamp failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider)
{
if(user.TwoFactorEnabled && user.TwoFactorProvider.HasValue && user.TwoFactorProvider.Value == provider)
{
switch(provider)
{
case Enums.TwoFactorProvider.Authenticator:
if(!string.IsNullOrWhiteSpace(user.AuthenticatorKey))
{
return;
}
break;
default:
throw new ArgumentException(nameof(provider));
}
}
user.TwoFactorProvider = provider;
// Reset authenticator key.
user.AuthenticatorKey = null;
switch(provider)
{
case Enums.TwoFactorProvider.Authenticator:
var key = KeyGeneration.GenerateRandomKey(20);
user.AuthenticatorKey = Base32Encoder.Encode(key);
break;
default:
throw new ArgumentException(nameof(provider));
}
await _userRepository.ReplaceAsync(user);
}
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword, bool validatePassword = true)
{
if(validatePassword)
{
var validate = await ValidatePasswordInternal(user, newPassword);
if(!validate.Succeeded)
{
return validate;
}
}
user.OldMasterPassword = user.MasterPassword;
user.MasterPassword = _passwordHasher.HashPassword(user, newPassword);
user.SecurityStamp = Guid.NewGuid().ToString();
return IdentityResult.Success;
}
private async Task<IdentityResult> ValidatePasswordInternal(User user, string password)
{
var errors = new List<IdentityError>();
foreach(var v in _passwordValidators)
{
var result = await v.ValidateAsync(this, user, password);
if(!result.Succeeded)
{
errors.AddRange(result.Errors);
}
}
if(errors.Count > 0)
{
Logger.LogWarning("User {userId} password validation failed: {errors}.", await GetUserIdAsync(user), string.Join(";", errors.Select(e => e.Code)));
return IdentityResult.Failed(errors.ToArray());
}
return IdentityResult.Success;
}
}
}

23
src/Core/project.json Normal file
View File

@ -0,0 +1,23 @@
{
"version": "0.0.1-*",
"description": "bitwarden Core Library",
"authors": [ "Kyle Spearrin" ],
"tags": [ "" ],
"projectUrl": "",
"licenseUrl": "",
"dependencies": {
"Microsoft.AspNet.Identity": "3.0.0-rc1-final",
"Microsoft.AspNet.Authentication.JwtBearer": "1.0.0-rc1-final",
"Microsoft.Azure.DocumentDB": "1.5.1",
"Newtonsoft.Json": "7.0.1",
"OtpSharp": "1.3.0.4",
"Microsoft.AspNet.Mvc.Abstractions": "6.0.0-rc1-final",
"Sendgrid": "6.3.0",
"Microsoft.AspNet.DataProtection.Extensions": "1.0.0-rc1-final"
},
"frameworks": {
"dnx451": { }
}
}