mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
initial commit of source
This commit is contained in:
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>
|
Reference in New Issue
Block a user