mirror of
https://github.com/bitwarden/server.git
synced 2026-02-09 02:13:11 +08:00
* Get limited life attachment download URL This change limits url download to a 1min lifetime. This requires moving to a new container to allow for non-public blob access. Clients will have to call GetAttachmentData api function to receive the download URL. For backwards compatibility, attachment URLs are still present, but will not work for attachments stored in non-public access blobs. * Make GlobalSettings interface for testing * Test LocalAttachmentStorageService equivalence * Remove comment * Add missing globalSettings using * Simplify default attachment container * Default to attachments containe for existing methods A new upload method will be made for uploading to attachments-v2. For compatibility for clients which don't use these new methods, we need to still use the old container. The new container will be used only for new uploads * Remove Default MetaData fixture. * Keep attachments container blob-level security for all instances * Close unclosed FileStream * Favor default value for noop services
413 lines
16 KiB
C#
413 lines
16 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Bit.Core.Models.Api;
|
|
using Bit.Core.Exceptions;
|
|
using Bit.Core.Services;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Bit.Core.Models.Table;
|
|
using Bit.Core.Enums;
|
|
using System.Linq;
|
|
using Bit.Core.Context;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Core.Utilities.Duo;
|
|
using Bit.Core.Settings;
|
|
|
|
namespace Bit.Api.Controllers
|
|
{
|
|
[Route("two-factor")]
|
|
[Authorize("Web")]
|
|
public class TwoFactorController : Controller
|
|
{
|
|
private readonly IUserService _userService;
|
|
private readonly IOrganizationRepository _organizationRepository;
|
|
private readonly IOrganizationService _organizationService;
|
|
private readonly GlobalSettings _globalSettings;
|
|
private readonly UserManager<User> _userManager;
|
|
private readonly ICurrentContext _currentContext;
|
|
|
|
public TwoFactorController(
|
|
IUserService userService,
|
|
IOrganizationRepository organizationRepository,
|
|
IOrganizationService organizationService,
|
|
GlobalSettings globalSettings,
|
|
UserManager<User> userManager,
|
|
ICurrentContext currentContext)
|
|
{
|
|
_userService = userService;
|
|
_organizationRepository = organizationRepository;
|
|
_organizationService = organizationService;
|
|
_globalSettings = globalSettings;
|
|
_userManager = userManager;
|
|
_currentContext = currentContext;
|
|
}
|
|
|
|
[HttpGet("")]
|
|
public async Task<ListResponseModel<TwoFactorProviderResponseModel>> Get()
|
|
{
|
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
if (user == null)
|
|
{
|
|
throw new UnauthorizedAccessException();
|
|
}
|
|
|
|
var providers = user.GetTwoFactorProviders()?.Select(
|
|
p => new TwoFactorProviderResponseModel(p.Key, p.Value));
|
|
return new ListResponseModel<TwoFactorProviderResponseModel>(providers);
|
|
}
|
|
|
|
[HttpGet("~/organizations/{id}/two-factor")]
|
|
public async Task<ListResponseModel<TwoFactorProviderResponseModel>> GetOrganization(string id)
|
|
{
|
|
var orgIdGuid = new Guid(id);
|
|
if (!_currentContext.OrganizationAdmin(orgIdGuid))
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var providers = organization.GetTwoFactorProviders()?.Select(
|
|
p => new TwoFactorProviderResponseModel(p.Key, p.Value));
|
|
return new ListResponseModel<TwoFactorProviderResponseModel>(providers);
|
|
}
|
|
|
|
[HttpPost("get-authenticator")]
|
|
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator([FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
var response = new TwoFactorAuthenticatorResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPut("authenticator")]
|
|
[HttpPost("authenticator")]
|
|
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
|
|
[FromBody]UpdateTwoFactorAuthenticatorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
model.ToUser(user);
|
|
|
|
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))
|
|
{
|
|
await Task.Delay(2000);
|
|
throw new BadRequestException("Token", "Invalid token.");
|
|
}
|
|
|
|
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Authenticator);
|
|
var response = new TwoFactorAuthenticatorResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("get-yubikey")]
|
|
public async Task<TwoFactorYubiKeyResponseModel> GetYubiKey([FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
var response = new TwoFactorYubiKeyResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPut("yubikey")]
|
|
[HttpPost("yubikey")]
|
|
public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody]UpdateTwoFactorYubicoOtpRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
model.ToUser(user);
|
|
|
|
await ValidateYubiKeyAsync(user, nameof(model.Key1), model.Key1);
|
|
await ValidateYubiKeyAsync(user, nameof(model.Key2), model.Key2);
|
|
await ValidateYubiKeyAsync(user, nameof(model.Key3), model.Key3);
|
|
await ValidateYubiKeyAsync(user, nameof(model.Key4), model.Key4);
|
|
await ValidateYubiKeyAsync(user, nameof(model.Key5), model.Key5);
|
|
|
|
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.YubiKey);
|
|
var response = new TwoFactorYubiKeyResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("get-duo")]
|
|
public async Task<TwoFactorDuoResponseModel> GetDuo([FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
var response = new TwoFactorDuoResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPut("duo")]
|
|
[HttpPost("duo")]
|
|
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody]UpdateTwoFactorDuoRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
try
|
|
{
|
|
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
|
duoApi.JSONApiCall<object>("GET", "/auth/v2/check");
|
|
}
|
|
catch (DuoException)
|
|
{
|
|
throw new BadRequestException("Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
|
}
|
|
|
|
model.ToUser(user);
|
|
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Duo);
|
|
var response = new TwoFactorDuoResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("~/organizations/{id}/two-factor/get-duo")]
|
|
public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,
|
|
[FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
|
|
var orgIdGuid = new Guid(id);
|
|
if (!_currentContext.ManagePolicies(orgIdGuid))
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var response = new TwoFactorDuoResponseModel(organization);
|
|
return response;
|
|
}
|
|
|
|
[HttpPut("~/organizations/{id}/two-factor/duo")]
|
|
[HttpPost("~/organizations/{id}/two-factor/duo")]
|
|
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
|
|
[FromBody]UpdateTwoFactorDuoRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
|
|
var orgIdGuid = new Guid(id);
|
|
if (!_currentContext.ManagePolicies(orgIdGuid))
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
try
|
|
{
|
|
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
|
duoApi.JSONApiCall<object>("GET", "/auth/v2/check");
|
|
}
|
|
catch (DuoException)
|
|
{
|
|
throw new BadRequestException("Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
|
}
|
|
|
|
model.ToOrganization(organization);
|
|
await _organizationService.UpdateTwoFactorProviderAsync(organization,
|
|
TwoFactorProviderType.OrganizationDuo);
|
|
var response = new TwoFactorDuoResponseModel(organization);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("get-u2f")]
|
|
public async Task<TwoFactorU2fResponseModel> GetU2f([FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
var response = new TwoFactorU2fResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("get-u2f-challenge")]
|
|
public async Task<TwoFactorU2fResponseModel.ChallengeModel> GetU2fChallenge(
|
|
[FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
var reg = await _userService.StartU2fRegistrationAsync(user);
|
|
var challenge = new TwoFactorU2fResponseModel.ChallengeModel(user, reg);
|
|
return challenge;
|
|
}
|
|
|
|
[HttpPut("u2f")]
|
|
[HttpPost("u2f")]
|
|
public async Task<TwoFactorU2fResponseModel> PutU2f([FromBody]TwoFactorU2fRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
var success = await _userService.CompleteU2fRegistrationAsync(
|
|
user, model.Id.Value, model.Name, model.DeviceResponse);
|
|
if (!success)
|
|
{
|
|
throw new BadRequestException("Unable to complete U2F key registration.");
|
|
}
|
|
var response = new TwoFactorU2fResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpDelete("u2f")]
|
|
public async Task<TwoFactorU2fResponseModel> DeleteU2f([FromBody]TwoFactorU2fDeleteRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, true);
|
|
await _userService.DeleteU2fKeyAsync(user, model.Id.Value);
|
|
var response = new TwoFactorU2fResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("get-email")]
|
|
public async Task<TwoFactorEmailResponseModel> GetEmail([FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
var response = new TwoFactorEmailResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("send-email")]
|
|
public async Task SendEmail([FromBody]TwoFactorEmailRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
model.ToUser(user);
|
|
await _userService.SendTwoFactorEmailAsync(user);
|
|
}
|
|
|
|
[AllowAnonymous]
|
|
[HttpPost("send-email-login")]
|
|
public async Task SendEmailLogin([FromBody]TwoFactorEmailRequestModel model)
|
|
{
|
|
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
|
|
if (user != null)
|
|
{
|
|
if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
|
{
|
|
await _userService.SendTwoFactorEmailAsync(user);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await Task.Delay(2000);
|
|
throw new BadRequestException("Cannot send two-factor email.");
|
|
}
|
|
|
|
[HttpPut("email")]
|
|
[HttpPost("email")]
|
|
public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody]UpdateTwoFactorEmailRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
model.ToUser(user);
|
|
|
|
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token))
|
|
{
|
|
await Task.Delay(2000);
|
|
throw new BadRequestException("Token", "Invalid token.");
|
|
}
|
|
|
|
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
|
var response = new TwoFactorEmailResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPut("disable")]
|
|
[HttpPost("disable")]
|
|
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody]TwoFactorProviderRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
|
|
var response = new TwoFactorProviderResponseModel(model.Type.Value, user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPut("~/organizations/{id}/two-factor/disable")]
|
|
[HttpPost("~/organizations/{id}/two-factor/disable")]
|
|
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
|
|
[FromBody]TwoFactorProviderRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
|
|
var orgIdGuid = new Guid(id);
|
|
if (!_currentContext.ManagePolicies(orgIdGuid))
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
await _organizationService.DisableTwoFactorProviderAsync(organization, model.Type.Value);
|
|
var response = new TwoFactorProviderResponseModel(model.Type.Value, organization);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("get-recover")]
|
|
public async Task<TwoFactorRecoverResponseModel> GetRecover([FromBody]TwoFactorRequestModel model)
|
|
{
|
|
var user = await CheckAsync(model.MasterPasswordHash, false);
|
|
var response = new TwoFactorRecoverResponseModel(user);
|
|
return response;
|
|
}
|
|
|
|
[HttpPost("recover")]
|
|
[AllowAnonymous]
|
|
public async Task PostRecover([FromBody]TwoFactorRecoveryRequestModel model)
|
|
{
|
|
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode,
|
|
_organizationService))
|
|
{
|
|
await Task.Delay(2000);
|
|
throw new BadRequestException(string.Empty, "Invalid information. Try again.");
|
|
}
|
|
}
|
|
|
|
private async Task<User> CheckAsync(string masterPasswordHash, bool premium)
|
|
{
|
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
if (user == null)
|
|
{
|
|
throw new UnauthorizedAccessException();
|
|
}
|
|
|
|
if (!await _userService.CheckPasswordAsync(user, masterPasswordHash))
|
|
{
|
|
await Task.Delay(2000);
|
|
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
|
}
|
|
|
|
if (premium && !(await _userService.CanAccessPremium(user)))
|
|
{
|
|
throw new BadRequestException("Premium status is required.");
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
private async Task ValidateYubiKeyAsync(User user, string name, string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value) || value.Length == 12)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value))
|
|
{
|
|
await Task.Delay(2000);
|
|
throw new BadRequestException(name, $"{name} is invalid.");
|
|
}
|
|
else
|
|
{
|
|
await Task.Delay(500);
|
|
}
|
|
}
|
|
}
|
|
}
|