-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #40 from EventStore/timothycoleman/validate-license
Add validation to License
- Loading branch information
Showing
2 changed files
with
118 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Security.Cryptography; | ||
using System.Threading.Tasks; | ||
using EventStore.Plugins.Licensing; | ||
using Xunit; | ||
|
||
namespace EventStore.Plugins.Tests.Licensing; | ||
|
||
public class LicenseTests { | ||
public static (string PublicKey, string PrivateKey) CreateKeyPair() { | ||
using var rsa = RSA.Create(512); | ||
var publicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); | ||
var privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey()); | ||
return (publicKey, privateKey); | ||
} | ||
|
||
[Fact] | ||
public async Task can_create_and_validate_license() { | ||
var (publicKey, privateKey) = CreateKeyPair(); | ||
|
||
var license = await License.CreateAsync(publicKey, privateKey, new Dictionary<string, object>() { | ||
{ "foo", "bar"}, | ||
}); | ||
|
||
// check repeatedly because of https://github.com/dotnet/runtime/issues/43087 | ||
Assert.True(await license.IsValidAsync(publicKey)); | ||
Assert.True(await license.IsValidAsync(publicKey)); | ||
Assert.True(await license.IsValidAsync(publicKey)); | ||
|
||
Assert.Equal("bar", license.Token.Claims.First(c => c.Type == "foo").Value); | ||
} | ||
|
||
[Fact] | ||
public async Task detects_incorrect_public_key() { | ||
var (publicKey, privateKey) = CreateKeyPair(); | ||
var (publicKey2, _) = CreateKeyPair(); | ||
|
||
var license = await License.CreateAsync(publicKey, privateKey, new Dictionary<string, object>() { | ||
{ "foo", "bar"}, | ||
}); | ||
|
||
Assert.False(await license.IsValidAsync(publicKey2)); | ||
} | ||
|
||
[Fact] | ||
public async Task cannot_create_with_inconsistent_keys() { | ||
var (publicKey, _) = CreateKeyPair(); | ||
var (_, privateKey) = CreateKeyPair(); | ||
|
||
var ex = await Assert.ThrowsAsync<Exception>(() => | ||
License.CreateAsync(publicKey, privateKey, new Dictionary<string, object>() { | ||
{ "foo", "bar"}, | ||
})); | ||
|
||
Assert.Equal("Token could not be validated", ex.Message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,62 @@ | ||
using Microsoft.IdentityModel.JsonWebTokens; | ||
using System.Security.Cryptography; | ||
using System.Threading.Tasks; | ||
using System; | ||
using Microsoft.IdentityModel.JsonWebTokens; | ||
using Microsoft.IdentityModel.Tokens; | ||
using System.Collections.Generic; | ||
|
||
namespace EventStore.Plugins.Licensing; | ||
|
||
public record License(JsonWebToken Token); | ||
public record License(JsonWebToken Token) { | ||
public async Task<bool> IsValidAsync(string publicKey) { | ||
var result = await ValidateTokenAsync(publicKey, Token.EncodedToken); | ||
return result.IsValid; | ||
} | ||
|
||
public static async Task<License> CreateAsync( | ||
string publicKey, | ||
string privateKey, | ||
IDictionary<string, object> claims) { | ||
|
||
using var rsa = RSA.Create(); | ||
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out _); | ||
var tokenHandler = new JsonWebTokenHandler(); | ||
var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { | ||
Audience = "esdb", | ||
Issuer = "esdb", | ||
Expires = DateTime.UtcNow + TimeSpan.FromHours(1), | ||
Claims = claims, | ||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256), | ||
}); | ||
|
||
var result = await ValidateTokenAsync(publicKey, token); | ||
|
||
if (!result.IsValid) | ||
throw new Exception("Token could not be validated"); | ||
|
||
if (result.SecurityToken is not JsonWebToken jwt) | ||
throw new Exception("Token is not a JWT"); | ||
|
||
return new License(jwt); | ||
} | ||
|
||
private static async Task<TokenValidationResult> ValidateTokenAsync(string publicKey, string token) { | ||
// not very satisfactory https://github.com/dotnet/runtime/issues/43087 | ||
CryptoProviderFactory.Default.CacheSignatureProviders = false; | ||
|
||
using var rsa = RSA.Create(); | ||
rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _); | ||
var result = await new JsonWebTokenHandler().ValidateTokenAsync( | ||
token, | ||
new TokenValidationParameters { | ||
ValidIssuer = "esdb", | ||
ValidAudience = "esdb", | ||
IssuerSigningKey = new RsaSecurityKey(rsa), | ||
ValidateAudience = true, | ||
ValidateIssuerSigningKey = true, | ||
ValidateIssuer = true, | ||
ValidateLifetime = true, | ||
}); | ||
return result; | ||
} | ||
} |