-
Notifications
You must be signed in to change notification settings - Fork 1
313 client assertion pem #42
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
base: main
Are you sure you want to change the base?
Conversation
src/Fhi.Authentication.Extensions/ClientCredentials/ClientCredentialsAssertionService.cs
Outdated
Show resolved
Hide resolved
src/Fhi.Authentication.Extensions/ServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
…' into feature/313-client-assertion-PEM # Conflicts: # src/Fhi.Authentication.Extensions/ClientCredentials/ClientAssertionOptions.cs # src/Fhi.Authentication.Extensions/ClientCredentials/ClientCredentialsAssertionService.cs # src/Fhi.Authentication.Extensions/Tokens/ICertificateKeyHandler.cs
Kattemat
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Synes det ser bra ut jeg :)
|
Gikk over til et factory pattern i denne committen: c26d5a0 |
# Conflicts: # tests/Fhi.Authentication.Extensions.UnitTests/Tokens/ClientAssertionTokenHandlerTests.cs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
|
New attempt at making a nice, expandable architecture. If none of these are acceptable I suggest someone rewrites this to their liking so I can learn from a final version, I won't be making yet another attempt at this point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 22 comments.
Comments suppressed due to low confidence (1)
tests/Fhi.Auth.IntegrationTests/Setup/TestCertificateBuilder.cs:46
- This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.
This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.
var rsa = RSA.Create(_keySize);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/Fhi.Authentication.Extensions.UnitTests/Tokens/CertificateKeyHandler.UnitTests.cs
Outdated
Show resolved
Hide resolved
src/Fhi.Authentication.Extensions/ClientCredentials/CertificateSecretStore.cs
Outdated
Show resolved
Hide resolved
tests/Fhi.Auth.IntegrationTests/Setup/TestCertificateBuilder.cs
Outdated
Show resolved
Hide resolved
src/Fhi.Authentication.Extensions/ClientCredentials/ClientCredentialsExtensions.cs
Outdated
Show resolved
Hide resolved
tests/Fhi.Auth.IntegrationTests/Setup/TestCertificateBuilder.cs
Outdated
Show resolved
Hide resolved
src/Fhi.Authentication.Extensions/ClientCredentials/ClientCredentialsAssertionService.cs
Outdated
Show resolved
Hide resolved
src/Fhi.Authentication.Extensions/ClientCredentials/ClientCredentialsAssertionService.cs
Outdated
Show resolved
Hide resolved
src/Fhi.Authentication.Extensions/Tokens/ICertificateProvider.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 27 comments.
Comments suppressed due to low confidence (2)
tests/Fhi.Auth.IntegrationTests/Setup/TestCertificateBuilder.cs:57
- This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.
var persistable = new X509Certificate2(pfx, (string?)null, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
tests/Fhi.Auth.IntegrationTests/Setup/TestCertificateBuilder.cs:63
- This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.
var publicOnly = X509Certificate2.CreateFromPem("-----BEGIN CERTIFICATE-----\n" + Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks) + "\n-----END CERTIFICATE-----\n");
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| [TestFixture] | ||
| [Explicit ("Requires Windows and Local Machine Certificate Store access")] | ||
| public class CertificateKeyHandlerTests |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test class is named "CertificateKeyHandlerTests" but the field and setup reference "IPrivateKeyHandler". For consistency with the interface/class being tested, consider renaming to "PrivateKeyHandlerTests".
| public class CertificateKeyHandlerTests | |
| public class PrivateKeyHandlerTests |
|
|
||
| using (Assert.EnterMultipleScope()) | ||
| { | ||
| Assert.That(jwkString, Is.Not.Null.Or.Empty); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assertion uses "Is.Not.Null.Or.Empty" which is not valid NUnit syntax. It should be "Is.Not.Null.And.Not.Empty" to properly combine the null check and empty string check.
| Assert.That(jwkString, Is.Not.Null.Or.Empty); | |
| Assert.That(jwkString, Is.Not.Null.And.Not.Empty); |
| private bool Load(X509Certificate2 certificate) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(certificate); | ||
|
|
||
| // Default: only load certificates with private keys that are not expired | ||
| if (!certificate.HasPrivateKey) | ||
| { | ||
| Logger.LogDebug("Skipping certificate {Thumbprint} - no private key", certificate.Thumbprint); | ||
| return false; | ||
| } | ||
|
|
||
| if (certificate.NotAfter < DateTime.Now) | ||
| { | ||
| Logger.LogWarning("Skipping expired certificate {Thumbprint}, expired on {ExpiryDate}", | ||
| certificate.Thumbprint, certificate.NotAfter); | ||
| return false; | ||
| } | ||
|
|
||
| if (certificate.NotBefore > DateTime.Now) | ||
| { | ||
| Logger.LogWarning("Skipping certificate {Thumbprint}, not valid until {ValidFrom}", | ||
| certificate.Thumbprint, certificate.NotBefore); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Load method is private but follows a naming convention that suggests it should be virtual and overridable (similar to the pattern mentioned in the class documentation about Azure's KeyVaultSecretManager). Consider making this method virtual and protected to allow derived classes to customize certificate filtering logic.
|
|
||
| if (!string.IsNullOrEmpty(certificateThumbprint)) | ||
| { | ||
| services.AddSingleton<CertificateSecretManager>(); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CertificateSecretManager is registered as a singleton without checking if it's actually needed. When using FileSecretStore (lines 155-161), the CertificateSecretManager is still registered but never used. Consider moving the CertificateSecretManager registration inside the certificate-specific block (line 140) to avoid unnecessary service registrations.
| var app = builder.Build(); | ||
|
|
||
| var app = builder.Build(); | ||
| await app.StartAsync(); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The app.StartAsync() is called but the subsequent code to actually use the service (commented out lines for getting health records) is missing. This makes the sample incomplete and non-functional. Either restore the functional code or add a comment explaining that this is intentionally incomplete pending the separate PR mentioned in the description.
| await app.StartAsync(); | |
| await app.StartAsync(); | |
| // The host is now started and HealthRecordService can be resolved from the DI container. | |
| // This sample intentionally stops after startup to focus on configuration of client | |
| // credentials authentication. Example code that calls the protected API using | |
| // HealthRecordService is provided separately. |
| sp.GetRequiredService<IPrivateKeyHandler>(), | ||
| sp.GetRequiredService<ILogger<CertificateSecretStore>>(), | ||
| sp.GetRequiredService<CertificateSecretManager>()); | ||
| }); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AddCertificateStoreKeyHandler extension method is not called in the ConfigureWithSecretStore method, but IPrivateKeyHandler is retrieved from the service provider. This will cause a runtime exception when GetRequiredService<IPrivateKeyHandler> is called because IPrivateKeyHandler was never registered. Add services.AddCertificateStoreKeyHandler() before the ISecretStore registration.
| if (string.IsNullOrEmpty(optionName)) | ||
| throw new ArgumentException("Option name cannot be null or empty", nameof(optionName)); | ||
|
|
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This validation throws an ArgumentException for an empty optionName, but this check is inconsistent with the other overloads which don't perform this validation. Either add this validation to all overloads for consistency, or remove it from this one and let the framework handle the validation.
| if (string.IsNullOrEmpty(optionName)) | |
| throw new ArgumentException("Option name cannot be null or empty", nameof(optionName)); |
| services.TryAddTransient<ICertificateProvider>(_ => | ||
| new StoreCertificateProvider(certificate.StoreLocation)); | ||
| services.TryAddTransient<IPrivateKeyHandler, PrivateKeyHandler>(); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses TryAddTransient for ICertificateProvider and IPrivateKeyHandler, which means if they're already registered, these registrations will be ignored. However, a specific StoreLocation is being configured based on certificate.StoreLocation. If a different registration already exists with a different StoreLocation, this could lead to unexpected behavior. Consider either using AddTransient (which would throw on duplicate) or documenting this behavior clearly.
| services.TryAddTransient<ICertificateProvider>(_ => | |
| new StoreCertificateProvider(certificate.StoreLocation)); | |
| services.TryAddTransient<IPrivateKeyHandler, PrivateKeyHandler>(); | |
| services.AddTransient<ICertificateProvider>(_ => | |
| new StoreCertificateProvider(certificate.StoreLocation)); | |
| services.AddTransient<IPrivateKeyHandler, PrivateKeyHandler>(); |
| this IServiceCollection services, | ||
| CertificateStoreLocation storeLocation = CertificateStoreLocation.CurrentUser) | ||
| { | ||
| services.AddTransient<ICertificateProvider>(_ => new StoreCertificateProvider(storeLocation)); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AddCertificateStoreKeyHandler method registers both ICertificateProvider and IPrivateKeyHandler as Transient, which means a new StoreCertificateProvider instance is created for each request. Since StoreCertificateProvider is stateless and accesses the certificate store each time, consider using Singleton lifetime instead to reduce allocations and improve performance.
| services.AddTransient<ICertificateProvider>(_ => new StoreCertificateProvider(storeLocation)); | |
| services.AddSingleton<ICertificateProvider>(_ => new StoreCertificateProvider(storeLocation)); |
| @@ -0,0 +1,171 @@ | |||
| using System.Security.Cryptography; | |||
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file name "CertificateKeyHandler.UnitTests.cs" doesn't follow the conventional test file naming pattern. It should be "PrivateKeyHandlerTests.cs" or "PrivateKeyHandler.UnitTests.cs" to match the class being tested (PrivateKeyHandler) and be consistent with other test files in the project.
| { | ||
| if (certificate is null) throw new ArgumentNullException(nameof(certificate)); | ||
|
|
||
| if (certificate.NotAfter < DateTime.Now) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably use DateTime.UtcNow to avoid time zone issues
Added functionality for retrieving private key from PEM file in certificate store, and related tests. Integration tests require access to cert store.
I was a bit unsure how to hook up the functionality when it comes to DI, ended up with adding optional extension method that must be called when pem is relevant. Not sure if this is the best way, there's probably more elegant ways of connecting the dots here.
Sample code will be added in a separate PR
RelatesTo #313