mirror of
https://github.com/bitwarden/server.git
synced 2025-04-07 14:08:13 -05:00
premium signup with license file
This commit is contained in:
parent
02bb037e38
commit
73029f76d2
@ -9,14 +9,6 @@
|
|||||||
<DockerComposeProjectPath>..\..\docker\Docker.dcproj</DockerComposeProjectPath>
|
<DockerComposeProjectPath>..\..\docker\Docker.dcproj</DockerComposeProjectPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="licensing.cer" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="licensing.cer" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -12,6 +12,9 @@ using System.Linq;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
namespace Bit.Api.Controllers
|
namespace Bit.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -378,7 +381,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<ProfileResponseModel> PostPremium([FromBody]PremiumRequestModel model)
|
public async Task<ProfileResponseModel> PostPremium(PremiumRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if(user == null)
|
if(user == null)
|
||||||
@ -386,7 +389,32 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userService.SignUpPremiumAsync(user, model.PaymentToken, model.AdditionalStorageGb.GetValueOrDefault(0));
|
var valid = model.Validate(_globalSettings);
|
||||||
|
UserLicense license = null;
|
||||||
|
if(valid && model.License != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var stream = model.License.OpenReadStream())
|
||||||
|
using(var reader = new StreamReader(stream))
|
||||||
|
{
|
||||||
|
var s = await reader.ReadToEndAsync();
|
||||||
|
license = JsonConvert.DeserializeObject<UserLicense>(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!valid)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid license.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userService.SignUpPremiumAsync(user, model.PaymentToken,
|
||||||
|
model.AdditionalStorageGb.GetValueOrDefault(0), license);
|
||||||
return new ProfileResponseModel(user, null);
|
return new ProfileResponseModel(user, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,10 +8,6 @@
|
|||||||
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="licensing.cer" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
Binary file not shown.
@ -7,6 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="licensing.cer" />
|
||||||
<EmbeddedResource Include="MailTemplates\VerifyDelete.cshtml" />
|
<EmbeddedResource Include="MailTemplates\VerifyDelete.cshtml" />
|
||||||
<EmbeddedResource Include="MailTemplates\VerifyDelete.text.cshtml" />
|
<EmbeddedResource Include="MailTemplates\VerifyDelete.text.cshtml" />
|
||||||
<EmbeddedResource Include="MailTemplates\VerifyEmail.cshtml" />
|
<EmbeddedResource Include="MailTemplates\VerifyEmail.cshtml" />
|
||||||
|
@ -1,10 +1,28 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class PremiumRequestModel : PaymentRequestModel
|
public class PremiumRequestModel : IValidatableObject
|
||||||
{
|
{
|
||||||
|
public string PaymentToken { get; set; }
|
||||||
[Range(0, 99)]
|
[Range(0, 99)]
|
||||||
public short? AdditionalStorageGb { get; set; }
|
public short? AdditionalStorageGb { get; set; }
|
||||||
|
public IFormFile License { get; set; }
|
||||||
|
|
||||||
|
public bool Validate(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
return (License == null && !globalSettings.SelfHosted) ||
|
||||||
|
(License != null && globalSettings.SelfHosted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(PaymentToken) && License == null)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult("Payment token or license is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -6,5 +7,6 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
bool VerifyOrganizationPlan(Organization organization);
|
bool VerifyOrganizationPlan(Organization organization);
|
||||||
bool VerifyUserPremium(User user);
|
bool VerifyUserPremium(User user);
|
||||||
|
bool VerifyLicense(ILicense license);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ namespace Bit.Core.Services
|
|||||||
Task<IdentityResult> DeleteAsync(User user);
|
Task<IdentityResult> DeleteAsync(User user);
|
||||||
Task<IdentityResult> DeleteAsync(User user, string token);
|
Task<IdentityResult> DeleteAsync(User user, string token);
|
||||||
Task SendDeleteConfirmationAsync(string email);
|
Task SendDeleteConfirmationAsync(string email);
|
||||||
Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb);
|
Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb, UserLicense license);
|
||||||
Task AdjustStorageAsync(User user, short storageAdjustmentGb);
|
Task AdjustStorageAsync(User user, short storageAdjustmentGb);
|
||||||
Task ReplacePaymentMethodAsync(User user, string paymentToken);
|
Task ReplacePaymentMethodAsync(User user, string paymentToken);
|
||||||
Task CancelPremiumAsync(User user, bool endOfPeriod = false);
|
Task CancelPremiumAsync(User user, bool endOfPeriod = false);
|
||||||
|
@ -28,8 +28,9 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_certificate = CoreHelpers.GetCertificate("licensing.crt", null);
|
_certificate = CoreHelpers.GetEmbeddedCertificate("licensing.cer", null);
|
||||||
if(false && !_certificate.Thumbprint.Equals(""))
|
if(!_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(
|
||||||
|
"207e64a231e8aa32aaf68a61037c075ebebd553f"), StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new Exception("Invalid licensing certificate.");
|
throw new Exception("Invalid licensing certificate.");
|
||||||
}
|
}
|
||||||
@ -62,6 +63,11 @@ namespace Bit.Core.Services
|
|||||||
return license != null && license.VerifyData(user) && license.VerifySignature(_certificate);
|
return license != null && license.VerifyData(user) && license.VerifySignature(_certificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool VerifyLicense(ILicense license)
|
||||||
|
{
|
||||||
|
return license.VerifySignature(_certificate);
|
||||||
|
}
|
||||||
|
|
||||||
private UserLicense ReadUserLicense(User user)
|
private UserLicense ReadUserLicense(User user)
|
||||||
{
|
{
|
||||||
if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
|
if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
|
||||||
|
@ -17,6 +17,8 @@ using U2F.Core.Models;
|
|||||||
using U2F.Core.Utils;
|
using U2F.Core.Utils;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -35,6 +37,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly IdentityOptions _identityOptions;
|
private readonly IdentityOptions _identityOptions;
|
||||||
private readonly IPasswordHasher<User> _passwordHasher;
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
|
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
|
||||||
|
private readonly ILicenseVerificationService _licenseVerificationService;
|
||||||
private readonly CurrentContext _currentContext;
|
private readonly CurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
@ -54,6 +57,7 @@ namespace Bit.Core.Services
|
|||||||
IdentityErrorDescriber errors,
|
IdentityErrorDescriber errors,
|
||||||
IServiceProvider services,
|
IServiceProvider services,
|
||||||
ILogger<UserManager<User>> logger,
|
ILogger<UserManager<User>> logger,
|
||||||
|
ILicenseVerificationService licenseVerificationService,
|
||||||
CurrentContext currentContext,
|
CurrentContext currentContext,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
: base(
|
: base(
|
||||||
@ -77,6 +81,7 @@ namespace Bit.Core.Services
|
|||||||
_identityErrorDescriber = errors;
|
_identityErrorDescriber = errors;
|
||||||
_passwordHasher = passwordHasher;
|
_passwordHasher = passwordHasher;
|
||||||
_passwordValidators = passwordValidators;
|
_passwordValidators = passwordValidators;
|
||||||
|
_licenseVerificationService = licenseVerificationService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
@ -481,7 +486,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode))
|
if(string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode))
|
||||||
{
|
{
|
||||||
user.TwoFactorRecoveryCode = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
||||||
}
|
}
|
||||||
await SaveUserAsync(user);
|
await SaveUserAsync(user);
|
||||||
}
|
}
|
||||||
@ -519,13 +524,13 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.TwoFactorProviders = null;
|
user.TwoFactorProviders = null;
|
||||||
user.TwoFactorRecoveryCode = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
||||||
await SaveUserAsync(user);
|
await SaveUserAsync(user);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb)
|
public async Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb, UserLicense license)
|
||||||
{
|
{
|
||||||
if(user.Premium)
|
if(user.Premium)
|
||||||
{
|
{
|
||||||
@ -533,6 +538,18 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
IPaymentService paymentService = null;
|
IPaymentService paymentService = null;
|
||||||
|
if(_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
if(license == null || !_licenseVerificationService.VerifyLicense(license))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid license.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_globalSettings.LicenseDirectory);
|
||||||
|
File.WriteAllText(_globalSettings.LicenseDirectory, JsonConvert.SerializeObject(license, Formatting.Indented));
|
||||||
|
}
|
||||||
|
else if(!string.IsNullOrWhiteSpace(paymentToken))
|
||||||
|
{
|
||||||
if(paymentToken.StartsWith("tok_"))
|
if(paymentToken.StartsWith("tok_"))
|
||||||
{
|
{
|
||||||
paymentService = new StripePaymentService();
|
paymentService = new StripePaymentService();
|
||||||
@ -543,9 +560,14 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
|
await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("License or payment token is required.");
|
||||||
|
}
|
||||||
|
|
||||||
user.Premium = true;
|
user.Premium = true;
|
||||||
user.MaxStorageGb = (short)(1 + additionalStorageGb);
|
user.MaxStorageGb = _globalSettings.SelfHosted ? (short)10240 : (short)(1 + additionalStorageGb);
|
||||||
user.RevisionDate = DateTime.UtcNow;
|
user.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -553,8 +575,12 @@ namespace Bit.Core.Services
|
|||||||
await SaveUserAsync(user);
|
await SaveUserAsync(user);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
{
|
||||||
|
if(!_globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
await paymentService.CancelAndRecoverChargesAsync(user);
|
await paymentService.CancelAndRecoverChargesAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using System;
|
using System;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -16,6 +17,11 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool VerifyLicense(ILicense license)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public bool VerifyOrganizationPlan(Organization organization)
|
public bool VerifyOrganizationPlan(Organization organization)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Table;
|
|
||||||
using Dapper;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -96,11 +96,16 @@ namespace Bit.Core.Utilities
|
|||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static X509Certificate2 GetCertificate(string thumbprint)
|
public static string CleanCertificateThumbprint(string thumbprint)
|
||||||
{
|
{
|
||||||
// Clean possible garbage characters from thumbprint copy/paste
|
// Clean possible garbage characters from thumbprint copy/paste
|
||||||
// ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint
|
// ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint
|
||||||
thumbprint = Regex.Replace(thumbprint, @"[^\da-fA-F]", string.Empty).ToUpper();
|
return Regex.Replace(thumbprint, @"[^\da-fA-F]", string.Empty).ToUpper();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static X509Certificate2 GetCertificate(string thumbprint)
|
||||||
|
{
|
||||||
|
thumbprint = CleanCertificateThumbprint(thumbprint);
|
||||||
|
|
||||||
X509Certificate2 cert = null;
|
X509Certificate2 cert = null;
|
||||||
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
|
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
|
||||||
@ -120,6 +125,17 @@ namespace Bit.Core.Utilities
|
|||||||
return new X509Certificate2(file, password);
|
return new X509Certificate2(file, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static X509Certificate2 GetEmbeddedCertificate(string file, string password)
|
||||||
|
{
|
||||||
|
var assembly = typeof(CoreHelpers).GetTypeInfo().Assembly;
|
||||||
|
using(var s = assembly.GetManifestResourceStream($"Bit.Core.{file}"))
|
||||||
|
using(var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
s.CopyTo(ms);
|
||||||
|
return new X509Certificate2(ms.ToArray(), password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static long ToEpocMilliseconds(DateTime date)
|
public static long ToEpocMilliseconds(DateTime date)
|
||||||
{
|
{
|
||||||
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
|
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
|
||||||
|
@ -9,14 +9,6 @@
|
|||||||
<DockerComposeProjectPath>..\..\docker\Docker.dcproj</DockerComposeProjectPath>
|
<DockerComposeProjectPath>..\..\docker\Docker.dcproj</DockerComposeProjectPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="licensing.cer" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="licensing.cer" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user