Skip to content

Upgrade to .NET 10, fix validation bugs, add diagnostics, tests and CI - #1

Open
hendem wants to merge 6 commits into
mainfrom
chore/net10-upgrade-and-fixes
Open

hendem wants to merge 6 commits into
mainfrom
chore/net10-upgrade-and-fixes

Conversation

@hendem

@hendem hendem commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Upgrades JwtValidator to .NET 10 and fixes several validation bugs, including one where JWKS validation used the wrong key. It also adds better diagnostics, opt-in claim checks, exit codes, unit tests and a CI build. The repo moves to a src/ + tests/ layout with a .slnx solution.

Bug fixes

  • Only the last RSA key in a JWKS was ever used. Every key was imported into one shared RSA instance, so each key overwrote the previous one and kid was ignored. Tokens signed by any other key in the set failed. Keys are now loaded with JsonWebKeySet and matched by kid, with a fallback that tries every key. There is a regression test.
  • --jwt-file didn't work with a trailing newline, which most files have; the token was reported as malformed. Input is now trimmed.
  • EC keys in a JWKS were skipped and only RS256 was accepted. RS/PS/ES 256/384/512 are now supported.
  • Sync-over-async HttpClient...Result with an undisposed client; legacy RSACryptoServiceProvider; manual validation returned an undisposed SHA256.
  • The process always exited with 0, even when validation failed.
  • All nullable warnings fixed; warnings are now errors.

Changes

  • .NET 10: net10.0, with JwtSecurityTokenHandler replaced by JsonWebTokenHandler (Microsoft.IdentityModel.JsonWebTokens 8.23.0) and Newtonsoft.Json dropped.
  • Layout: src/JwtValidator, tests/JwtValidator.Tests, JwtValidator.slnx (migrated from .sln), a shared Directory.Build.props, .editorconfig and global.json.
  • Code structure: Program.cs is split into Options, KeyLoader, TokenValidator, TokenDiagnostics and ValidateCommand.
  • Diagnostics: the output shows the decoded header and claims, plus iat/nbf/exp as UTC with expired/not-yet-valid status. It lists the key set, warns about unusable JWKS entries and a kid that isn't in the set, and gives a one-line failure reason (--verbose adds the full payload and error detail).
  • New options: --issuer, --audience, --validate-lifetime, --clock-skew. All are off by default, so existing usage behaves the same.
  • Exit codes: 0 valid, 1 invalid token, 2 usage or input error.
  • Stricter input: passing both --jwt and --jwt-file, or more than one key source, is now a usage error. Previously one of them was silently ignored.
  • CI: GitHub Actions builds and tests on push/PR to main.
  • JwtValidator.csproj.user is no longer tracked.
  • Docs: every method and type has XML doc comments, and GenerateDocumentationFile makes the compiler check them.
  • All NuGet packages are on their latest versions (xunit.v3 4.0.2).

Testing

  • dotnet test --solution JwtValidator.slnx: 56/56 pass. Tests sign tokens with RSA/EC keys generated in memory, so no key material is committed.
  • Manual CLI checks:
    • --help/--version exit 0; missing or conflicting options exit 2; a missing file exits 2.
    • A malformed token exits 1.
    • The jwks file launch-profile token validates against the live --jwks-url and reports the matching kid.
  • The good key / bad key / key2 launch-profile tokens don't validate against their inline JWKS. The original code on main rejects them too, so that's stale sample data, not a regression.

🤖 Generated with Claude Code

Mark Henderson and others added 5 commits October 12, 2026 16:55
- Retarget JwtValidator to net10.0
- Bump System.IdentityModel.Tokens.Jwt 7.6.0 -> 8.23.0
- Fix typo in project description
- Stop tracking JwtValidator.csproj.user and broaden .gitignore

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odes

Bug fixes:
- JWKS with several RSA keys: every key was imported into one shared RSA
  instance, so only the last RSA key was ever used. Keys are now loaded
  with JsonWebKeySet and passed as IssuerSigningKeys, so kid is honoured.
- EC (ES256/384/512) and PS* tokens are supported instead of RS256 only.
- JWT read from --jwt-file is trimmed; a trailing newline made it unreadable.
- Replace sync-over-async HttpClient .Result with async Main and dispose it.
- Replace legacy RSACryptoServiceProvider with RSA.Create().
- Manual validation no longer returns an undisposed SHA256; supports
  RS256/384/512 and picks the key by kid.
- Fix all nullable warnings; enable TreatWarningsAsErrors.

Changes:
- Switch from JwtSecurityTokenHandler to JsonWebTokenHandler
  (Microsoft.IdentityModel.JsonWebTokens 8.23.0) and drop Newtonsoft.Json.
- Split Program.cs into Options, KeyLoader, TokenValidator,
  TokenDiagnostics and ValidateCommand.
- Diagnostics: decoded header/claims, iat/nbf/exp as UTC with
  expired/not-yet-valid status, key set summary, kid mismatch warning,
  and a readable failure reason (full detail with --verbose).
- New options: --issuer, --audience, --validate-lifetime, --clock-skew.
- Exit codes: 0 valid, 1 invalid token, 2 usage or input error.
  --help and --version exit 0.
- Reject ambiguous input (both --jwt and --jwt-file, or several key sources).
- Add .editorconfig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Move JwtValidator to src/JwtValidator
- Replace JwtValidator.sln with JwtValidator.slnx (dotnet sln migrate),
  with src/ and tests/ solution folders and the repo files as solution items
- Move shared build settings (TFM, nullable, warnings as errors,
  code style enforcement) into Directory.Build.props

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tests/JwtValidator.Tests (xUnit v3 on Microsoft.Testing.Platform):
  56 tests that sign tokens with RSA/EC keys generated in memory and cover
  PEM and JWKS keys, the multi-key JWKS regression, ES256/384/512, PS256,
  wrong key, tampered payload, unsigned tokens, lifetime, issuer and
  audience checks, manual validation, diagnostics output, option
  validation and end-to-end exit codes (including JWKS over HTTP).
- global.json opts in to the Microsoft.Testing.Platform dotnet test mode.
- .github/workflows/build.yml builds and tests on push/PR to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 23:04
- Add /// documentation to every method and type in src and tests
- Enable GenerateDocumentationFile so doc comments (including crefs) are
  checked by the compiler, with warnings as errors
- Bump xunit.v3 4.0.1 -> 4.0.2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A malformed JWKS throws InvalidDataException, which escapes ValidateCommand's catch filter and crashes the process instead of returning the documented exit code 2.

1 open finding
What changed in this PR

This PR modernizes the JwtValidator CLI troubleshooting tool. It upgrades the target framework from .NET 8 to .NET 10, replaces the deprecated JwtSecurityTokenHandler/Newtonsoft.Json stack with JsonWebTokenHandler (Microsoft.IdentityModel.JsonWebTokens 8.23.0), and restructures the single Program.cs into focused types (Options, KeyLoader, TokenValidator, TokenDiagnostics, ValidateCommand, ExitCodes). It fixes several real validation bugs (notably JWKS multi-key handling that previously only used the last RSA key, trailing-newline handling in --jwt-file, EC key support, and undisposed crypto/HTTP resources), adds rich diagnostics output, opt-in claim checks, process exit codes, a src/ + tests/ layout with a .slnx solution, a comprehensive xUnit v3 test suite, and a GitHub Actions CI workflow.

Changes:

  • Upgrade to .NET 10 and switch to JsonWebTokenHandler; load JWKS via JsonWebKeySet with kid-aware matching and support for RS/PS/ES 256/384/512.
  • Split Program.cs into cohesive components, add diagnostics, new claim-check options, and standardized exit codes (0/1/2).
  • Introduce repository restructure (src/, tests/, .slnx, Directory.Build.props, .editorconfig, global.json), unit tests, and a CI build/test workflow.
File Description
src/​JwtValidator/​ValidateCommand.cs Orchestrates read/load/validate and maps outcomes to exit codes; catch filter omits InvalidDataException.
src/​JwtValidator/​KeyLoader.cs Loads keys from PEM/JWKS (URL/file/JSON); describes every JWKS entry; RSA parameter extraction.
src/​JwtValidator/​TokenValidator.cs Builds validation parameters and performs handler-based and manual RSA signature validation.
src/​JwtValidator/​TokenDiagnostics.cs Renders human-readable token/key diagnostics, time humanization, and lifetime notes.
src/​JwtValidator/​Options.cs CLI options plus usage-error validation for token/key source combinations and clock skew.
src/​JwtValidator/​Program.cs Entry point: arg parsing, help/version handling, usage errors, and command dispatch.
src/​JwtValidator/​ExitCodes.cs Centralizes 0/1/2 exit-code constants.
src/​JwtValidator/​JwtValidator.csproj Targets new package set, adds InternalsVisibleTo for tests.
src/​JwtValidator/​Properties/​launchSettings.json Dev launch profiles (sample tokens/JWKS, some stale).
tests/​JwtValidator.Tests/​*.cs xUnit v3 tests for options, key loading, validation, diagnostics, and the command.
tests/​JwtValidator.Tests/​JwtValidator.Tests.csproj Test project referencing xunit.v3 and the main project.
Directory.Build.props /​ global.json /​ .editorconfig Shared build settings, SDK/test-runner pinning, and style rules.
JwtValidator.slnx /​ JwtValidator.sln Migrates to the .slnx solution format.
.github/​workflows/​build.yml CI build and test on push/PR to main.
README.md /​ .gitignore Rewritten usage docs; broader ignore rules; untracks *.user.
JwtValidator/​Program.cs, *.csproj.user Removes the old flat-layout source and tracked user file.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +17 to +18
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or HttpRequestException
or TaskCanceledException or ArgumentException or JsonException)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants