-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds LicensingService
#40
base: main
Are you sure you want to change the base?
Changes from 11 commits
14e878b
42d5b7b
5448139
aede3ce
b08a2ab
e49220d
7dd8a28
43eb08c
2cc8f7a
2abf460
bdf7a03
5597d20
8154ce5
04c405f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Security.Claims; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.IdentityModel.Tokens; | ||
|
||
namespace Bitwarden.Extensions.Hosting.Licensing; | ||
|
||
internal sealed class DefaultLicensingService : ILicensingService | ||
{ | ||
private readonly LicensingOptions _licensingOptions; | ||
private readonly TimeProvider _timeProvider; | ||
private readonly ILogger<DefaultLicensingService> _logger; | ||
private readonly InternalLicensingOptions _internalLicensingOptions; | ||
|
||
public DefaultLicensingService( | ||
IOptions<LicensingOptions> licensingOptions, | ||
TimeProvider timeProvider, | ||
ILogger<DefaultLicensingService> logger, | ||
IOptions<InternalLicensingOptions> internalLicensingOptions) | ||
{ | ||
ArgumentNullException.ThrowIfNull(licensingOptions); | ||
ArgumentNullException.ThrowIfNull(timeProvider); | ||
ArgumentNullException.ThrowIfNull(logger); | ||
ArgumentNullException.ThrowIfNull(internalLicensingOptions); | ||
|
||
// TODO: Do we need to support runtime changes to these settings at all, I don't think we do... | ||
_licensingOptions = licensingOptions.Value; | ||
_timeProvider = timeProvider; | ||
_logger = logger; | ||
_internalLicensingOptions = internalLicensingOptions.Value; | ||
|
||
// We are cloud if the signing certificate has a private key that can sign licenses and local development | ||
// hasn't forced self host. | ||
IsCloud = _licensingOptions.SigningCertificate.HasPrivateKey && !_licensingOptions.ForceSelfHost; | ||
} | ||
|
||
public bool IsCloud { get; } | ||
|
||
public string CreateLicense(IEnumerable<Claim> claims, TimeSpan validFor) | ||
{ | ||
ArgumentNullException.ThrowIfNull(claims); | ||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(validFor, TimeSpan.Zero); | ||
|
||
if (!IsCloud) | ||
{ | ||
throw new InvalidOperationException("Self-hosted services can not create a license, please check 'IsCloud' before calling this method."); | ||
} | ||
|
||
var now = _timeProvider.GetUtcNow().UtcDateTime; | ||
|
||
|
||
var tokenDescriptor = new SecurityTokenDescriptor | ||
{ | ||
Subject = new ClaimsIdentity(claims), | ||
Issuer = _licensingOptions.CloudHost, | ||
Audience = _internalLicensingOptions.ProductName, | ||
SigningCredentials = new SigningCredentials( | ||
new X509SecurityKey(_licensingOptions.SigningCertificate), SecurityAlgorithms.RsaSha256), | ||
IssuedAt = now, | ||
NotBefore = now, | ||
Expires = now.Add(validFor), | ||
}; | ||
|
||
var tokenHandler = new JwtSecurityTokenHandler(); | ||
|
||
var token = tokenHandler.CreateToken(tokenDescriptor); | ||
|
||
return tokenHandler.WriteToken(token); | ||
} | ||
|
||
public async Task<IEnumerable<Claim>> VerifyLicenseAsync(string license) | ||
{ | ||
ArgumentNullException.ThrowIfNull(license); | ||
// TODO: Should we validate that this is self host? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โน๏ธ I'd say no. I can make up some ideas around how we'd offer license verification capabilities somewhere new so we don't need to constrain this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sweet, I concur. |
||
// It's not technically wrong to be able to do that but we don't do it currently | ||
// so we could disallow it. | ||
|
||
var tokenHandler = new JwtSecurityTokenHandler(); | ||
|
||
if (!tokenHandler.CanReadToken(license)) | ||
{ | ||
throw new InvalidLicenseException(InvalidLicenseReason.InvalidFormat); | ||
Check warning on line 83 in extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs Codecov / codecov/patchextensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs#L82-L83
|
||
} | ||
|
||
var tokenValidateParameters = new TokenValidationParameters | ||
{ | ||
IssuerSigningKey = new X509SecurityKey(_licensingOptions.SigningCertificate), | ||
ValidateIssuerSigningKey = true, | ||
ValidateLifetime = true, | ||
ValidIssuer = _licensingOptions.CloudHost, | ||
ValidateIssuer = true, | ||
ValidAudience = _internalLicensingOptions.ProductName, | ||
ValidateAudience = true, | ||
#if DEBUG | ||
// It's useful to be stricter in tests so that we don't have to wait 5 minutes | ||
ClockSkew = TimeSpan.Zero, | ||
#endif | ||
}; | ||
|
||
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(license, tokenValidateParameters); | ||
|
||
if (!tokenValidationResult.IsValid) | ||
{ | ||
var exception = tokenValidationResult.Exception; | ||
_logger.LogWarning(exception, "The given license is not valid."); | ||
if (exception is SecurityTokenExpiredException securityTokenExpiredException) | ||
{ | ||
throw new InvalidLicenseException(InvalidLicenseReason.Expired, null, securityTokenExpiredException); | ||
} | ||
else if (exception is SecurityTokenSignatureKeyNotFoundException securityTokenSignatureKeyNotFoundException) | ||
{ | ||
throw new InvalidLicenseException(InvalidLicenseReason.WrongKey, null, securityTokenSignatureKeyNotFoundException); | ||
} | ||
// TODO: Handle other known failures | ||
|
||
throw new InvalidLicenseException(InvalidLicenseReason.Unknown, null, exception); | ||
} | ||
|
||
// Should I even take a ClaimsIdentity and return it here instead of a list of claims? | ||
return tokenValidationResult.ClaimsIdentity.Claims; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โน๏ธ I took a look and don't see anything else we'd want to return really. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using System.Security.Claims; | ||
|
||
namespace Bitwarden.Extensions.Hosting.Licensing; | ||
|
||
/// <summary> | ||
/// A service with the ability to consume and create licenses. | ||
/// </summary> | ||
public interface ILicensingService | ||
{ | ||
/// <summary> | ||
/// Returns whether or not the current service is running as a cloud instance. | ||
/// </summary> | ||
bool IsCloud { get; } | ||
|
||
// TODO: Any other options than valid for? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ญ Limit the product names enabled perhaps? Made up constraint by me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally don't want to limit the product names in anyway, if we did it would require that we update this library whenever we create a new product, which feels like an artificial slowdown. It would also do nothing to stop one product from using another products name. But I do think that maybe taking the product name in this method could be a good idea. Right now, this has a limitation of creating licenses for only a single products licenses per project. Maybe that is a limitation we don't want. Just adding the product name here though would still have all products share the same signing certificate which maybe we'd want that configurable per product too. But forcing the product name to be passed in here does force them to configure it, vs now I allow it to be configured but grab the Also, I think I am going back on accepting a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not opposed to updating this when a product is added -- that's a major event that requires preparation. That said, it has pitfalls and I am just making this up so we don't need to add it; if we want to license differently in the future we can figure it out then. Renaming a I like the idea of allowing a set time to be the input instead. Let the caller figure out when they want it to expire. |
||
/// <summary> | ||
/// Creates a signed license that can be consumed on self-hosted instances. | ||
/// </summary> | ||
/// <remarks> | ||
/// This method can only be called when <see cref="IsCloud"/> returns <see langword="true" />. | ||
/// </remarks> | ||
/// <param name="claims">The claims to include in the license file.</param> | ||
/// <param name="validFor">How long the license should be valid for.</param> | ||
/// <exception cref="InvalidOperationException"> | ||
/// The exception that is thrown if this method is called when the service is not running as a cloud service. | ||
/// </exception> | ||
/// <returns> | ||
/// A string representation of the license that can be given to people to store with their self hosted instance. | ||
/// </returns> | ||
string CreateLicense(IEnumerable<Claim> claims, TimeSpan validFor); | ||
|
||
/// <summary> | ||
/// Verifies that the given license is valid and can have it's contents be trusted. | ||
/// </summary> | ||
/// <param name="license">The license to check.</param> | ||
/// <exception cref="InvalidLicenseException"> | ||
/// The exception that is thrown when the given license is invalid and data stored in it can not be trusted. | ||
/// </exception> | ||
/// <returns>An enumerable of claims included in the license.</returns> | ||
Task<IEnumerable<Claim>> VerifyLicenseAsync(string license); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
โน๏ธ No.