Skip to content

Commit abeda24

Browse files
jbogardclaude
andauthored
License validation: clear messages for malformed keys, client platforms, and IdentityModel version mismatches (#4670)
* License validation: clear messages for malformed keys and client platforms - Check the key's structure (three base64url segments, 342-character RS256 signature) before validating, and log an error-level "incomplete or corrupted" message instead of an IDX signature error (#4661). The key is never logged. - Add a copy/paste hint when validation fails with an invalid signature. - Detect client platforms without RSA support, such as Blazor WASM (#4662). JsonWebTokenHandler swallows the PlatformNotSupportedException and reports IDX10511, with the original exception only as PII-hidden message text, so probe RSA directly before validating. Also walk the result's exception chain for a PlatformNotSupportedException. Fixes #4661 Fixes #4662 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Log plain license validation messages, keep IdentityModel details at debug IDX exceptions aren't actionable for customers, and with PII logging off they hide most details anyway. Log one plain critical message per failure kind (invalid signature, undecodable token, wrong issuer/audience, other) without the exception, and log the exception at debug level for support. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * License validation: name mismatched Microsoft.IdentityModel versions Mixed Microsoft.IdentityModel.* package versions surface as IDX14102 wrapping a MissingMethodException (seen in support on MAUI). Report that as a package version problem rather than a corrupted key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * License messages: link through /go redirects on luckypennysoftware.com Shipped binaries live for years, so link to redirects we control (license key, client platforms, IdentityModel versions) instead of docs URLs that have already moved once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: troubleshooting license key validation Document the new validation messages (incomplete/corrupted key, signature mismatch, not a Lucky Penny key, mismatched Microsoft.IdentityModel versions), the client-platform INFO message, and how to see the underlying exception at Debug level. These sections are the targets of the /go redirects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: no missing-license warning on client platforms; CodeQL - When license keys can't be validated on this platform (no RSA, e.g. Blazor WASM), mark the license as validation-unsupported so the validator skips the "no valid license key" warning after the INFO client-platform message. - Rename LicenseKeyLink to AccountPageLink: CodeQL's name heuristic flagged the URL constant as sensitive data. - Include the debug-level exception in the test failure message for the plain-critical assertions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Skip the end-to-end invalid signature test on .NET Framework test host Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run net471 tests in an AppDomain so the generated binding redirects apply The test host's AppDomain doesn't use AutoMapper.UnitTests.dll.config, so redirects such as System.Threading.Tasks.Extensions were ignored and license validation failed to load them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Revert enabling xunit AppDomains on net471: tests are no longer discovered With xunit.appDomain=ifAvailable the net471 run reports "No test is available" and passes with zero tests. Back to skipping the end-to-end invalid signature test on .NET Framework. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 08e5577 commit abeda24

6 files changed

Lines changed: 396 additions & 18 deletions

File tree

‎docs/source/License-configuration.md‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ No code change is required when using an environment variable—just register Au
3939

4040
Licensing is enforced via log messages at various levels:
4141

42-
- INFO: Valid license message
42+
- INFO: Valid license message, or license validation is not supported on this platform (see Client Redistribution Scenarios below)
4343
- WARNING: Missing license message, or a license term that has ended but is still within its grace period
44-
- ERROR: Invalid/expired license message
44+
- ERROR: Invalid/expired license message, or a license key that is incomplete or corrupted
45+
- CRITICAL: The license key could not be validated (see Troubleshooting License Key Validation below)
4546

4647
### Renewal grace period
4748

@@ -106,4 +107,53 @@ The license key should NOT be set as this would result in secrets transmitted to
106107
builder.Logging.AddFilter("LuckyPennySoftware.AutoMapper.License", LogLevel.None);
107108
```
108109

109-
This will depend on your logging setup. A missing/invalid license key does not affect runtime behavior in any way.
110+
This will depend on your logging setup. A missing/invalid license key does not affect runtime behavior in any way.
111+
112+
If a license key is set on a platform that does not support RSA cryptography, such as Blazor WASM, AutoMapper skips validation and logs an INFO message linking to this section instead of a validation error.
113+
114+
## Troubleshooting License Key Validation
115+
116+
When a license key can't be validated, AutoMapper logs a message describing the problem. The underlying exception is logged separately at `Debug` level; to see it, enable debug logging for the `LuckyPennySoftware.AutoMapper.License` category:
117+
118+
```csharp
119+
builder.Logging.AddFilter("LuckyPennySoftware.AutoMapper.License", LogLevel.Debug);
120+
```
121+
122+
### The license key is incomplete or corrupted
123+
124+
The key is missing characters or contains characters that aren't part of a license key. This usually happens when the key is copied from a PDF or an email, where it can be truncated or wrapped across lines.
125+
126+
AutoMapper removes whitespace (spaces, tabs, and line breaks) from the key before validating it, so line breaks and trailing newlines, such as those in Kubernetes secrets created from files, are not a problem. Missing characters can't be recovered: copy the full key again from your [account page](https://luckypennysoftware.com/account).
127+
128+
### The license key signature does not match
129+
130+
The key is well formed but its contents have been changed, usually by a copy/paste or formatting problem. Copy the full key again from your [account page](https://luckypennysoftware.com/account).
131+
132+
Online JWT decoders such as jwt.io can show a damaged key as valid, because they ignore whitespace and don't check the signature against the Lucky Penny Software key. Use your account page as the source of truth.
133+
134+
### The license key is not a Lucky Penny Software license key
135+
136+
The value is a token, but not one issued by Lucky Penny Software. Check that the value you configured (in code or in the `AUTOMAPPER_LICENSE_KEY` or `LUCKYPENNY_LICENSE_KEY` environment variables) is the license key from your [account page](https://luckypennysoftware.com/account).
137+
138+
### Mismatched Microsoft.IdentityModel package versions
139+
140+
AutoMapper validates license keys with the `Microsoft.IdentityModel.JsonWebTokens` package. If your application references different versions of the `Microsoft.IdentityModel.*` packages (for example, through an authentication library that pins an older `Microsoft.IdentityModel.Tokens`), validation fails with an error such as:
141+
142+
```
143+
System.MissingMethodException: Method not found: 'Void Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Decode(...)'
144+
```
145+
146+
To find the versions in use, run:
147+
148+
```
149+
dotnet list package --include-transitive
150+
```
151+
152+
Then reference the same version of every `Microsoft.IdentityModel.*` package, for example by adding direct references at the highest version in use:
153+
154+
```xml
155+
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
156+
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
157+
```
158+
159+
If you use [Central Package Management](https://learn.microsoft.com/nuget/consume-packages/central-package-management), you can enable transitive pinning (`CentralPackageTransitivePinningEnabled`) and pin the versions in `Directory.Packages.props`.

‎src/AutoMapper/Licensing/License.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,8 @@ public License(ClaimsPrincipal claims)
7171
public bool IsPerpetual { get; }
7272

7373
public bool IsConfigured { get; }
74+
75+
// Set when this platform can't validate license keys (no RSA, e.g. Blazor WASM). The accessor has
76+
// already logged client-platform guidance, so validation logs nothing further.
77+
public bool IsValidationUnsupported { get; internal set; }
7478
}

‎src/AutoMapper/Licensing/LicenseAccessor.cs‎

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ internal class LicenseAccessor
1212
internal const string AutoMapperLicenseKeyEnvVariable = "AUTOMAPPER_LICENSE_KEY";
1313
internal const string SharedLicenseKeyEnvVariable = "LUCKYPENNY_LICENSE_KEY";
1414

15+
// Shipped binaries live for years, so messages link through redirects on our own domain that can be
16+
// repointed when the docs move. Configure these under https://luckypennysoftware.com/go/.
17+
internal const string AccountPageLink = "https://luckypennysoftware.com/go/automapper-license-key";
18+
internal const string ClientPlatformsLink = "https://luckypennysoftware.com/go/automapper-client-platforms";
19+
internal const string IdentityModelVersionsLink = "https://luckypennysoftware.com/go/automapper-identitymodel-versions";
20+
1521
private readonly IGlobalConfiguration _configuration;
1622
private readonly ILogger _logger;
1723

@@ -23,6 +29,9 @@ public LicenseAccessor(IGlobalConfiguration configuration, ILoggerFactory logger
2329

2430
private License _license;
2531
private readonly object _lock = new();
32+
private bool _validationUnsupported;
33+
34+
internal bool IsValidationUnsupported => _validationUnsupported;
2635

2736
public License Current => _license ??= Initialize();
2837

@@ -42,6 +51,11 @@ private License Initialize()
4251
}
4352

4453
var licenseClaims = ValidateKey(key);
54+
if (_validationUnsupported)
55+
{
56+
return new License { IsValidationUnsupported = true };
57+
}
58+
4559
return licenseClaims.Any()
4660
? new License(new ClaimsPrincipal(new ClaimsIdentity(licenseClaims)))
4761
: new License();
@@ -69,20 +83,41 @@ private static string NormalizeLicenseKey(string key)
6983
return new string(key.Where(c => !char.IsWhiteSpace(c)).ToArray());
7084
}
7185

72-
private Claim[] ValidateKey(string licenseKey)
86+
// RS256 signature over the 2048-bit Lucky Penny key: 256 bytes, base64url-encoded without padding.
87+
private const int SignatureLength = 342;
88+
89+
private static readonly RSAParameters LicenseKeyParameters = new()
7390
{
74-
try
91+
Exponent = Convert.FromBase64String("AQAB"),
92+
Modulus = Convert.FromBase64String(
93+
"2LTtdJV2b0mYoRqChRCfcqnbpKvsiCcDYwJ+qPtvQXWXozOhGo02/V0SWMFBdbZHUzpEytIiEcojo7Vbq5mQmt4lg92auyPKsWq6qSmCVZCUuL/kpYqLCit4yUC0YqZfw4H9zLf1yAIOgyXQf1x6g+kscDo1pWAniSl9a9l/LXRVEnGz+OfeUrN/5gzpracGUY6phx6T09UCRuzi4YqqO4VJzL877W0jCW2Q7jMzHxOK04VSjNc22CADuCd34mrFs23R0vVm1DVLYtPGD76/rGOcxO6vmRc7ydBAvt1IoUsrY0vQ2rahp51YPxqqhKPd8nNOomHWblCCA7YUeV3C1Q==")
94+
};
95+
96+
internal Claim[] ValidateKey(string licenseKey)
97+
{
98+
if (!IsWellFormed(licenseKey))
7599
{
76-
var handler = new JsonWebTokenHandler();
100+
// Never log the key itself.
101+
_logger.LogError(
102+
"The Lucky Penny software license key appears to be incomplete or corrupted " +
103+
"(it may have been truncated when copied). " +
104+
"Copy the full key again from " + AccountPageLink + ".");
105+
return [];
106+
}
77107

78-
var rsa = new RSAParameters
108+
try
109+
{
110+
// JsonWebTokenHandler swallows a PlatformNotSupportedException from the RSA provider and
111+
// reports it as an IDX10511 signature failure, with the original exception only as (PII-hidden)
112+
// message text. Probe RSA directly so client platforms such as Blazor WASM are detected (#4662).
113+
using (var rsa = RSA.Create())
79114
{
80-
Exponent = Convert.FromBase64String("AQAB"),
81-
Modulus = Convert.FromBase64String(
82-
"2LTtdJV2b0mYoRqChRCfcqnbpKvsiCcDYwJ+qPtvQXWXozOhGo02/V0SWMFBdbZHUzpEytIiEcojo7Vbq5mQmt4lg92auyPKsWq6qSmCVZCUuL/kpYqLCit4yUC0YqZfw4H9zLf1yAIOgyXQf1x6g+kscDo1pWAniSl9a9l/LXRVEnGz+OfeUrN/5gzpracGUY6phx6T09UCRuzi4YqqO4VJzL877W0jCW2Q7jMzHxOK04VSjNc22CADuCd34mrFs23R0vVm1DVLYtPGD76/rGOcxO6vmRc7ydBAvt1IoUsrY0vQ2rahp51YPxqqhKPd8nNOomHWblCCA7YUeV3C1Q==")
83-
};
115+
rsa.ImportParameters(LicenseKeyParameters);
116+
}
117+
118+
var handler = new JsonWebTokenHandler();
84119

85-
var key = new RsaSecurityKey(rsa)
120+
var key = new RsaSecurityKey(LicenseKeyParameters)
86121
{
87122
KeyId = "LuckyPennySoftwareLicenseKey/bbb13acb59904d89b4cb1c85f088ccf9"
88123
};
@@ -101,19 +136,93 @@ private Claim[] ValidateKey(string licenseKey)
101136
var validateResult = handler.ValidateTokenAsync(licenseKey, parms).GetAwaiter().GetResult();
102137
if (!validateResult.IsValid)
103138
{
104-
_logger.LogCritical(validateResult.Exception, "Error validating the Lucky Penny software license key");
139+
LogValidationFailure(validateResult.Exception);
105140
}
106141

107142
return validateResult.ClaimsIdentity?.Claims.ToArray() ?? [];
108143
}
109144
catch (PlatformNotSupportedException)
110145
{
111-
_logger.LogInformation(
112-
"RSA cryptography is not supported on this platform. " +
113-
"For client redistribution scenarios such as Blazor WASM, see: " +
114-
"https://docs.automapper.io/en/latest/License-configuration.html#client-redistribution-scenarios");
146+
LogPlatformNotSupported();
115147
return [];
116148
}
117149
}
118150

151+
/// <summary>
152+
/// Cheap structural check of a compact JWS: three dot-separated base64url segments, with a
153+
/// signature segment of the expected length. Catches keys truncated or mangled by copy/paste.
154+
/// </summary>
155+
internal static bool IsWellFormed(string licenseKey)
156+
{
157+
var segments = licenseKey.Split('.');
158+
return segments.Length == 3
159+
&& segments.All(segment => segment.Length > 0 && segment.All(IsBase64UrlChar))
160+
&& segments[2].Length == SignatureLength;
161+
162+
static bool IsBase64UrlChar(char c) =>
163+
c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '-' or '_';
164+
}
165+
166+
internal void LogValidationFailure(Exception exception)
167+
{
168+
if (ContainsPlatformNotSupported(exception))
169+
{
170+
LogPlatformNotSupported();
171+
return;
172+
}
173+
174+
// IdentityModel's IDX messages aren't actionable for customers (and hide most details unless PII
175+
// logging is on), so log a plain message and keep the exception at debug level for support.
176+
_logger.LogCritical(exception switch
177+
{
178+
SecurityTokenInvalidSignatureException =>
179+
"The Lucky Penny software license key is not valid: its signature does not match. " +
180+
"If the key was copied from a PDF, email, or file, it may have been corrupted; " +
181+
"copy the full key again from " + AccountPageLink + ".",
182+
_ when ContainsAssemblyMismatch(exception) =>
183+
"The Lucky Penny software license key could not be validated because the Microsoft.IdentityModel " +
184+
"packages in this application are different versions. Reference the same version of all " +
185+
"Microsoft.IdentityModel.* packages (for example, by referencing Microsoft.IdentityModel.JsonWebTokens directly). " +
186+
"See " + IdentityModelVersionsLink + ".",
187+
SecurityTokenMalformedException or ArgumentException =>
188+
"The Lucky Penny software license key appears to be incomplete or corrupted. " +
189+
"Copy the full key again from " + AccountPageLink + ".",
190+
SecurityTokenInvalidIssuerException or SecurityTokenInvalidAudienceException =>
191+
"The Lucky Penny software license key is not valid: it is not a Lucky Penny Software license key. " +
192+
"Copy your license key from " + AccountPageLink + ".",
193+
_ =>
194+
"Error validating the Lucky Penny software license key. " +
195+
"Copy the full key again from " + AccountPageLink + ". " +
196+
"If the problem persists, enable debug logging for this category and contact support."
197+
});
198+
_logger.LogDebug(exception, "License key validation failure details");
199+
}
200+
201+
internal static bool ContainsPlatformNotSupported(Exception exception) =>
202+
exception switch
203+
{
204+
null => false,
205+
PlatformNotSupportedException => true,
206+
AggregateException aggregate => aggregate.InnerExceptions.Any(ContainsPlatformNotSupported),
207+
_ => ContainsPlatformNotSupported(exception.InnerException)
208+
};
209+
210+
// Mixed Microsoft.IdentityModel.* package versions surface as IDX14102 wrapping a MissingMethodException.
211+
internal static bool ContainsAssemblyMismatch(Exception exception) =>
212+
exception switch
213+
{
214+
null => false,
215+
MissingMemberException or TypeLoadException or FileLoadException => true,
216+
AggregateException aggregate => aggregate.InnerExceptions.Any(ContainsAssemblyMismatch),
217+
_ => ContainsAssemblyMismatch(exception.InnerException)
218+
};
219+
220+
private void LogPlatformNotSupported()
221+
{
222+
_validationUnsupported = true;
223+
_logger.LogInformation(
224+
"RSA cryptography is not supported on this platform. " +
225+
"For client redistribution scenarios such as Blazor WASM, see: " +
226+
ClientPlatformsLink);
227+
}
119228
}

‎src/AutoMapper/Licensing/LicenseValidator.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public void Validate(License license)
2525
{
2626
var errors = new List<string>();
2727

28+
if (license is { IsValidationUnsupported: true })
29+
{
30+
return;
31+
}
32+
2833
if (license is not { IsConfigured: true })
2934
{
3035
var message = "You do not have a valid license key for the Lucky Penny software AutoMapper. " +

0 commit comments

Comments
 (0)