1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

[Pm 3797 Part 2] Add emergency access rotations (#3434)

## Type of change

<!-- (mark with an `X`) -->

```
- [ ] Bug fix
- [ ] New feature development
- [x] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
See #3425 for part 1 and background.

This PR adds emergency access to the rotation. All new code is hidden behind a feature flag.

The Accounts controller has also been moved to Auth ownership.

## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->

* **file.ext:** Description of what was changed and why
* **AccountsController.cs:** Moved to Auth ownership. Emergency access validation was added (as well as initializing empty lists to avoid errors).
* **EmergencyAccessRotationValidator.cs:** Performs validation on the provided list of new emergency access keys.
* **EmergencyAccessRepository.cs:** Adds a method to rotate encryption keys. This is added to a list in the `RotateUserKeyCommand` that the `UserRepository` calls so it doesn't have to know about all the domains.

## Before you submit

- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
- If making database changes - make sure you also update Entity Framework queries and/or migrations
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
- If this change requires a **documentation update** - notify the documentation team
- If this change has particular **deployment requirements** - notify the DevOps team
This commit is contained in:
Jake Fink
2023-12-05 12:05:51 -05:00
committed by GitHub
parent eedc96263a
commit 989603ddd3
19 changed files with 423 additions and 39 deletions

View File

@ -0,0 +1,962 @@
using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Validators;
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Auth.Utilities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("accounts")]
[Authorize("Application")]
public class AccountsController : Controller
{
private readonly GlobalSettings _globalSettings;
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IPaymentService _paymentService;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
private readonly ISendRepository _sendRepository;
private readonly ISendService _sendService;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator;
public AccountsController(
GlobalSettings globalSettings,
ICipherRepository cipherRepository,
IFolderRepository folderRepository,
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
IPaymentService paymentService,
IUserRepository userRepository,
IUserService userService,
ISendRepository sendRepository,
ISendService sendService,
ICaptchaValidationService captchaValidationService,
IPolicyService policyService,
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
IRotateUserKeyCommand rotateUserKeyCommand,
IFeatureService featureService,
ICurrentContext currentContext,
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
emergencyAccessValidator
)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_globalSettings = globalSettings;
_organizationService = organizationService;
_organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository;
_paymentService = paymentService;
_userRepository = userRepository;
_userService = userService;
_sendRepository = sendRepository;
_sendService = sendService;
_captchaValidationService = captchaValidationService;
_policyService = policyService;
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
_rotateUserKeyCommand = rotateUserKeyCommand;
_featureService = featureService;
_currentContext = currentContext;
_emergencyAccessValidator = emergencyAccessValidator;
}
#region DEPRECATED (Moved to Identity Service)
[Obsolete("TDL-136 Moved to Identity (2022-01-12 cloud, 2022-09-19 self-hosted), left for backwards compatability with older clients.")]
[HttpPost("prelogin")]
[AllowAnonymous]
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
{
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
if (kdfInformation == null)
{
kdfInformation = new UserKdfInformation
{
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
};
}
return new PreloginResponseModel(kdfInformation);
}
[Obsolete("TDL-136 Moved to Identity (2022-01-12 cloud, 2022-09-19 self-hosted), left for backwards compatability with older clients.")]
[HttpPost("register")]
[AllowAnonymous]
[CaptchaProtected]
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
{
var user = model.ToUser();
var result = await _userService.RegisterUserAsync(user, model.MasterPasswordHash,
model.Token, model.OrganizationUserId);
if (result.Succeeded)
{
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
return new RegisterResponseModel(captchaBypassToken);
}
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
#endregion
[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)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
}
[HttpPost("email")]
public async Task PostEmail([FromBody] EmailRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
model.NewMasterPasswordHash, model.Token, model.Key);
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("verify-email")]
public async Task PostVerifyEmail()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.SendEmailVerificationAsync(user);
}
[HttpPost("verify-email-token")]
[AllowAnonymous]
public async Task PostVerifyEmailToken([FromBody] VerifyEmailRequestModel model)
{
var user = await _userService.GetUserByIdAsync(new Guid(model.UserId));
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ConfirmEmailAsync(user, model.Token);
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")]
public async Task PostPassword([FromBody] PasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ChangePasswordAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.MasterPasswordHint, model.Key);
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("set-password")]
public async Task PostSetPasswordAsync([FromBody] SetPasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
model.ToUser(user),
model.MasterPasswordHash,
model.Key,
model.OrgIdentifier);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("verify-password")]
public async Task<MasterPasswordPolicyResponseModel> PostVerifyPassword([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
var policyData = await _policyService.GetMasterPasswordPolicyForUserAsync(user);
return new MasterPasswordPolicyResponseModel(policyData);
}
ModelState.AddModelError(nameof(model.MasterPasswordHash), "Invalid password.");
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpPost("set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("convert-to-key-connector")]
public async Task PostConvertToKeyConnector()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ConvertToKeyConnectorAsync(user);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("kdf")]
public async Task PostKdf([FromBody] KdfRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ChangeKdfAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.Key, model.Kdf.Value, model.KdfIterations.Value, model.KdfMemory, model.KdfParallelism);
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("key")]
public async Task PostKey([FromBody] UpdateKeyRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
IdentityResult result;
if (_featureService.IsEnabled(FeatureFlagKeys.KeyRotationImprovements, _currentContext))
{
var dataModel = new RotateUserKeyData
{
MasterPasswordHash = model.MasterPasswordHash,
Key = model.Key,
PrivateKey = model.PrivateKey,
Ciphers = new List<Cipher>(),
Folders = new List<Folder>(),
Sends = new List<Send>(),
EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
ResetPasswordKeys = new List<OrganizationUser>(),
};
result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel);
}
else
{
var ciphers = new List<Cipher>();
if (model.Ciphers.Any())
{
var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: UseFlexibleCollections);
ciphers.AddRange(existingCiphers
.Join(model.Ciphers, c => c.Id, c => c.Id, (existing, c) => c.ToCipher(existing)));
}
var folders = new List<Folder>();
if (model.Folders.Any())
{
var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id);
folders.AddRange(existingFolders
.Join(model.Folders, f => f.Id, f => f.Id, (existing, f) => f.ToFolder(existing)));
}
var sends = new List<Send>();
if (model.Sends?.Any() == true)
{
var existingSends = await _sendRepository.GetManyByUserIdAsync(user.Id);
sends.AddRange(existingSends
.Join(model.Sends, s => s.Id, s => s.Id, (existing, s) => s.ToSend(existing, _sendService)));
}
result = await _userService.UpdateKeyAsync(
user,
model.MasterPasswordHash,
model.Key,
model.PrivateKey,
ciphers,
folders,
sends);
}
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("security-stamp")]
public async Task PostSecurityStamp([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.RefreshSecurityStampAsync(user, model.Secret);
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 async Task<ProfileResponseModel> GetProfile()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id,
OrganizationUserStatusType.Confirmed);
var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync(user.Id,
ProviderUserStatusType.Confirmed);
var providerUserOrganizationDetails =
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
ProviderUserStatusType.Confirmed);
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg);
return response;
}
[HttpGet("organizations")]
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetOrganizations()
{
var userId = _userService.GetProperUserId(User);
var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value,
OrganizationUserStatusType.Confirmed);
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o));
return new ListResponseModel<ProfileOrganizationResponseModel>(responseData);
}
[HttpPut("profile")]
[HttpPost("profile")]
public async Task<ProfileResponseModel> PutProfile([FromBody] UpdateProfileRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.SaveUserAsync(model.ToUser(user));
var response = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user));
return response;
}
[HttpPut("avatar")]
[HttpPost("avatar")]
public async Task<ProfileResponseModel> PutAvatar([FromBody] UpdateAvatarRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.SaveUserAsync(model.ToUser(user), true);
var response = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user));
return response;
}
[HttpGet("revision-date")]
public async Task<long?> GetAccountRevisionDate()
{
var userId = _userService.GetProperUserId(User);
long? revisionDate = null;
if (userId.HasValue)
{
var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);
revisionDate = CoreHelpers.ToEpocMilliseconds(date);
}
return revisionDate;
}
[HttpPost("keys")]
public async Task<KeysResponseModel> PostKeys([FromBody] KeysRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.SaveUserAsync(model.ToUser(user));
return new KeysResponseModel(user);
}
[HttpGet("keys")]
public async Task<KeysResponseModel> GetKeys()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
return new KeysResponseModel(user);
}
[HttpDelete]
[HttpPost("delete")]
public async Task Delete([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
ModelState.AddModelError(string.Empty, "User verification failed.");
await Task.Delay(2000);
}
else
{
var result = await _userService.DeleteAsync(user);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
throw new BadRequestException(ModelState);
}
[AllowAnonymous]
[HttpPost("delete-recover")]
public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model)
{
await _userService.SendDeleteConfirmationAsync(model.Email);
}
[HttpPost("delete-recover-token")]
[AllowAnonymous]
public async Task PostDeleteRecoverToken([FromBody] VerifyDeleteRecoverRequestModel model)
{
var user = await _userService.GetUserByIdAsync(new Guid(model.UserId));
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.DeleteAsync(user, model.Token);
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("iap-check")]
public async Task PostIapCheck([FromBody] IapCheckRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.IapCheckAsync(user, model.PaymentMethodType.Value);
}
[HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremium(PremiumRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var valid = model.Validate(_globalSettings);
UserLicense license = null;
if (valid && _globalSettings.SelfHosted)
{
license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
}
if (!valid && !_globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country))
{
throw new BadRequestException("Country is required.");
}
if (!valid || (_globalSettings.SelfHosted && license == null))
{
throw new BadRequestException("Invalid license.");
}
var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken,
model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
new TaxInfo
{
BillingAddressCountry = model.Country,
BillingAddressPostalCode = model.PostalCode,
});
var profile = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user));
return new PaymentResponseModel
{
UserProfile = profile,
PaymentIntentClientSecret = result.Item2,
Success = result.Item1
};
}
[HttpGet("subscription")]
public async Task<SubscriptionResponseModel> GetSubscription()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!_globalSettings.SelfHosted && user.Gateway != null)
{
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(user);
var license = await _userService.GenerateLicenseAsync(user, subscriptionInfo);
return new SubscriptionResponseModel(user, subscriptionInfo, license);
}
else if (!_globalSettings.SelfHosted)
{
var license = await _userService.GenerateLicenseAsync(user);
return new SubscriptionResponseModel(user, license);
}
else
{
return new SubscriptionResponseModel(user);
}
}
[HttpPost("payment")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostPayment([FromBody] PaymentRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType.Value,
new TaxInfo
{
BillingAddressCountry = model.Country,
BillingAddressPostalCode = model.PostalCode,
});
}
[HttpPost("storage")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> PostStorage([FromBody] StorageRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.AdjustStorageAsync(user, model.StorageGbAdjustment.Value);
return new PaymentResponseModel
{
Success = true,
PaymentIntentClientSecret = result
};
}
[HttpPost("license")]
[SelfHosted(SelfHostedOnly = true)]
public async Task PostLicense(LicenseRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
if (license == null)
{
throw new BadRequestException("Invalid license");
}
await _userService.UpdateLicenseAsync(user, license);
}
[HttpPost("cancel-premium")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostCancel()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.CancelPremiumAsync(user);
}
[HttpPost("reinstate-premium")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostReinstate()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.ReinstatePremiumAsync(user);
}
[HttpGet("tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<TaxInfoResponseModel> GetTaxInfo()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var taxInfo = await _paymentService.GetTaxInfoAsync(user);
return new TaxInfoResponseModel(taxInfo);
}
[HttpPut("tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PutTaxInfo([FromBody] TaxInfoUpdateRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var taxInfo = new TaxInfo
{
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
};
await _paymentService.SaveTaxInfoAsync(user, taxInfo);
}
[HttpDelete("sso/{organizationId}")]
public async Task DeleteSsoUser(string organizationId)
{
var userId = _userService.GetProperUserId(User);
if (!userId.HasValue)
{
throw new NotFoundException();
}
await _organizationService.DeleteSsoUserAsync(userId.Value, new Guid(organizationId));
}
[HttpGet("sso/user-identifier")]
public async Task<string> GetSsoUserIdentifier()
{
var user = await _userService.GetUserByPrincipalAsync(User);
var token = await _userService.GenerateSignInTokenAsync(user, TokenPurposes.LinkSso);
var userIdentifier = $"{user.Id},{token}";
return userIdentifier;
}
[HttpPost("api-key")]
public async Task<ApiKeyResponseModel> ApiKey([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
return new ApiKeyResponseModel(user);
}
[HttpPost("rotate-api-key")]
public async Task<ApiKeyResponseModel> RotateApiKey([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
await _userService.RotateApiKeyAsync(user);
var response = new ApiKeyResponseModel(user);
return response;
}
[HttpPut("update-temp-password")]
public async Task PutUpdateTempPasswordAsync([FromBody] UpdateTempPasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.UpdateTempPasswordAsync(user, model.NewMasterPasswordHash, model.Key, model.MasterPasswordHint);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("request-otp")]
public async Task PostRequestOTP()
{
var user = await _userService.GetUserByPrincipalAsync(User);
await _userService.SendOTPAsync(user);
}
[HttpPost("verify-otp")]
public async Task VerifyOTP([FromBody] VerifyOTPRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (!await _userService.VerifyOTPAsync(user, model.OTP))
{
await Task.Delay(2000);
throw new BadRequestException("Token", "Invalid token");
}
}
}