1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -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

19
src/Api/Api.xproj Normal file
View File

@ -0,0 +1,19 @@
<?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>e8548ad6-7fb0-439a-8eb5-549a10336d2d</ProjectGuid>
<RootNamespace>Bit.Api</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>4000</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,210 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.DataProtection;
using Microsoft.AspNet.Mvc;
using Bit.Api.Models;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Microsoft.AspNet.Identity;
using Bit.Core.Domains;
using Bit.Core.Enums;
using Bit.Core;
using System.Security.Claims;
namespace Bit.Api.Controllers
{
[Route("accounts")]
[Authorize("Application")]
public class AccountsController : Controller
{
private readonly IUserService _userService;
private readonly UserManager<User> _userManager;
private readonly CurrentContext _currentContext;
public AccountsController(
IDataProtectionProvider dataProtectionProvider,
IUserService userService,
UserManager<User> userManager,
CurrentContext currentContext)
{
_userService = userService;
_userManager = userManager;
_currentContext = currentContext;
}
[HttpPost("register-token")]
[AllowAnonymous]
public async Task PostRegisterToken([FromBody]RegisterTokenRequestModel model)
{
await _userService.InitiateRegistrationAsync(model.Email);
}
[HttpPost("register")]
[AllowAnonymous]
public async Task PostRegister([FromBody]RegisterRequestModel model)
{
var result = await _userService.RegisterUserAsync(model.Token, model.ToUser(), model.MasterPasswordHash);
if(result.Succeeded)
{
return;
}
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpPost("password-hint")]
[AllowAnonymous]
public async Task PostPasswordHint([FromBody]PasswordHintRequestModel model)
{
await _userService.SendMasterPasswordHintAsync(model.Email);
}
[HttpPost("email-token")]
public async Task PostEmailToken([FromBody]EmailTokenRequestModel model)
{
if(!await _userManager.CheckPasswordAsync(_currentContext.User, model.MasterPasswordHash))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
await _userService.InitiateEmailChangeAsync(_currentContext.User, model.NewEmail);
}
[HttpPut("email")]
public async Task PutEmail([FromBody]EmailRequestModel model)
{
// NOTE: It is assumed that the eventual repository call will make sure the updated
// ciphers belong to user making this call. Therefore, no check is done here.
var ciphers = CipherRequestModel.ToDynamicCiphers(model.Ciphers, User.GetUserId());
var result = await _userService.ChangeEmailAsync(
_currentContext.User,
model.MasterPasswordHash,
model.NewEmail,
model.NewMasterPasswordHash,
model.Token,
ciphers);
if(result.Succeeded)
{
return;
}
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpPut("password")]
public async Task PutPassword([FromBody]PasswordRequestModel model)
{
// NOTE: It is assumed that the eventual repository call will make sure the updated
// ciphers belong to user making this call. Therefore, no check is done here.
var ciphers = CipherRequestModel.ToDynamicCiphers(model.Ciphers, User.GetUserId());
var result = await _userService.ChangePasswordAsync(
_currentContext.User,
model.MasterPasswordHash,
model.NewMasterPasswordHash,
ciphers);
if(result.Succeeded)
{
return;
}
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpPut("security-stamp")]
public async Task PutSecurityStamp([FromBody]SecurityStampRequestModel model)
{
var result = await _userService.RefreshSecurityStampAsync(_currentContext.User, model.MasterPasswordHash);
if(result.Succeeded)
{
return;
}
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpGet("profile")]
public Task<ProfileResponseModel> GetProfile()
{
var response = new ProfileResponseModel(_currentContext.User);
return Task.FromResult(response);
}
[HttpPut("profile")]
public async Task<ProfileResponseModel> PutProfile([FromBody]UpdateProfileRequestModel model)
{
await _userService.SaveUserAsync(model.ToUser(_currentContext.User));
var response = new ProfileResponseModel(_currentContext.User);
return response;
}
[HttpGet("two-factor")]
public async Task<TwoFactorResponseModel> GetTwoFactor(string masterPasswordHash, TwoFactorProvider provider)
{
var user = _currentContext.User;
if(!await _userManager.CheckPasswordAsync(user, masterPasswordHash))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
await _userService.GetTwoFactorAsync(user, provider);
var response = new TwoFactorResponseModel(user);
return response;
}
[HttpPut("two-factor")]
public async Task<TwoFactorResponseModel> PutTwoFactor([FromBody]UpdateTwoFactorRequestModel model)
{
var user = _currentContext.User;
if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
if(model.Enabled.Value && !await _userManager.VerifyTwoFactorTokenAsync(user, "Authenticator", model.Token))
{
await Task.Delay(2000);
throw new BadRequestException("Token", "Invalid token.");
}
user.TwoFactorEnabled = model.Enabled.Value;
await _userService.SaveUserAsync(user);
var response = new TwoFactorResponseModel(user);
return response;
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Bit.Core.Identity;
using Bit.Core.Repositories;
using Bit.Api.Models;
using Microsoft.AspNet.Authorization;
using Bit.Core.Exceptions;
using Bit.Core;
namespace Bit.Api.Controllers
{
[Route("auth")]
public class AuthController : Controller
{
private readonly JwtBearerSignInManager _signInManager;
private readonly IUserRepository _userRepository;
private readonly CurrentContext _currentContext;
public AuthController(
JwtBearerSignInManager signInManager,
IUserRepository userRepository,
CurrentContext currentContext)
{
_signInManager = signInManager;
_userRepository = userRepository;
_currentContext = currentContext;
}
[HttpPost("token")]
[AllowAnonymous]
public async Task<AuthTokenResponseModel> PostToken([FromBody]AuthTokenRequestModel model)
{
var result = await _signInManager.PasswordSignInAsync(model.Email.ToLower(), model.MasterPasswordHash);
if(result == JwtBearerSignInResult.Success)
{
return new AuthTokenResponseModel(result.Token, result.User);
}
else if(result == JwtBearerSignInResult.TwoFactorRequired)
{
return new AuthTokenResponseModel(result.Token, null);
}
await Task.Delay(2000);
throw new BadRequestException("Username or password is incorrect. Try again.");
}
[HttpPost("token/two-factor")]
[Authorize("TwoFactor")]
public async Task<AuthTokenResponseModel> PostTokenTwoFactor([FromBody]AuthTokenTwoFactorRequestModel model)
{
var result = await _signInManager.TwoFactorSignInAsync(_currentContext.User, model.Provider, model.Code);
if(result == JwtBearerSignInResult.Success)
{
return new AuthTokenResponseModel(result.Token, result.User);
}
await Task.Delay(2000);
throw new BadRequestException("Code is not correct. Try again.");
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Bit.Core.Repositories;
using System.Security.Claims;
using Microsoft.AspNet.Authorization;
using Bit.Api.Models;
using Bit.Core.Exceptions;
namespace Bit.Api.Controllers
{
[Route("folders")]
[Authorize("Application")]
public class FoldersController : Controller
{
private readonly IFolderRepository _folderRepository;
public FoldersController(
IFolderRepository folderRepository)
{
_folderRepository = folderRepository;
}
[HttpGet("{id}")]
public async Task<FolderResponseModel> Get(string id)
{
var folder = await _folderRepository.GetByIdAsync(id, User.GetUserId());
if(folder == null)
{
throw new NotFoundException();
}
return new FolderResponseModel(folder);
}
[HttpGet("")]
public async Task<ListResponseModel<FolderResponseModel>> Get(bool dirty = false)
{
var folders = await _folderRepository.GetManyByUserIdAsync(User.GetUserId());
return new ListResponseModel<FolderResponseModel>(folders.Select(f => new FolderResponseModel(f)));
}
[HttpPost("")]
public async Task<FolderResponseModel> Post([FromBody]FolderRequestModel model)
{
var folder = model.ToFolder(User.GetUserId());
await _folderRepository.CreateAsync(folder);
return new FolderResponseModel(folder);
}
[HttpPut("{id}")]
public async Task<FolderResponseModel> Put(string id, [FromBody]FolderRequestModel model)
{
var folder = await _folderRepository.GetByIdAsync(id, User.GetUserId());
if(folder == null)
{
throw new NotFoundException();
}
await _folderRepository.ReplaceAsync(model.ToFolder(folder));
return new FolderResponseModel(folder);
}
[HttpDelete("{id}")]
public async Task Delete(string id)
{
var folder = await _folderRepository.GetByIdAsync(id, User.GetUserId());
if(folder == null)
{
throw new NotFoundException();
}
await _folderRepository.DeleteAsync(folder);
}
}
}

View File

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Bit.Core.Repositories;
using System.Security.Claims;
using Microsoft.AspNet.Authorization;
using Bit.Api.Models;
using Bit.Core.Exceptions;
using Bit.Core.Domains;
namespace Bit.Api.Controllers
{
[Route("sites")]
[Authorize("Application")]
public class SitesController : Controller
{
private readonly ISiteRepository _siteRepository;
private readonly IFolderRepository _folderRepository;
public SitesController(
ISiteRepository siteRepository,
IFolderRepository folderRepository)
{
_siteRepository = siteRepository;
_folderRepository = folderRepository;
}
[HttpGet("{id}")]
public async Task<SiteResponseModel> Get(string id, string[] expand = null)
{
var site = await _siteRepository.GetByIdAsync(id, User.GetUserId());
if(site == null)
{
throw new NotFoundException();
}
var response = new SiteResponseModel(site);
await ExpandAsync(site, response, expand, null);
return response;
}
[HttpGet("")]
public async Task<ListResponseModel<SiteResponseModel>> Get(bool dirty = false, string[] expand = null)
{
var sites = await _siteRepository.GetManyByUserIdAsync(User.GetUserId(), dirty);
var responses = sites.Select(s => new SiteResponseModel(s));
await ExpandManyAsync(sites, responses, expand, null);
return new ListResponseModel<SiteResponseModel>(responses);
}
[HttpPost("")]
public async Task<SiteResponseModel> Post([FromBody]SiteRequestModel model, string[] expand = null)
{
var site = model.ToSite(User.GetUserId());
await _siteRepository.CreateAsync(site);
var response = new SiteResponseModel(site);
await ExpandAsync(site, response, expand, null);
return response;
}
[HttpPut("{id}")]
public async Task<SiteResponseModel> Put(string id, [FromBody]SiteRequestModel model, string[] expand = null)
{
var site = await _siteRepository.GetByIdAsync(id, User.GetUserId());
if(site == null)
{
throw new NotFoundException();
}
await _siteRepository.ReplaceAsync(model.ToSite(site));
var response = new SiteResponseModel(site);
await ExpandAsync(site, response, expand, null);
return response;
}
[HttpDelete("{id}")]
public async Task Delete(string id)
{
var site = await _siteRepository.GetByIdAsync(id, User.GetUserId());
if(site == null)
{
throw new NotFoundException();
}
await _siteRepository.DeleteAsync(site);
}
private async Task ExpandAsync(Site site, SiteResponseModel response, string[] expand, Folder folder)
{
if(expand == null || expand.Count() == 0)
{
return;
}
if(expand.Any(e => e.ToLower() == "folder"))
{
if(folder == null)
{
folder = await _folderRepository.GetByIdAsync(site.FolderId);
}
response.Folder = new FolderResponseModel(folder);
}
}
private async Task ExpandManyAsync(IEnumerable<Site> sites, IEnumerable<SiteResponseModel> responses, string[] expand, IEnumerable<Folder> folders)
{
if(expand == null || expand.Count() == 0)
{
return;
}
if(expand.Any(e => e.ToLower() == "folder"))
{
if(folders == null)
{
folders = await _folderRepository.GetManyByUserIdAsync(User.GetUserId());
}
if(folders != null && folders.Count() > 0)
{
foreach(var response in responses)
{
var site = sites.SingleOrDefault(s => s.Id == response.Id);
if(site == null)
{
continue;
}
var folder = folders.SingleOrDefault(f => f.Id == site.FolderId);
if(folder == null)
{
continue;
}
response.Folder = new FolderResponseModel(folder);
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class EmailRequestModel
{
[Required]
[EmailAddress]
public string NewEmail { get; set; }
[Required]
public string MasterPasswordHash { get; set; }
[Required]
public string NewMasterPasswordHash { get; set; }
[Required]
public string Token { get; set; }
[Required]
public CipherRequestModel[] Ciphers { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class EmailTokenRequestModel
{
[Required]
[EmailAddress]
public string NewEmail { get; set; }
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class PasswordHintRequestModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class PasswordRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
[Required]
public string NewMasterPasswordHash { get; set; }
[Required]
public CipherRequestModel[] Ciphers { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class RegisterRequestModel
{
[Required]
public string Token { get; set; }
[Required]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
public string MasterPasswordHash { get; set; }
public string MasterPasswordHint { get; set; }
public User ToUser()
{
return new User
{
Name = Name,
Email = Email,
MasterPasswordHint = MasterPasswordHint
};
}
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class RegisterTokenRequestModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class SecurityStampRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class UpdateProfileRequestModel
{
[Required]
public string Name { get; set; }
public string MasterPasswordHint { get; set; }
[Required]
[RegularExpression("^[a-z]{2}-[A-Z]{2}$")]
public string Culture { get; set; }
public User ToUser(User existingUser)
{
existingUser.Name = Name;
existingUser.MasterPasswordHint = string.IsNullOrWhiteSpace(MasterPasswordHint) ? null : MasterPasswordHint;
existingUser.Culture = Culture;
return existingUser;
}
}
}

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class UpdateTwoFactorRequestModel : IValidatableObject
{
[Required]
public string MasterPasswordHash { get; set; }
[Required]
public bool? Enabled { get; set; }
public string Token { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(Enabled.HasValue && Enabled.Value && string.IsNullOrWhiteSpace(Token))
{
yield return new ValidationResult("Token is required.", new[] { "Token" });
}
}
}
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class AuthTokenRequestModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class AuthTokenTwoFactorRequestModel
{
[Required]
public string Code { get; set; }
[Required]
public string Provider { get; set; }
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities;
using Bit.Core.Domains;
using System.Linq;
using Bit.Core.Enums;
namespace Bit.Api.Models
{
public class CipherRequestModel : IValidatableObject
{
public CipherType Type { get; set; }
[Required]
public string Id { get; set; }
public string FolderId { get; set; }
[Required]
[EncryptedString]
public string Name { get; set; }
[EncryptedString]
public string Uri { get; set; }
[EncryptedString]
public string Username { get; set; }
[EncryptedString]
public string Password { get; set; }
[EncryptedString]
public string Notes { get; set; }
public virtual Site ToSite(string userId = null)
{
return new Site
{
Id = Id,
UserId = userId,
FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId,
Name = Name,
Uri = Uri,
Username = Username,
Password = Password,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes
};
}
public Folder ToFolder(string userId = null)
{
return new Folder
{
Id = Id,
UserId = userId,
Name = Name
};
}
public static IEnumerable<dynamic> ToDynamicCiphers(CipherRequestModel[] models, string userId)
{
var sites = models.Where(m => m.Type == CipherType.Site).Select(m => m.ToSite(userId)).ToList();
var folders = models.Where(m => m.Type == CipherType.Folder).Select(m => m.ToFolder(userId)).ToList();
var ciphers = new List<dynamic>();
ciphers.AddRange(sites);
ciphers.AddRange(folders);
return ciphers;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(Type == CipherType.Site)
{
if(string.IsNullOrWhiteSpace(Uri))
{
yield return new ValidationResult("Uri is required for a site cypher.", new[] { "Uri" });
}
if(string.IsNullOrWhiteSpace(Username))
{
yield return new ValidationResult("Username is required for a site cypher.", new[] { "Username" });
}
if(string.IsNullOrWhiteSpace(Password))
{
yield return new ValidationResult("Password is required for a site cypher.", new[] { "Password" });
}
}
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class FolderRequestModel
{
[Required]
[EncryptedString]
public string Name { get; set; }
public Folder ToFolder(string userId = null)
{
return new Folder
{
UserId = userId,
Name = Name
};
}
public Folder ToFolder(Folder existingFolder)
{
existingFolder.Name = Name;
return existingFolder;
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class SiteRequestModel
{
public string FolderId { get; set; }
[Required]
[EncryptedString]
public string Name { get; set; }
[Required]
[EncryptedString]
public string Uri { get; set; }
[Required]
[EncryptedString]
public string Username { get; set; }
[Required]
[EncryptedString]
public string Password { get; set; }
[EncryptedString]
public string Notes { get; set; }
public Site ToSite(string userId = null)
{
return new Site
{
UserId = userId,
FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId,
Name = Name,
Uri = Uri,
Username = Username,
Password = Password,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes
};
}
public Site ToSite(Site existingSite)
{
existingSite.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId;
existingSite.Name = Name;
existingSite.Uri = Uri;
existingSite.Username = Username;
existingSite.Password = Password;
existingSite.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes;
return existingSite;
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class AuthTokenResponseModel : ResponseModel
{
public AuthTokenResponseModel(string token, User user = null)
: base("authToken")
{
Token = token;
Profile = user == null ? null : new ProfileResponseModel(user);
}
public string Token { get; set; }
public ProfileResponseModel Profile { get; set; }
}
}

View File

@ -0,0 +1,51 @@
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Bit.Api.Models.Response
{
public class ErrorResponseModel : ResponseModel
{
public ErrorResponseModel()
: base("error")
{ }
public ErrorResponseModel(ModelStateDictionary modelState)
: this()
{
Message = "The model state is invalid.";
ValidationErrors = new Dictionary<string, IEnumerable<string>>();
var keys = modelState.Keys.ToList();
var values = modelState.Values.ToList();
for(var i = 0; i < values.Count; i++)
{
var value = values[i];
if(keys.Count <= i)
{
// Keys not available for some reason.
break;
}
var key = keys[i];
if(value.ValidationState != ModelValidationState.Invalid || value.Errors.Count == 0)
{
continue;
}
var errors = value.Errors.Select(e => e.ErrorMessage);
ValidationErrors.Add(key, errors);
}
}
public string Message { get; set; }
public Dictionary<string, IEnumerable<string>> ValidationErrors { get; set; }
// For use in development environments.
public string ExceptionMessage { get; set; }
public string ExceptionStackTrace { get; set; }
public string InnerExceptionMessage { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class FolderResponseModel : ResponseModel
{
public FolderResponseModel(Folder folder)
: base("folder")
{
if(folder == null)
{
throw new ArgumentNullException(nameof(folder));
}
Id = folder.Id;
Name = folder.Name;
}
public string Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
namespace Bit.Api.Models
{
public class ListResponseModel<T> : ResponseModel where T : ResponseModel
{
public ListResponseModel(IEnumerable<T> data)
: base("list")
{
Data = data;
}
public IEnumerable<T> Data { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class ProfileResponseModel : ResponseModel
{
public ProfileResponseModel(User user)
: base("profile")
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
Id = user.Id;
Name = user.Name;
Email = user.Email;
MasterPasswordHint = string.IsNullOrWhiteSpace(user.MasterPasswordHint) ? null : user.MasterPasswordHint;
Culture = user.Culture;
TwoFactorEnabled = user.TwoFactorEnabled;
}
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string MasterPasswordHint { get; set; }
public string Culture { get; set; }
public bool TwoFactorEnabled { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace Bit.Api.Models
{
public abstract class ResponseModel
{
public ResponseModel(string obj)
{
if(string.IsNullOrWhiteSpace(obj))
{
throw new ArgumentNullException(nameof(obj));
}
Object = obj;
}
public string Object { get; private set; }
}
}

View File

@ -0,0 +1,36 @@
using System;
using Bit.Core.Domains;
namespace Bit.Api.Models
{
public class SiteResponseModel : ResponseModel
{
public SiteResponseModel(Site site)
: base("site")
{
if(site == null)
{
throw new ArgumentNullException(nameof(site));
}
Id = site.Id;
FolderId = string.IsNullOrWhiteSpace(site.FolderId) ? null : site.FolderId;
Name = site.Name;
Uri = site.Uri;
Username = site.Username;
Password = site.Password;
Notes = site.Notes;
}
public string Id { get; set; }
public string FolderId { get; set; }
public string Name { get; set; }
public string Uri { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Notes { get; set; }
// Expandables
public FolderResponseModel Folder { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using Bit.Core.Domains;
using Bit.Core.Enums;
namespace Bit.Api.Models
{
public class TwoFactorResponseModel : ResponseModel
{
public TwoFactorResponseModel(User user)
: base("twoFactor")
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
TwoFactorEnabled = user.TwoFactorEnabled;
AuthenticatorKey = user.AuthenticatorKey;
TwoFactorProvider = user.TwoFactorProvider;
}
public bool TwoFactorEnabled { get; set; }
public TwoFactorProvider? TwoFactorProvider { get; set; }
public string AuthenticatorKey { get; set; }
}
}

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.Api")]
[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("e8548ad6-7fb0-439a-8eb5-549a10336d2d")]

View File

@ -0,0 +1,25 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:4000",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"Hosting:Environment": "Development"
}
},
"web": {
"commandName": "web",
"environmentVariables": {
"Hosting:Environment": "Development"
}
}
}
}

157
src/Api/Startup.cs Normal file
View File

@ -0,0 +1,157 @@
using System;
using System.Security.Claims;
using Microsoft.AspNet.Authentication.JwtBearer;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.OptionsModel;
using Bit.Api.Utilities;
using Bit.Core;
using Bit.Core.Domains;
using Bit.Core.Identity;
using Bit.Core.Repositories;
using Bit.Core.Repositories.DocumentDB.Utilities;
using Bit.Core.Services;
using Repos = Bit.Core.Repositories.DocumentDB;
namespace Bit.Api
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("settings.json")
.AddJsonFile($"settings.{env.EnvironmentName}.json", optional: true);
if(env.IsDevelopment())
{
builder.AddUserSecrets();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<GlobalSettings>(Configuration.GetSection("globalSettings"));
// Options
services.AddOptions();
// Settings
var provider = services.BuildServiceProvider();
var globalSettings = provider.GetRequiredService<IOptions<GlobalSettings>>().Value;
services.AddSingleton(s => globalSettings);
// Repositories
var documentDBClient = DocumentClientHelpers.InitClient(globalSettings.DocumentDB);
services.AddSingleton<IUserRepository>(s => new Repos.UserRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
services.AddSingleton<ISiteRepository>(s => new Repos.SiteRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
services.AddSingleton<IFolderRepository>(s => new Repos.FolderRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
services.AddSingleton<ICipherRepository>(s => new Repos.CipherRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
// Context
services.AddScoped<CurrentContext>();
// Identity
services.AddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
services.AddJwtBearerIdentity(options =>
{
options.User = new UserOptions
{
RequireUniqueEmail = true,
AllowedUserNameCharacters = null // all
};
options.Password = new PasswordOptions
{
RequireDigit = false,
RequireLowercase = false,
RequiredLength = 8,
RequireNonLetterOrDigit = false,
RequireUppercase = false
};
options.ClaimsIdentity = new ClaimsIdentityOptions
{
SecurityStampClaimType = "securitystamp",
UserNameClaimType = ClaimTypes.Email
};
options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider;
}, jwtBearerOptions =>
{
jwtBearerOptions.Audience = "bitwarden";
jwtBearerOptions.Issuer = "bitwarden";
jwtBearerOptions.TokenLifetime = TimeSpan.FromDays(10 * 365);
jwtBearerOptions.TwoFactorTokenLifetime = TimeSpan.FromMinutes(10);
// TODO: Symmetric key
// waiting on https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/250
jwtBearerOptions.SigningCredentials = null;
})
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.AddTokenProvider<AuthenticatorTokenProvider>("Authenticator")
.AddTokenProvider<EmailTokenProvider<User>>(TokenOptions.DefaultEmailProvider);
var jwtIdentityOptions = provider.GetRequiredService<IOptions<JwtBearerIdentityOptions>>().Value;
services.AddAuthorization(auth =>
{
auth.AddPolicy("Application", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().RequireClaim(ClaimTypes.AuthenticationMethod, jwtIdentityOptions.AuthenticationMethod).Build());
auth.AddPolicy("TwoFactor", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().RequireClaim(ClaimTypes.AuthenticationMethod, jwtIdentityOptions.TwoFactorAuthenticationMethod).Build());
});
services.AddScoped<AuthenticatorTokenProvider>();
// Services
services.AddSingleton<IMailService, MailService>();
services.AddScoped<IUserService, UserService>();
// Cors
services.AddCors(o => o.AddPolicy("All", policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
// MVC
services.AddMvc(o =>
{
o.Filters.Add(new ExceptionHandlerFilterAttribute());
o.Filters.Add(new ModelStateValidationFilterAttribute());
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.MinimumLevel = LogLevel.Information;
loggerFactory.AddConsole();
loggerFactory.AddDebug();
// Add the platform handler to the request pipeline.
app.UseIISPlatformHandler();
// Add static files to the request pipeline.
app.UseStaticFiles();
// Add Cors
app.UseCors("All");
// Add Jwt authentication to the request pipeline.
app.UseJwtBearerIdentity();
// Add MVC to the request pipeline.
app.UseMvc();
}
// Entry point for the application.
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Utilities
{
/// <summary>
/// Validates a string that is in encrypted form: "b64iv=|b64ct="
/// </summary>
public class EncryptedStringAttribute : ValidationAttribute
{
public EncryptedStringAttribute()
: base("{0} is not a valid encrypted string.")
{ }
public override bool IsValid(object value)
{
if(value == null)
{
return true;
}
try
{
var encString = value?.ToString();
if(string.IsNullOrWhiteSpace(encString))
{
return false;
}
var encStringPieces = encString.Split('|');
if(encStringPieces.Length != 2)
{
return false;
}
var iv = Convert.FromBase64String(encStringPieces[0]);
var ct = Convert.FromBase64String(encStringPieces[1]);
if(iv.Length < 1 || ct.Length < 1)
{
return false;
}
}
catch
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.IdentityModel.Tokens;
using Bit.Api.Models.Response;
using Bit.Core.Exceptions;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Api.Utilities
{
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
var errorModel = new ErrorResponseModel { Message = "An error has occured." };
var exception = context.Exception;
if(exception == null)
{
// Should never happen.
return;
}
var badRequestException = exception as BadRequestException;
if(badRequestException != null)
{
context.HttpContext.Response.StatusCode = 400;
if(badRequestException != null)
{
errorModel = new ErrorResponseModel(badRequestException.ModelState);
}
else
{
errorModel.Message = badRequestException.Message;
}
}
else if(exception is ApplicationException)
{
context.HttpContext.Response.StatusCode = 402;
}
else if(exception is NotFoundException)
{
errorModel.Message = "Resource not found.";
context.HttpContext.Response.StatusCode = 404;
}
else
{
errorModel.Message = "An unhandled server error has occured.";
context.HttpContext.Response.StatusCode = 500;
}
var env = context.HttpContext.ApplicationServices.GetRequiredService<IHostingEnvironment>();
if(env.IsDevelopment())
{
errorModel.ExceptionMessage = exception.Message;
errorModel.ExceptionStackTrace = exception.StackTrace;
errorModel.InnerExceptionMessage = exception?.InnerException?.Message;
}
context.Result = new ObjectResult(errorModel);
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Filters;
using Bit.Api.Models.Response;
using System.Linq;
namespace Bit.Api.Utilities
{
public class ModelStateValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var model = context.ActionArguments.FirstOrDefault(a => a.Key == "model");
if(model.Key == "model" && model.Value == null)
{
context.ModelState.AddModelError(string.Empty, "Body is empty.");
}
if(!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new ErrorResponseModel(context.ModelState));
}
}
}
}

42
src/Api/project.json Normal file
View File

@ -0,0 +1,42 @@
{
"userSecretsId": "aspnet5-bitwarden-Api",
"version": "0.0.1-*",
"compilationOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Core": {
"version": "0.0.1",
"target": "project"
},
"Microsoft.AspNet.DataProtection.Extensions": "1.0.0-rc1-final",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
"Microsoft.AspNet.Cors": "6.0.0-rc1-final",
"Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final"
},
"commands": {
"web": "Microsoft.AspNet.Server.Kestrel"
},
"frameworks": {
"dnx451": { }
},
"exclude": [
"wwwroot",
"node_modules"
],
"publishExclude": [
"**.user",
"**.vspscc"
]
}

View File

@ -0,0 +1,5 @@
{
"globalSettings": {
"baseVaultUri": "https://vault.bitwarden.com"
}
}

View File

@ -0,0 +1,5 @@
{
"globalSettings": {
"baseVaultUri": "https://vault.bitwarden.com"
}
}

17
src/Api/settings.json Normal file
View File

@ -0,0 +1,17 @@
{
"globalSettings": {
"siteName": "bitwarden",
"baseVaultUri": "http://localhost:4001",
"documentDB": {
"uri": "SECRET",
"key": "SECRET",
"databaseId": "SECRET",
"collectionIdPrefix": "SECRET",
"numberOfCollections": 1
},
"mail": {
"apiKey": "SECRET",
"replyToEmail": "do-not-reply@bitwarden.com"
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="httpPlatformHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
</handlers>
<httpPlatform processPath="%DNX_PATH%" arguments="%DNX_ARGS%" stdoutLogEnabled="false" startupTimeLimit="3600"/>
</system.webServer>
</configuration>