Skip to content
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

License validation #28

Merged
merged 10 commits into from
Dec 17, 2020
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,4 @@ workspace.xml
src/IdentityServer4/host/identityserver.db
tempkey.jwk
keys
*.key
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,10 @@ public class IdentityServerOptions
/// Gets or sets the signing key management options.
/// </summary>
public KeyManagementOptions KeyManagement { get; set; } = new KeyManagementOptions();

/// <summary>
/// Gets or sets the license key.
/// </summary>
public string LicenseKey { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Hosting;
using Duende.IdentityServer.Stores;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -63,6 +64,10 @@ internal static void Validate(this IApplicationBuilder app)
{
var serviceProvider = scope.ServiceProvider;

var options = serviceProvider.GetRequiredService<IdentityServerOptions>();
LicenseValidator.Initalize(loggerFactory, options);
LicenseValidator.ValidateLicense();

TestService(serviceProvider, typeof(IPersistedGrantStore), logger, "No storage mechanism for grants specified. Use the 'AddInMemoryPersistedGrants' extension method to register a development version.");
TestService(serviceProvider, typeof(IClientStore), logger, "No storage mechanism for clients specified. Use the 'AddInMemoryClients' extension method to register a development version.");
TestService(serviceProvider, typeof(IResourceStore), logger, "No storage mechanism for resources specified. Use the 'AddInMemoryIdentityResources' or 'AddInMemoryApiResources' extension method to register a development version.");
Expand All @@ -73,7 +78,6 @@ internal static void Validate(this IApplicationBuilder app)
logger.LogInformation("You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation.");
}

var options = serviceProvider.GetRequiredService<IdentityServerOptions>();
ValidateOptions(options, logger);

ValidateAsync(serviceProvider, logger).GetAwaiter().GetResult();
Expand Down
3 changes: 3 additions & 0 deletions src/IdentityServer/Hosting/IdentityServerMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Validation;

namespace Duende.IdentityServer.Hosting
{
Expand Down Expand Up @@ -69,6 +70,8 @@ public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSessi
var endpoint = router.Find(context);
if (endpoint != null)
{
LicenseValidator.ValidateIssuer(context.GetIdentityServerIssuerUri());

_logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", endpoint.GetType().FullName, context.Request.Path.ToString());

var result = await endpoint.ProcessAsync(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<DeviceFlowAuthorizationRequest> GetAuthorizationContextAsync(s
var deviceAuth = await _devices.FindByUserCodeAsync(userCode);
if (deviceAuth == null) return null;

var client = await _clients.FindClientByIdAsync(deviceAuth.ClientId);
var client = await _clients.FindEnabledClientByIdAsync(deviceAuth.ClientId);
if (client == null) return null;

var parsedScopesResult = _scopeParser.ParseScopeValues(deviceAuth.RequestedScopes);
Expand All @@ -62,7 +62,7 @@ public async Task<DeviceFlowInteractionResult> HandleRequestAsync(string userCod
var deviceAuth = await _devices.FindByUserCodeAsync(userCode);
if (deviceAuth == null) return LogAndReturnError("Invalid user code", "Device authorization failure - user code is invalid");

var client = await _clients.FindClientByIdAsync(deviceAuth.ClientId);
var client = await _clients.FindEnabledClientByIdAsync(deviceAuth.ClientId);
if (client == null) return LogAndReturnError("Invalid client", "Device authorization failure - requesting client is invalid");

var subject = await _session.GetUserAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ public async Task<AuthorizeRequestValidationResult> ValidateAsync(NameValueColle

_logger.LogTrace("Authorize request protocol validation successful");

LicenseValidator.ValidateClient(request.ClientId);

return Valid(request);
}

Expand Down
224 changes: 224 additions & 0 deletions src/IdentityServer/Validation/Default/LicenseValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.


using Microsoft.Extensions.Logging;
using Duende.IdentityServer.Configuration;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Security.Claims;

namespace Duende.IdentityServer.Validation
{
internal class LicenseValidator
{
const string LicenseFileName = "Duende_IdentityServer_License.key";

static ILogger _logger;
static IdentityServerOptions _options;
static License _license;

static ConcurrentDictionary<string, byte> _clientIds = new ConcurrentDictionary<string, byte>();
static ConcurrentDictionary<string, byte> _issuers = new ConcurrentDictionary<string, byte>();

public static void Initalize(ILoggerFactory loggerFactory, IdentityServerOptions options)
{
_logger = loggerFactory.CreateLogger("Duende.IdentityServer");
_options = options;

var key = options.LicenseKey ?? LoadFromFile();
_license = ValidateKey(key);
}

private static string LoadFromFile()
{
var path = Path.Combine(Directory.GetCurrentDirectory(), LicenseFileName);
if (File.Exists(path))
{
return File.ReadAllText(path).Trim();
}

return null;
}

// todo: check this periodcally?
public static void ValidateLicense()
{
var errors = new List<string>();

if (_license == null)
{
// todo: more wording on license options? URL to license details?
// error? or info?
_logger.LogWarning("You do not have a valid license key for Duende IdentityServer.");
return;
}
else
{
if (_license.Expiration.HasValue)
{
var diff = DateTime.UtcNow.Date.Subtract(_license.Expiration.Value.Date).TotalDays;
if (diff > 0)
{
errors.Add($"Your license for Duende IdentityServer expired {diff} days ago.");
}
}

if (_options.KeyManagement.Enabled && !_license.KeyManagement)
{
errors.Add("You have automatic key management enabled, but you do not have a valid license for that feature of Duende IdentityServer.");
}

// todo: add resource isolation check here
}

if (errors.Count > 0)
{
foreach (var err in errors)
{
_logger.LogError(err);
}

if (_license != null)
{
_logger.LogError("Please contact {licenceContact} from {licenseCompany} to obtain a valid license for Duende IdentityServer.", _license.ContactInfo, _license.CompanyName);
}
}
else
{
if (_license.Expiration.HasValue)
{
_logger.LogInformation("You have a valid license key for Duende IdentityServer for use at {licenseCompany}. The license expires on {licenseExpiration}.", _license.CompanyName, _license.Expiration.Value.ToLongDateString());
}
else
{
_logger.LogInformation("You have a valid license key for Duende IdentityServer for use at {licenseCompany}.", _license.CompanyName);
}
}
}

public static void ValidateClient(string clientId)
{
if (_license != null)
{
if (_license.ClientLimit.HasValue)
{
_clientIds.TryAdd(clientId, 1);
if (_clientIds.Count > _license.ClientLimit)
{
_logger.LogError("Your license for Duende IdentityServer only permits {clientLimit} number of clients. You have processed requests for {clientCount}.", _license.ClientLimit, _clientIds.Count);
}
}
}
}

public static void ValidateIssuer(string iss)
{
if (_license != null)
{
if (_license.IssuerLimit.HasValue)
{
_issuers.TryAdd(iss, 1);
if (_issuers.Count > _license.IssuerLimit)
{
_logger.LogError("Your license for Duende IdentityServer only permits {issuerLimit} number of issuers. You have processed requests for {issuerCount}.", _license.IssuerLimit, _issuers.Count);
}
}
}
}

internal static License ValidateKey(string licenseKey)
{
if (!String.IsNullOrWhiteSpace(licenseKey))
{
var handler = new JwtSecurityTokenHandler();

try
{
var rsa = new RSAParameters
{
Exponent = Convert.FromBase64String("AQAB"),
Modulus = Convert.FromBase64String("tAHAfvtmGBng322TqUXF/Aar7726jFELj73lywuCvpGsh3JTpImuoSYsJxy5GZCRF7ppIIbsJBmWwSiesYfxWxBsfnpOmAHU3OTMDt383mf0USdqq/F0yFxBL9IQuDdvhlPfFcTrWEL0U2JsAzUjt9AfsPHNQbiEkOXlIwtNkqMP2naynW8y4WbaGG1n2NohyN6nfNb42KoNSR83nlbBJSwcc3heE3muTt3ZvbpguanyfFXeoP6yyqatnymWp/C0aQBEI5kDahOU641aDiSagG7zX1WaF9+hwfWCbkMDKYxeSWUkQOUOdfUQ89CQS5wrLpcU0D0xf7/SrRdY2TRHvQ=="),
};

var key = new RsaSecurityKey(rsa)
{
KeyId = "IdentityServerLicensekey/7ceadbb78130469e8806891025414f16"
};

var parms = new TokenValidationParameters
{
ValidIssuer = "https://duendesoftware.com",
ValidAudience = "IdentityServer",
IssuerSigningKey = key,
ValidateLifetime = false
};

var validateResult = handler.ValidateToken(licenseKey, parms, out _);
return new License(validateResult);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error validating Duende IdentityServer license key");
}
}

return null;
}
}

internal class License
{
public License(ClaimsPrincipal claims)
{
CompanyName = claims.FindFirst("company_name")?.Value;
ContactInfo = claims.FindFirst("contact_info")?.Value;

if (Int64.TryParse(claims.FindFirst("exp")?.Value, out var exp))
{
var expDate = DateTimeOffset.FromUnixTimeSeconds(exp);
Expiration = expDate.UtcDateTime;
}

Edition = claims.FindFirst("edition")?.Value;
IsEnterprise = "enterprise".Equals(Edition, StringComparison.OrdinalIgnoreCase);
IsBusiness = IsEnterprise || "business".Equals(Edition, StringComparison.OrdinalIgnoreCase);

KeyManagement = IsBusiness || claims.HasClaim("feature", "key_management");
ResourceIsolation = IsEnterprise || claims.HasClaim("feature", "resource_isolation");

if (!IsEnterprise)
{
if (Int32.TryParse(claims.FindFirst("client_limit")?.Value, out var clientLimit))
{
ClientLimit = clientLimit;
}

if (Int32.TryParse(claims.FindFirst("issuer_limit")?.Value, out var issuerLimit))
{
IssuerLimit = issuerLimit;
}
}
}

public string CompanyName { get; set; }
public string ContactInfo { get; set; }

public DateTime? Expiration { get; set; }

public string Edition { get; set; }
public bool IsEnterprise { get; set; }
public bool IsBusiness { get; set; }

public int? ClientLimit { get; set; }
public int? IssuerLimit { get; set; }

public bool KeyManagement { get; set; }
public bool ResourceIsolation { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ private async Task<TokenRequestValidationResult> RunValidationAsync(Func<NameVal
}

LogSuccess();

LicenseValidator.ValidateClient(customValidationContext.Result.ValidatedRequest.ClientId);

return customValidationContext.Result;
}

Expand Down