mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +01:00
initial commit of source
This commit is contained in:
commit
437b971003
199
.gitignore
vendored
Normal file
199
.gitignore
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Visual Studo 2015 cache/options directory
|
||||
.vs/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding addin-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Others
|
||||
*.[Cc]ache
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Other
|
||||
project.lock.json
|
8
NuGet.Config
Normal file
8
NuGet.Config
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
<clear />
|
||||
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
42
bitwarden.sln
Normal file
42
bitwarden.sln
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.23107.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{458155D3-BCBC-481D-B37A-40D2ED10F0A4}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitignore = .gitignore
|
||||
global.json = global.json
|
||||
NuGet.Config = NuGet.Config
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Core", "src\Core\Core.xproj", "{3973D21B-A692-4B60-9B70-3631C057423A}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Api", "src\Api\Api.xproj", "{E8548AD6-7FB0-439A-8EB5-549A10336D2D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{3973D21B-A692-4B60-9B70-3631C057423A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3973D21B-A692-4B60-9B70-3631C057423A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3973D21B-A692-4B60-9B70-3631C057423A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3973D21B-A692-4B60-9B70-3631C057423A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{3973D21B-A692-4B60-9B70-3631C057423A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||
{E8548AD6-7FB0-439A-8EB5-549A10336D2D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
6
global.json
Normal file
6
global.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"projects": [ "src", "test" ],
|
||||
"sdk": {
|
||||
"version": "1.0.0-rc1-final"
|
||||
}
|
||||
}
|
19
src/Api/Api.xproj
Normal file
19
src/Api/Api.xproj
Normal 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>
|
210
src/Api/Controllers/AccountsController.cs
Normal file
210
src/Api/Controllers/AccountsController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
62
src/Api/Controllers/AuthController.cs
Normal file
62
src/Api/Controllers/AuthController.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
78
src/Api/Controllers/FoldersController.cs
Normal file
78
src/Api/Controllers/FoldersController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
148
src/Api/Controllers/SitesController.cs
Normal file
148
src/Api/Controllers/SitesController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
19
src/Api/Models/Request/Accounts/EmailRequestModel.cs
Normal file
19
src/Api/Models/Request/Accounts/EmailRequestModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
13
src/Api/Models/Request/Accounts/EmailTokenRequestModel.cs
Normal file
13
src/Api/Models/Request/Accounts/EmailTokenRequestModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
11
src/Api/Models/Request/Accounts/PasswordHintRequestModel.cs
Normal file
11
src/Api/Models/Request/Accounts/PasswordHintRequestModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Models
|
||||
{
|
||||
public class PasswordHintRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
14
src/Api/Models/Request/Accounts/PasswordRequestModel.cs
Normal file
14
src/Api/Models/Request/Accounts/PasswordRequestModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
29
src/Api/Models/Request/Accounts/RegisterRequestModel.cs
Normal file
29
src/Api/Models/Request/Accounts/RegisterRequestModel.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
11
src/Api/Models/Request/Accounts/RegisterTokenRequestModel.cs
Normal file
11
src/Api/Models/Request/Accounts/RegisterTokenRequestModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Models
|
||||
{
|
||||
public class RegisterTokenRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
10
src/Api/Models/Request/Accounts/SecurityStampRequestModel.cs
Normal file
10
src/Api/Models/Request/Accounts/SecurityStampRequestModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Models
|
||||
{
|
||||
public class SecurityStampRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
24
src/Api/Models/Request/Accounts/UpdateProfileRequestModel.cs
Normal file
24
src/Api/Models/Request/Accounts/UpdateProfileRequestModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
src/Api/Models/Request/Auth/AuthTokenRequestModel.cs
Normal file
13
src/Api/Models/Request/Auth/AuthTokenRequestModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
85
src/Api/Models/Request/Ciphers/CipherRequestModel.cs
Normal file
85
src/Api/Models/Request/Ciphers/CipherRequestModel.cs
Normal 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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
src/Api/Models/Request/Folders/FolderRequestModel.cs
Normal file
30
src/Api/Models/Request/Folders/FolderRequestModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
52
src/Api/Models/Request/Sites/SiteRequestModel.cs
Normal file
52
src/Api/Models/Request/Sites/SiteRequestModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
18
src/Api/Models/Response/AuthTokenResponseModel.cs
Normal file
18
src/Api/Models/Response/AuthTokenResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
51
src/Api/Models/Response/ErrorResponseModel.cs
Normal file
51
src/Api/Models/Response/ErrorResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
23
src/Api/Models/Response/FolderResponseModel.cs
Normal file
23
src/Api/Models/Response/FolderResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
16
src/Api/Models/Response/ListResponseModel.cs
Normal file
16
src/Api/Models/Response/ListResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
31
src/Api/Models/Response/ProfileResponseModel.cs
Normal file
31
src/Api/Models/Response/ProfileResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
19
src/Api/Models/Response/ResponseModel.cs
Normal file
19
src/Api/Models/Response/ResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
36
src/Api/Models/Response/SiteResponseModel.cs
Normal file
36
src/Api/Models/Response/SiteResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
26
src/Api/Models/Response/TwoFactorResponseModel.cs
Normal file
26
src/Api/Models/Response/TwoFactorResponseModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
23
src/Api/Properties/AssemblyInfo.cs
Normal file
23
src/Api/Properties/AssemblyInfo.cs
Normal 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")]
|
25
src/Api/Properties/launchSettings.json
Normal file
25
src/Api/Properties/launchSettings.json
Normal 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
157
src/Api/Startup.cs
Normal 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);
|
||||
}
|
||||
}
|
52
src/Api/Utilities/EncryptedValueAttribute.cs
Normal file
52
src/Api/Utilities/EncryptedValueAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
65
src/Api/Utilities/ExceptionHandlerFilterAttribute.cs
Normal file
65
src/Api/Utilities/ExceptionHandlerFilterAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
24
src/Api/Utilities/ModelStateValidationFilterAttribute.cs
Normal file
24
src/Api/Utilities/ModelStateValidationFilterAttribute.cs
Normal 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
42
src/Api/project.json
Normal 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"
|
||||
]
|
||||
}
|
5
src/Api/settings.Production.json
Normal file
5
src/Api/settings.Production.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"globalSettings": {
|
||||
"baseVaultUri": "https://vault.bitwarden.com"
|
||||
}
|
||||
}
|
5
src/Api/settings.Staging.json
Normal file
5
src/Api/settings.Staging.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"globalSettings": {
|
||||
"baseVaultUri": "https://vault.bitwarden.com"
|
||||
}
|
||||
}
|
17
src/Api/settings.json
Normal file
17
src/Api/settings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
9
src/Api/wwwroot/web.config
Normal file
9
src/Api/wwwroot/web.config
Normal 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>
|
18
src/Core/Core.xproj
Normal file
18
src/Core/Core.xproj
Normal 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>
|
13
src/Core/CurrentContext.cs
Normal file
13
src/Core/CurrentContext.cs
Normal 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; }
|
||||
}
|
||||
}
|
23
src/Core/Domains/Cipher.cs
Normal file
23
src/Core/Domains/Cipher.cs
Normal 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;
|
||||
}
|
||||
}
|
9
src/Core/Domains/Folder.cs
Normal file
9
src/Core/Domains/Folder.cs
Normal 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
10
src/Core/Domains/Role.cs
Normal 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
16
src/Core/Domains/Site.cs
Normal 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
30
src/Core/Domains/User.cs
Normal 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;
|
||||
}
|
||||
}
|
8
src/Core/Enums/CipherType.cs
Normal file
8
src/Core/Enums/CipherType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum CipherType
|
||||
{
|
||||
Folder = 0,
|
||||
Site = 1
|
||||
}
|
||||
}
|
7
src/Core/Enums/TwoFactorProvider.cs
Normal file
7
src/Core/Enums/TwoFactorProvider.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum TwoFactorProvider
|
||||
{
|
||||
Authenticator = 0
|
||||
}
|
||||
}
|
30
src/Core/Exceptions/BadRequestException.cs
Normal file
30
src/Core/Exceptions/BadRequestException.cs
Normal 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; }
|
||||
}
|
||||
}
|
6
src/Core/Exceptions/NotFoundException.cs
Normal file
6
src/Core/Exceptions/NotFoundException.cs
Normal file
@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.Core.Exceptions
|
||||
{
|
||||
public class NotFoundException : Exception { }
|
||||
}
|
28
src/Core/GlobalSettings.cs
Normal file
28
src/Core/GlobalSettings.cs
Normal 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
12
src/Core/IDataObject.cs
Normal 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; }
|
||||
}
|
||||
}
|
43
src/Core/Identity/AuthenticatorTokenProvider.cs
Normal file
43
src/Core/Identity/AuthenticatorTokenProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
62
src/Core/Identity/JwtBearerBuilderExtensions.cs
Normal file
62
src/Core/Identity/JwtBearerBuilderExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
50
src/Core/Identity/JwtBearerEventImplementations.cs
Normal file
50
src/Core/Identity/JwtBearerEventImplementations.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
16
src/Core/Identity/JwtBearerIdentityOptions.cs
Normal file
16
src/Core/Identity/JwtBearerIdentityOptions.cs
Normal 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";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
160
src/Core/Identity/JwtBearerSignInManager.cs
Normal file
160
src/Core/Identity/JwtBearerSignInManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
34
src/Core/Identity/JwtBearerSignInResult.cs
Normal file
34
src/Core/Identity/JwtBearerSignInResult.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
12
src/Core/Identity/LowerInvariantLookupNormalizer.cs
Normal file
12
src/Core/Identity/LowerInvariantLookupNormalizer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
64
src/Core/Identity/RoleStore.cs
Normal file
64
src/Core/Identity/RoleStore.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
160
src/Core/Identity/UserStore.cs
Normal file
160
src/Core/Identity/UserStore.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
23
src/Core/Properties/AssemblyInfo.cs
Normal file
23
src/Core/Properties/AssemblyInfo.cs
Normal 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")]
|
67
src/Core/Repositories/DocumentDB/BaseRepository.cs
Normal file
67
src/Core/Repositories/DocumentDB/BaseRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
40
src/Core/Repositories/DocumentDB/CipherRepository.cs
Normal file
40
src/Core/Repositories/DocumentDB/CipherRepository.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
src/Core/Repositories/DocumentDB/FolderRepository.cs
Normal file
49
src/Core/Repositories/DocumentDB/FolderRepository.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
76
src/Core/Repositories/DocumentDB/Repository.cs
Normal file
76
src/Core/Repositories/DocumentDB/Repository.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
50
src/Core/Repositories/DocumentDB/SiteRepository.cs
Normal file
50
src/Core/Repositories/DocumentDB/SiteRepository.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
37
src/Core/Repositories/DocumentDB/UserRepository.cs
Normal file
37
src/Core/Repositories/DocumentDB/UserRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
10
src/Core/Repositories/ICipherRepository.cs
Normal file
10
src/Core/Repositories/ICipherRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
13
src/Core/Repositories/IFolderRepository.cs
Normal file
13
src/Core/Repositories/IFolderRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
14
src/Core/Repositories/IRepository.cs
Normal file
14
src/Core/Repositories/IRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
14
src/Core/Repositories/ISiteRepository.cs
Normal file
14
src/Core/Repositories/ISiteRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
13
src/Core/Repositories/IUserRepository.cs
Normal file
13
src/Core/Repositories/IUserRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
16
src/Core/Services/IMailService.cs
Normal file
16
src/Core/Services/IMailService.cs
Normal 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);
|
||||
}
|
||||
}
|
22
src/Core/Services/IUserService.cs
Normal file
22
src/Core/Services/IUserService.cs
Normal 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);
|
||||
}
|
||||
}
|
138
src/Core/Services/MailService.cs
Normal file
138
src/Core/Services/MailService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
308
src/Core/Services/UserService.cs
Normal file
308
src/Core/Services/UserService.cs
Normal 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
23
src/Core/project.json
Normal 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": { }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user