Skip to content

Comments

Release v2.2 - Custom JWT Implementation#41

Merged
kirill-abblix merged 40 commits intomasterfrom
release/2.2
Feb 18, 2026
Merged

Release v2.2 - Custom JWT Implementation#41
kirill-abblix merged 40 commits intomasterfrom
release/2.2

Conversation

@kirill-abblix
Copy link
Member

@kirill-abblix kirill-abblix commented Dec 23, 2025

Release v2.2 - Custom JWT Implementation & OpenID Connect Enhancements

Custom JWT Implementation

Implemented complete JWT signing and encryption infrastructure optimized exclusively for JSON tokens, eliminating Microsoft.IdentityModel.Tokens dependency from production code. Uses modern JsonObject-based programming model from System.Text.Json.Nodes designed specifically for JWT workflows.

Key improvements:

  • Native handling of JSON types (numbers, arrays, nested objects) without string conversions
  • Direct use of .NET cryptographic primitives (RSA, ECDsa, AES) for simplified key management
  • Exception-free Try pattern throughout validation pipeline for better performance
  • Specific, actionable error messages with explicit validation options

Enhanced JWE Algorithm Support

Added support for RSA-OAEP-256 (RSA-OAEP with SHA-256) and AES-GCM key wrapping algorithms (A128GCMKW, A192GCMKW, A256GCMKW) that were not available in Microsoft's JWT library.

Benefits:

  • Enhanced security over RSA-OAEP with SHA-1, recommended by modern security standards
  • Authenticated encryption for symmetric keys with single-pass operation
  • Superior performance compared to AES-CBC-HMAC

DirectKeyAgreement Algorithm

Implemented RFC 7518 Section 4.5 direct key agreement (dir algorithm) for JWE where the Content Encryption Key (CEK) is the shared symmetric key itself.

Use cases:

  • Most efficient key management mode when both parties share a symmetric key
  • Reduces computational cost and improves performance
  • Critical for microservices architectures and internal service-to-service communication

JsonWebKey Operation Capabilities

Added CanEncrypt, CanDecrypt, CanSign, and CanVerify properties to all JsonWebKey types (RSA, EC, Octet) with runtime validation. Keys are checked for required material before performing cryptographic operations.

ACR Values Support

Added acr_values_supported metadata to OpenID Connect discovery document per OpenID Connect Discovery 1.0. Introduced IAcrMetadataProvider interface for configurable Authentication Context Class Reference values.

RFC 8176 AMR Compliance

Removed non-standard Authentication Method Reference values not present in IANA registry. Documented proper RFC 8176 alternatives for migration guidance.

Interoperability Validation

Added comprehensive interoperability tests verifying bidirectional compatibility between Abblix JWT implementation and Microsoft.IdentityModel.Tokens JsonWebTokenHandler.

Coverage:

  • Unsigned JWTs
  • All signing algorithms (RSA, ECDSA, HMAC)
  • JWE encryption with multiple algorithm combinations
  • Full RFC 7515/7516 compliance verification

Bug Fixes

  • Dynamic Client Registration: Allow clients to provide JWKS for encryption and request signing regardless of authentication method, aligning with OIDC Dynamic Client Registration specification
  • SsrfValidatingHttpMessageHandler: Changed to transient lifetime to fix ObjectDisposedException when HttpClient disposes shared singleton handler
  • RFC 7523 Compliance: Accept both token endpoint URL and issuer identifier as valid audience values in client assertion JWTs
  • Session Claim Serialization: Fixed InvalidOperationException when serializing JsonValue<string> instances during session sign-in
  • Session Cookie Path: Added configurable Path property to CheckSessionCookieOptions for restricting session cookie scope
  • JWT Validation: Accept JWTs with missing exp/nbf claims for OIDC request objects; support client identification via client_id claim when issuer is missing

Code Quality & Refactoring

  • Improved JWT validation error messages with ToDescription extension method
  • Enhanced LicenseLogger thread safety with atomic operations
  • Fixed multiple SonarQube code smells (S3993, S1135, S3236, S6934, S107, and others)
  • Improved signing key selection with RFC 7517 fallback logic
  • Reduced AuthorizationResponseFormatter constructor parameters

Security & Maintenance

  • GitHub Actions: Fixed security vulnerability by validating user-controlled inputs before use
  • CodeQL: Upgraded from v3 to v4 (v3 deprecated December 2026)
  • Dependencies: Updated Microsoft.Extensions.* (10.0.2), Grpc.AspNetCore (2.76.0), Google.Protobuf (3.33.5), Microsoft.SourceLink.GitHub (10.0.102)

Add missing Authentication Method Reference (AMR) values from RFC 8176:
- bio: Generic biometric authentication
- oob: Out-of-band authentication
- x509: X.509 certificate-based authentication

These standardized AMR values complete the implementation of RFC 8176
and enable proper authentication method reporting in ID tokens.
Introduce IAcrMetadataProvider interface and default implementation to support
configurable ACR (Authentication Context Class Reference) values.

The provider pattern allows extensibility while the default implementation
reads values from DiscoveryOptions configuration. This enables dynamic
ACR values based on deployment environment or custom authentication policies.
Add support for acr_values_supported metadata in OpenID Connect discovery document
as defined in OpenID Connect Discovery 1.0 specification.

The discovery document now includes ACR values when configured, allowing clients
to discover supported authentication assurance levels. Integration uses the
IAcrMetadataProvider to retrieve configured values.

Changes:
- Add AcrValuesSupported property to ConfigurationResponse models
- Update ConfigurationHandler to populate ACR values from provider
- Update ConfigurationResponseFormatter to serialize ACR values to JSON
…ance

Remove non-standard Authentication Method Reference (AMR) values that are not
present in the IANA registry or RFC 8176 specification. Comment out "bio",
"oob", and "x509" constants with detailed explanations of why they are
non-compliant and what standard alternatives should be used instead.

The "oob" (out-of-band) identifier is used in OAuth for deprecated redirect
URIs, not as an AMR value. RFC 8176 defines "sms" and "tel" for out-of-band
methods. The generic "bio" should be replaced with specific biometric values
like "face", "fpt", "iris", "retina", or "vbm". The "x509" should use "sc"
(SmartCard) for certificate-based authentication per RFC 8176.

This change improves standards compliance and provides clear migration guidance
for developers using these values.
Allow clients to provide JWKS for encryption and request signing regardless of authentication method. Public clients and client_secret_* clients may use JWKS for ID token encryption, UserInfo encryption, and request object signing. This aligns with OIDC Dynamic Client Registration specification.

- Remove restriction preventing public clients from having JWKS
- Remove restriction preventing client_secret_basic/post clients from having JWKS
- Fix TLS authentication validation (tls_client_auth requires TLS metadata, self_signed_tls_client_auth can use JWKS)
- Update documentation explaining JWKS usage for encryption and signing
Fixes ObjectDisposedException when HttpClient disposes the shared singleton handler.
Changed from AddSingleton to AddTransient to ensure each HTTP client gets its own
handler instance that can be safely disposed.

Issue: 'upstream sent too big header' errors during Implicit Flow requests due to
disposed message handler causing 502 Bad Gateway responses.
…ntation (#39)

* feat: add missing RFC 8176 AMR values

Add missing Authentication Method Reference (AMR) values from RFC 8176:
- bio: Generic biometric authentication
- oob: Out-of-band authentication
- x509: X.509 certificate-based authentication

These standardized AMR values complete the implementation of RFC 8176
and enable proper authentication method reporting in ID tokens.

* feat: add IAcrMetadataProvider for ACR values configuration

Introduce IAcrMetadataProvider interface and default implementation to support
configurable ACR (Authentication Context Class Reference) values.

The provider pattern allows extensibility while the default implementation
reads values from DiscoveryOptions configuration. This enables dynamic
ACR values based on deployment environment or custom authentication policies.

* feat: add acr_values_supported to OpenID discovery document

Add support for acr_values_supported metadata in OpenID Connect discovery document
as defined in OpenID Connect Discovery 1.0 specification.

The discovery document now includes ACR values when configured, allowing clients
to discover supported authentication assurance levels. Integration uses the
IAcrMetadataProvider to retrieve configured values.

Changes:
- Add AcrValuesSupported property to ConfigurationResponse models
- Update ConfigurationHandler to populate ACR values from provider
- Update ConfigurationResponseFormatter to serialize ACR values to JSON

* refactor: remove non-standard AMR values and document RFC 8176 compliance

Remove non-standard Authentication Method Reference (AMR) values that are not
present in the IANA registry or RFC 8176 specification. Comment out "bio",
"oob", and "x509" constants with detailed explanations of why they are
non-compliant and what standard alternatives should be used instead.

The "oob" (out-of-band) identifier is used in OAuth for deprecated redirect
URIs, not as an AMR value. RFC 8176 defines "sms" and "tel" for out-of-band
methods. The generic "bio" should be replaced with specific biometric values
like "face", "fpt", "iris", "retina", or "vbm". The "x509" should use "sc"
(SmartCard) for certificate-based authentication per RFC 8176.

This change improves standards compliance and provides clear migration guidance
for developers using these values.

* Fix dynamic client registration JWKS validation

Allow clients to provide JWKS for encryption and request signing regardless of authentication method. Public clients and client_secret_* clients may use JWKS for ID token encryption, UserInfo encryption, and request object signing. This aligns with OIDC Dynamic Client Registration specification.

- Remove restriction preventing public clients from having JWKS
- Remove restriction preventing client_secret_basic/post clients from having JWKS
- Fix TLS authentication validation (tls_client_auth requires TLS metadata, self_signed_tls_client_auth can use JWKS)
- Update documentation explaining JWKS usage for encryption and signing

* fix: accept JWTs with missing exp/nbf claims in lifetime validation

Modified JsonWebTokenValidator to use custom LifetimeValidator that validates
exp/nbf claims when present but allows them to be missing. This is required
for OpenID Connect request objects which may not include expiration times.

Per OIDC spec, request objects are one-time use and bound to authorization
requests, making lifetime claims optional. The validator still properly
validates exp/nbf when present to reject expired or not-yet-valid tokens.

Added TimeProvider parameter to JsonWebTokenValidator constructor for
testability and consistent time handling in validation.

* fix: accept client JWTs without issuer claim for OIDC certification

Modified ClientJwtValidator to support client identification via client_id
claim when issuer claim is missing. This addresses OpenID Connect certification
test requirements where request objects may omit the issuer claim.

Client identification now follows this priority:
1. If issuer present: validate and use it to find client
2. If client_id claim present: use it to find client
3. If both present: validate they match the same client
4. If neither present: fail validation

RequestObjectFetcher now validates issuer when present (but accepts missing
issuer) to ensure proper client authentication while supporting flexible
JWT formats required by OIDC certification tests.

* refactor: simplify error handling and improve code clarity

Simplified RequestObjectFetchAdapter by using Result.MapFailure instead of
explicit pattern matching. Enhanced documentation for authorization request
fetching workflow.

Updated related components for consistency and improved readability.

* feat: add byte array concatenation and base64url error handling

Add utility methods for JWT implementation:
- ArrayExtensions.Concat for combining byte arrays in encryption/signing operations
- Enhanced Base64UrlTextEncoderConverter error handling for malformed JWT tokens

These utilities provide foundation for custom JWT processing.

* refactor: remove Microsoft.IdentityModel.Tokens from production code

Implement complete custom JWT signing and encryption infrastructure to eliminate
Microsoft.IdentityModel.Tokens dependency from Abblix.Jwt library.

Key changes:
- Add custom JWT signing for RSA, ECDSA, and HMAC algorithms (RFC 7515)
- Add custom JWT encryption for RSA-OAEP, AES-GCM, and direct key agreement (RFC 7516)
- Create EllipticCurveTypes constants for EC cryptography
- Reorganize JWK types into JsonWebKeys subfolder
- Add DirectKeyAgreement constructor with algorithm validation
- Remove Microsoft.IdentityModel.Tokens package from Abblix.Jwt.csproj
- Update OIDC Server to use custom JWT implementation
- Add Microsoft interoperability tests (179 tests passing)
- Keep Microsoft.IdentityModel.Tokens in test project for validation

Benefits:
- Broader algorithm support (RSA-OAEP-256, AES-GCM with RSA)
- Full control over JWT implementation
- Zero production dependency on Microsoft libraries
- 100% RFC 7515/7516 compliant

* refactor: redesign ValidationOptions with composite flags for clarity

Introduces composite ValidationOptions flags that combine "require" and
"validate" semantics into more intuitive single flags:
- RequireValidIssuer (RequireIssuer | ValidateIssuer)
- RequireValidAudience (RequireAudience | ValidateAudience)
- RequireValidSignedTokens (RequireSignedTokens | ValidateIssuerSigningKey)

This makes the API clearer while maintaining backward compatibility through
bitwise operations. Default validation remains comprehensive but now uses
composite flags for better maintainability.

* fix: improve JWT validation logic and exception handling

Enhances JWT validation by properly handling flag combinations and adding
exception handling for malformed tokens:

- Fixed signature validation to check both RequireSignedTokens and
  ValidateIssuerSigningKey flags
- Fixed issuer/audience validation to respect both "require" and "validate"
  flags independently
- Added JsonException handling in TryParseJsonObject to prevent crashes
  on malformed JWT JSON
- Simplified JwtBearerGrantHandler to use ValidationOptions.Default

These changes make validation more accurate and resilient to invalid input.

* feat: enhance signing key selection with RFC 7517 fallback logic

Improves JsonWebKeyExtensions.FirstByAlgorithmAsync to prioritize exact
algorithm matches while falling back to algorithm-agnostic keys per RFC 7517,
which allows keys without 'alg' parameter to be used with any compatible
algorithm.

Also adds clear error messages when no suitable signing key is found,
helping developers quickly identify configuration issues.

* chore: improve code quality and test infrastructure

- Make EllipticCurveOids and EllipticCurveTypes public for external use
- Simplify LicenseCollection with primary constructor syntax
- Streamline LicenseFixture by removing redundant field manipulation
- Add License collection to ClientIdValidatorTests for proper isolation
- Simplify ClientInfoBuilder initialization

* chore: upgrade Microsoft packages to 10.0.1

Update Microsoft.Extensions and related packages from 10.0.0 to 10.0.1
for latest bug fixes and improvements.

* fix: handle JsonValue<T> types in session claim serialization

Fixes InvalidOperationException when serializing JsonValue<string> instances
to claims during session sign-in. The bug occurred when processing external
ID tokens with custom claims during OpenID certification testing.

Changes:
- Renamed SerializeJsonValue to CreateClaim, returns Claim directly
- Use TryGetValue<T>() pattern instead of GetValue<JsonElement>()
- Added support for int, float, decimal, DateTime, DateTimeOffset
- Fixed TryParseJsonValue to use claim.ValueType for type preservation
- Added comprehensive unit tests (6 tests, all passing)
- Converted IsStandardClaim to pattern matching with 'or' operator

The fix ensures round-trip type preservation: sign-in → serialize claims →
sign-out → deserialize claims → sign-in maintains exact types.

Discovered during oidcc-idtoken-unsigned certification test when processing
external unsigned ID token with custom claims.
* feat: add missing RFC 8176 AMR values

Add missing Authentication Method Reference (AMR) values from RFC 8176:
- bio: Generic biometric authentication
- oob: Out-of-band authentication
- x509: X.509 certificate-based authentication

These standardized AMR values complete the implementation of RFC 8176
and enable proper authentication method reporting in ID tokens.

* feat: add IAcrMetadataProvider for ACR values configuration

Introduce IAcrMetadataProvider interface and default implementation to support
configurable ACR (Authentication Context Class Reference) values.

The provider pattern allows extensibility while the default implementation
reads values from DiscoveryOptions configuration. This enables dynamic
ACR values based on deployment environment or custom authentication policies.

* feat: add acr_values_supported to OpenID discovery document

Add support for acr_values_supported metadata in OpenID Connect discovery document
as defined in OpenID Connect Discovery 1.0 specification.

The discovery document now includes ACR values when configured, allowing clients
to discover supported authentication assurance levels. Integration uses the
IAcrMetadataProvider to retrieve configured values.

Changes:
- Add AcrValuesSupported property to ConfigurationResponse models
- Update ConfigurationHandler to populate ACR values from provider
- Update ConfigurationResponseFormatter to serialize ACR values to JSON

* refactor: remove non-standard AMR values and document RFC 8176 compliance

Remove non-standard Authentication Method Reference (AMR) values that are not
present in the IANA registry or RFC 8176 specification. Comment out "bio",
"oob", and "x509" constants with detailed explanations of why they are
non-compliant and what standard alternatives should be used instead.

The "oob" (out-of-band) identifier is used in OAuth for deprecated redirect
URIs, not as an AMR value. RFC 8176 defines "sms" and "tel" for out-of-band
methods. The generic "bio" should be replaced with specific biometric values
like "face", "fpt", "iris", "retina", or "vbm". The "x509" should use "sc"
(SmartCard) for certificate-based authentication per RFC 8176.

This change improves standards compliance and provides clear migration guidance
for developers using these values.
Fix dynamic client registration JWKS validation to properly handle credential validation.
…ssue

Fix GitHub Actions security vulnerability by validating user-controlled inputs
before using them in action parameters. The 'pre_release' input is now normalized
to a validated boolean output to prevent injection attacks.

Clean up codebase by removing commented code in AuthenticationMethodReferences
and extracting nested code blocks into separate methods in AutoPostFormatter
and FrontChannelLogoutResult for better readability and maintainability.
- Add explicit AttributeUsage to HttpGetOrPostAttribute for clarity (S3993)
- Change TODO to NOTE in AuthenticationSchemeAdapter for future enhancement (S1135)
- Remove redundant parameter name from ArgumentNullException.ThrowIfNull to preserve caller info (S3236)
- Suppress S6934 in controllers where action-level routes make class-level route redundant
Address various SonarQube issues to improve code quality and maintainability:
- S1006: Add default parameter value in JsonSerializationBinder to match interface
- S1104: Convert public field to property in AuthorizationValidationContext
- S4136: Group InvalidRequest method overloads together in ErrorFactory
- S1066: Merge nested conditionals in AggregationExtensions
- S1118: Make LicenseLoader class static as all members are static
- S4487: Suppress warning for timer field in LicenseLogger (must remain alive)
- S7758: Use String.fromCodePoint in loop instead of apply() in checkSession.html
- S127: Replace for loop with while loop in EnumerableExtensions to avoid updating counter in body
- Fix S125: Remove commented code from AuthenticationMethodReferences.cs
- Fix S4138: Use for-of loop in checkSession.html for better code quality
- Fix race condition in IsAllowed using atomic AddOrUpdate operation
- Improve cleanup loop efficiency by collecting keys before removal
- Add null validation in Init method
- Fix misleading documentation in Log method (doesn't implement throttling directly)
- Document singleton lifetime and timer lifecycle in class remarks
- Add comprehensive XML documentation explaining timer callback purpose
- Document that method prevents unbounded memory growth
- Include parameter documentation with proper see-cref reference
- Rename method to better describe its purpose
- More descriptive than generic OnTimer callback name
- Include specific error details from JWT validation in error message
- Convert error to lowercase for consistency with error message style
- Apply changes to both IdTokenHintValidator and UserIdentityValidator
- Helps developers debug token validation issues more effectively
- Create extension method for formatting JWT validation errors with proper capitalization
- Support both standalone error messages (preserve caps) and embedded messages (lowercase first)
- Apply to IdTokenHintValidator, UserIdentityValidator, and UserInfoRequestValidator
- Provides consistent, user-friendly error messages across all JWT validation points
- Includes comprehensive XML documentation with usage examples
- Fix incorrect false.ToDescription() to error.ToString()
- Use default ToString() which returns error description with preserved capitalization
Modify ClientJwtValidator to accept either the token endpoint URL or the
authorization server's issuer identifier as valid audience values in
client assertion JWTs. This complies with RFC 7523 Section 3 and OpenID
Connect Core 1.0 Section 9, which specify both values are acceptable.

Changes:
- Add IIssuerProvider dependency to access issuer identifier
- Update ValidateAudience to check both requestUri and issuer
- Enhance logging to show both expected audience values
- Add test coverage for issuer identifier as audience
Update CodeQL Action to v4 to address deprecation warning. CodeQL Action v3
will be deprecated in December 2026.

Changes:
- Update github/codeql-action/init from v3 to v4
- Update github/codeql-action/analyze from v3 to v4
Add CanEncrypt, CanDecrypt, CanSign, and CanVerify properties to JsonWebKey base class and all derived types (RSA, EC, Octet). These properties determine key usability based on available key material:
- RSA: public key (Modulus+Exponent) for encrypt/verify, private key (PrivateExponent) for decrypt/sign
- EC: public key (X+Y+Curve) for encrypt/verify, private key (PrivateKey) for decrypt/sign
- Octet: symmetric key (KeyValue) required for all operations

Add runtime validation in JsonWebTokenSigner and JsonWebTokenEncryptor that checks key capabilities before performing cryptographic operations. Throws InvalidOperationException with descriptive messages when keys lack required material.

Add comprehensive test suite covering all key types, edge cases, and consistency with Sanitize behavior.
Add [JsonIgnore] attributes to computed capability properties in all
JsonWebKey implementations (RSA, EC, Octet). These properties are
derived from the presence of key material and should not be included
when serializing keys to JSON format, as they would introduce
non-standard fields into the JWK structure.
…gement

Add Path property to CheckSessionCookieOptions to allow restricting session
cookie scope to the check_session_iframe endpoint. Also improve cache prevention
headers with comprehensive directives (no-store, no-cache, must-revalidate,
max-age=0, Expires) for maximum browser compatibility.

This removes the custom HttpResponseHeaders class in favor of standard
Microsoft.Net.Http.Headers constants.
Fixes SonarCloud S107 (constructor has 8 parameters, max 7 authorized).

Changes:
- Remove ILogger and IServiceProvider from constructor (8 → 6 params)
- Remove runtime validation in favor of XML documentation
- Document session cookie requirements in SessionManagementService
- Update CheckSessionCookieOptions docs to clarify SameSite must be "None"
Updates:
- Microsoft.Extensions.* packages: 10.0.1 → 10.0.2
- Microsoft.SourceLink.GitHub: 8.0.0 → 10.0.102
- Grpc.AspNetCore: 2.71.0 → 2.76.0
- Google.Protobuf: 3.33.2 → 3.33.5
- Microsoft.AspNetCore.Http.Abstractions: 2.3.0 → 2.3.9
The test was using shared options with Path defaulting to "/",
but it should test the fallback behavior when Path is empty.
Now creates isolated options with Path="" to properly test
that empty path falls back to RequestInfoProvider.PathBase.
…pport

Replace programmatic XML-based HTML generation with a template-based approach
using an embedded HTML resource. The new FrontChannelLogoutService generates
cryptographically secure CSP nonces per request, enabling Content-Security-Policy
headers that protect against XSS attacks while allowing the inline script for
iframe synchronization.

The HTML template includes browser compatibility improvements: lang attribute,
charset meta tag, and zero-iframes handling for edge cases.
Add Content-Security-Policy nonce support to check_session_iframe for XSS
protection. Fix browser compatibility issues in checkSession.html: correct
DOCTYPE declaration, add charset meta tag, replace ES2020+ features
(replaceAll, nullish coalescing) with ES5-compatible alternatives for
older browser support.
Extract SetNoCacheHeaders extension method for HttpResponse to eliminate
duplication between ActionResult decorators and GeneratedHtmlResult.
Use typed headers with CacheControlHeaderValue for type safety and add
s-maxage directive for shared cache prevention.
Add sealed modifier to private CheckSessionHtmlResult class.
Switch to String.replaceAll() for base64url encoding in checkSession.html.
Update NuGet package dependencies:
- Microsoft.Extensions.* 10.0.2 → 10.0.3
- Microsoft.SourceLink.GitHub 10.0.102 → 10.0.103
- coverlet.collector 6.0.4 → 8.0.0
- System.IdentityModel.Tokens.Jwt 8.15.0 → 8.16.0

Bump version to 2.2 in all publishable packages:
- AssemblyVersion: 2.1.0 → 2.2.0
- FileVersion: 2.1.0 → 2.2.0
- PackageVersion: 2.1 → 2.2

Update PackageReleaseNotes to reference v2.2 release.
Replace the single generic Nuget/README.md with tailored READMEs
for each package (Utils, DependencyInjection, JWT, OIDC.Server,
OIDC.Server.MVC) highlighting package-specific features, v2.2
changes, and relevant standards.
Release notes live in signed tag annotations. Migration guide is
in MIGRATION-2.0.md. Simplified RELEASE_NOTES gitignore pattern.
@sonarqubecloud
Copy link

@kirill-abblix kirill-abblix merged commit cb1e2cd into master Feb 18, 2026
2 checks passed
@kirill-abblix kirill-abblix deleted the release/2.2 branch February 18, 2026 19:15
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.

1 participant