Skip to content

Commit b51f78f

Browse files
committed
feat(auth): add certificate provider for dev environments
- Add new ICertificateProvider implementation for development environments - Update dependency injection to use the new provider in dev environments - Add logging for existing DopplerCertificateProvider.
1 parent 3a9f77f commit b51f78f

File tree

4 files changed

+109
-4
lines changed

4 files changed

+109
-4
lines changed

src/Openlysis.Authentication.API/Infrastructure/DependencyInjection.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ internal static class DependencyInjection
3434
/// </summary>
3535
/// <param name="services">The service collection to add services to.</param>
3636
/// <param name="configuration">The application configuration instance.</param>
37+
/// <param name="environment">The host environment instance.</param>
3738
public static void AddInfrastructure(
3839
this IServiceCollection services,
39-
IConfiguration configuration)
40+
IConfiguration configuration,
41+
IHostEnvironment environment)
4042
{
4143
AddRepositories(services, configuration);
4244
AddPasswordHasher(services, configuration);
43-
AddCertificateProvider(services, configuration);
45+
AddCertificateProvider(services, configuration, environment);
4446
AddSigningCertificateManager(services);
4547
AddTokenGenerator(services, configuration);
4648
AddTokenHasher(services);
@@ -101,8 +103,17 @@ private static void AddPasswordHasher(
101103

102104
private static void AddCertificateProvider(
103105
IServiceCollection services,
104-
IConfiguration configuration)
106+
IConfiguration configuration,
107+
IHostEnvironment environment)
105108
{
109+
if (environment.IsDevelopment())
110+
{
111+
services.AddSingleton<DevelopmentCertificateProvider>();
112+
services.AddHostedService(sp => sp.GetRequiredService<DevelopmentCertificateProvider>());
113+
services.AddSingleton<ICertificateProvider>(sp => sp.GetRequiredService<DevelopmentCertificateProvider>());
114+
return;
115+
}
116+
106117
var certificateOptionsSection = configuration
107118
.GetRequiredSection(DopplerCertificateOptions.SectionName);
108119

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System.Security.Cryptography;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
namespace Openlysis.Authentication.API.Infrastructure.Services;
5+
6+
/// <summary>
7+
/// Provides a X509 certificate for authentication purposes in development environments.
8+
/// </summary>
9+
internal sealed class DevelopmentCertificateProvider : ICertificateProvider, IHostedService, IDisposable
10+
{
11+
private const string EcCurveFriendlyName = "nistP256";
12+
private const string X500DistinguishedName = "CN=OpenlysisAuth";
13+
private const int CertificateYearsValidity = 1;
14+
private const string CertFileName = "dev_x509_certificate.pem";
15+
private const string PrivateKeyFileName = "dev_ecdsa_key.pem";
16+
17+
/// <inheritdoc/>
18+
public X509Certificate2 Certificate
19+
{
20+
get
21+
{
22+
return _selfSignedCertificate ?? throw new InvalidOperationException("Certificate has not been set.");
23+
}
24+
}
25+
26+
private readonly ILogger<DevelopmentCertificateProvider> _logger;
27+
private X509Certificate2? _selfSignedCertificate;
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="DevelopmentCertificateProvider"/> class.
31+
/// </summary>
32+
/// <param name="logger">The logger instance for logging certificate provider events.</param>
33+
public DevelopmentCertificateProvider(ILogger<DevelopmentCertificateProvider> logger)
34+
{
35+
_logger = logger;
36+
}
37+
38+
/// <inheritdoc/>
39+
public void Dispose()
40+
{
41+
_selfSignedCertificate?.Dispose();
42+
}
43+
44+
/// <inheritdoc/>
45+
public async Task StartAsync(CancellationToken cancellationToken)
46+
{
47+
string certFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, CertFileName);
48+
string privateKeyFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PrivateKeyFileName);
49+
50+
if (File.Exists(certFilePath) && File.Exists(privateKeyFilePath))
51+
{
52+
_selfSignedCertificate = X509Certificate2.CreateFromPemFile(certFilePath, privateKeyFilePath);
53+
if (_selfSignedCertificate.NotAfter.ToUniversalTime() > DateTime.UtcNow)
54+
{
55+
_logger.LogInformation("X509 certificate for development has been loaded successfully from existing certificate.");
56+
return;
57+
}
58+
}
59+
60+
using var ecDsaKeys = ECDsa.Create(ECCurve.CreateFromFriendlyName(EcCurveFriendlyName));
61+
_selfSignedCertificate = CreateCertificate(ecDsaKeys);
62+
63+
string certPem = _selfSignedCertificate.ExportCertificatePem();
64+
string privateKeyPem = ecDsaKeys.ExportECPrivateKeyPem();
65+
66+
await File.WriteAllTextAsync(certFilePath, certPem, cancellationToken);
67+
await File.WriteAllTextAsync(privateKeyFilePath, privateKeyPem, cancellationToken);
68+
69+
_logger.LogInformation("X509 certificate for development has been created and loaded successfully.");
70+
}
71+
72+
/// <inheritdoc/>
73+
public Task StopAsync(CancellationToken cancellationToken)
74+
{
75+
return Task.CompletedTask;
76+
}
77+
78+
private static X509Certificate2 CreateCertificate(ECDsa ecDsaKeys)
79+
{
80+
var subjectName = new X500DistinguishedName(X500DistinguishedName);
81+
var request = new CertificateRequest(subjectName, ecDsaKeys, HashAlgorithmName.SHA256);
82+
83+
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
84+
85+
DateTimeOffset notBefore = DateTimeOffset.UtcNow;
86+
DateTimeOffset notAfter = notBefore.AddYears(CertificateYearsValidity);
87+
return request.CreateSelfSigned(notBefore, notAfter);
88+
}
89+
}

src/Openlysis.Authentication.API/Infrastructure/Services/DopplerCertificateProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Openlysis.Authentication.API.Infrastructure.Services;
1515
/// </summary>
1616
internal sealed class DopplerCertificateProvider : ICertificateProvider, IHostedService, IDisposable
1717
{
18+
private readonly ILogger<DopplerCertificateProvider> _logger;
1819
private readonly IOptions<DopplerCertificateOptions> _certificateOptions;
1920
private readonly IDopplerClient _dopplerClient;
2021
private X509Certificate2? _certificate;
@@ -36,12 +37,15 @@ public X509Certificate2 Certificate
3637
/// <summary>
3738
/// Initializes a new instance of the <see cref="DopplerCertificateProvider"/> class.
3839
/// </summary>
40+
/// <param name="logger">The logger instance for logging operations.</param>
3941
/// <param name="certificateOptions">The options containing Doppler certificate configuration.</param>
4042
/// <param name="dopplerClient">The Doppler client used to fetch secrets.</param>
4143
public DopplerCertificateProvider(
44+
ILogger<DopplerCertificateProvider> logger,
4245
IOptions<DopplerCertificateOptions> certificateOptions,
4346
IDopplerClient dopplerClient)
4447
{
48+
_logger = logger;
4549
_certificateOptions = certificateOptions;
4650
_dopplerClient = dopplerClient;
4751
}
@@ -69,6 +73,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
6973
}
7074

7175
_certificate = CreateCertificate(certSecret, passwdSecret);
76+
_logger.LogInformation("X509 certificate from Doppler has been loaded successfully.");
7277
}
7378

7479
/// <inheritdoc/>

src/Openlysis.Authentication.API/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Openlysis.Authentication.API.Infrastructure;
44

55
var builder = WebApplication.CreateSlimBuilder(args);
6-
builder.Services.AddInfrastructure(builder.Configuration);
6+
builder.Services.AddInfrastructure(builder.Configuration, builder.Environment);
77
builder.Services.AddApplication();
88
builder.Services.AddApi(builder.Configuration);
99
var app = builder.Build();

0 commit comments

Comments
 (0)