Hi Team,
We recently encountered an authentication issue in the Web UI due to an invalid X.509 encryption certificate configured in the Identity Service. The application logs reported the following error in identity service:
System.InvalidOperationException: When using X.509 encryption credentials, at least one of the registered certificates must be valid
The issue occurred because the Identity Service could not identify a valid registered certificate for the required encryption operation. We resolved it by creating a new Identity Service build image and redeploying it. To prevent similar issues in the future, we would like to explore proactive monitoring and alerting mechanisms. Could you please advise on the following?
- Certificate validity monitoring: How can we monitor the validity and expiry dates of X.509 certificates used by the Identity Service?
- Expiry notifications: How can we configure automated alerts to notify the support team in advance of certificate expiry, allowing sufficient time for renewal or replacement?
- Preventive validation: Can we incorporate certificate validity checks into the CI/CD pipeline or application health checks to detect invalid or expired certificates before deployment?
Please share your recommendations on the best approach to implement these preventive measures.
Below is the code snippet for your reference.
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); if (!hostingEnvironment.IsDevelopment()) { PreConfigure<AbpOpenIddictAspNetCoreOptions>(options => { options.AddDevelopmentEncryptionAndSigningCertificate = false; });
PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.AddSigningCertificate(GetSigningCertificate(hostingEnvironment, configuration));
builder.AddEncryptionCertificate(GetSigningCertificate(hostingEnvironment, configuration));
private X509Certificate2 GetSigningCertificate(IWebHostEnvironment hostingEnv, IConfiguration configuration) { var fileName = "authserver.pfx"; var passPhrase = "XXXXXXXX-XXXX-XXXXX-XXXX-XXXXX8ED"; var file = Path.Combine(hostingEnv.ContentRootPath, fileName);
if (!File.Exists(file))
{
throw new FileNotFoundException($"Signing Certificate couldn't found: {file}");
}
return new X509Certificate2(file, passPhrase);
}
3 Answer(s)
-
0
Root cause / what that OpenIddict error means
System.InvalidOperationException: When using X.509 encryption credentials, at least one of the registered certificates must be valid- This is thrown by OpenIddict when none of the registered X.509 certificates are considered valid at runtime (typically: expired/not-yet-valid, or sometimes the cert loads but private key isn’t usable). In your code, you register a PFX via
AddSigningCertificate(...)+AddEncryptionCertificate(...), so if that PFX becomes invalid/expired, AuthServer/IdentityService will fail.
1) Certificate validity monitoring (recommended approach)
- Implement a small runtime check in the Identity Service that loads the same certificate(s) you register with OpenIddict and records:
NotBefore,NotAfterDateTime.UtcNowvsNotAfter- (optionally) whether the private key is available:
certificate.HasPrivateKey
- Then expose this via a monitoring-friendly surface:
- App logs (warn when expiring soon, error when expired)
- Metrics (e.g., Prometheus/Grafana) if you already use metrics
- A health check endpoint (see #3)
Example (simple reusable validator you can call from health checks and/or startup):
- Implement a small runtime check in the Identity Service that loads the same certificate(s) you register with OpenIddict and records:
using System.Security.Cryptography.X509Certificates; public static class CertificateValidity { public static void EnsureValid(X509Certificate2 cert, int warnBeforeDays = 30) { var now = DateTimeOffset.UtcNow; if (now < cert.NotBefore.ToUniversalTime()) { throw new InvalidOperationException( $"Certificate is not yet valid. NotBefore={cert.NotBefore:u}"); } if (now > cert.NotAfter.ToUniversalTime()) { throw new InvalidOperationException( $"Certificate is expired. NotAfter={cert.NotAfter:u}"); } // Optional: ensure private key exists when you expect one if (!cert.HasPrivateKey) { throw new InvalidOperationException("Certificate doesn't have a private key."); } var remaining = cert.NotAfter.ToUniversalTime() - now.UtcDateTime; if (remaining.TotalDays <= warnBeforeDays) { // log warning from your caller (health check can degrade, startup can warn, etc.) } } }2) Expiry notifications / alerting
- The most reliable pattern is:
- Publish a health check that fails (or “Degraded”) when the certificate is nearing expiry.
- Let your existing platform monitoring (Kubernetes liveness/readiness probes + Prometheus, Azure Monitor, Application Insights, etc.) alert on:
- health endpoint status
- metric threshold (days-to-expiry)
- log query (if you prefer log-based alerts)
- In ABP microservice environments, this is typically done outside ABP itself (in your infrastructure), but ABP can provide the endpoint/metric/log signals.
- The most reliable pattern is:
3) Preventive validation in CI/CD and/or application health checks
- CI/CD gate (pre-deploy):
- Add a pipeline step that loads the PFX (from the same secret store used by deployment) and verifies
NotAfteris beyond your required window (e.g., must be valid for at least 30 days). - This is the best way to prevent deploying an already-expired cert image/secret.
- Add a pipeline step that loads the PFX (from the same secret store used by deployment) and verifies
- Application health check (runtime / post-deploy):
- Add an ASP.NET Core health check that loads the cert (same code path as production) and fails if invalid/expired or expiring soon.
- Your orchestrator can then block traffic (readiness probe) or alert immediately.
Example health check:
- CI/CD gate (pre-deploy):
using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Security.Cryptography.X509Certificates; public sealed class OpenIddictCertificateHealthCheck : IHealthCheck { private readonly IWebHostEnvironment _env; private readonly IConfiguration _configuration; public OpenIddictCertificateHealthCheck(IWebHostEnvironment env, IConfiguration configuration) { _env = env; _configuration = configuration; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { try { // Load the same cert you use for OpenIddict var cert = LoadCertificate(_env, _configuration); // Fail if invalid/expired; optionally degrade if expiring soon CertificateValidity.EnsureValid(cert, warnBeforeDays: 30); var daysLeft = (cert.NotAfter.ToUniversalTime() - DateTime.UtcNow).TotalDays; if (daysLeft <= 30) { return Task.FromResult(HealthCheckResult.Degraded( $"Certificate is expiring soon. NotAfter={cert.NotAfter:u}, DaysLeft={daysLeft:0.##}")); } return Task.FromResult(HealthCheckResult.Healthy( $"Certificate OK. NotAfter={cert.NotAfter:u}, DaysLeft={daysLeft:0.##}")); } catch (Exception ex) { return Task.FromResult(HealthCheckResult.Unhealthy("Certificate validation failed.", ex)); } } private static X509Certificate2 LoadCertificate(IWebHostEnvironment env, IConfiguration configuration) { var fileName = "authserver.pfx"; var passPhrase = configuration["AuthServer:CertificatePassPhrase"] ?? "CHANGE-ME"; var file = Path.Combine(env.ContentRootPath, fileName); // Consider also passing X509KeyStorageFlags as needed for IIS/Azure return new X509Certificate2(file, passPhrase); } }Register health check (in your module):
public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddHealthChecks() .AddCheck<OpenIddictCertificateHealthCheck>("openiddict_certificate"); }Important operational recommendation (to avoid sudden failures)
- OpenIddict’s own message mentions rollover: “To use key rollover, register both the new certificate and the old one”.
- Practically, this means during renewal/rotation, deploy in a way that the service always has at least one currently-valid cert registered (often by temporarily registering both old+new during a rotation window).
Notes specific to ABP/OpenIddict production usage
- ABP strongly recommends using a real production certificate for OpenIddict and configuring it appropriately (rather than relying on dev cert behavior).
- If you run under IIS/Azure and face certificate load/private-key issues, use the ABP/OpenIddict production helper and pass
X509KeyStorageFlagsas shown in the ABP community article.
Sources:
- https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/openiddict-deployment
- https://abp.io/community/articles/0znavo8r
- https://abp.io/community/articles/kwnjkun1
- https://abp.io/support/questions/9016
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Please assign to support team to review and confirm.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
OpenIddict throws this error when all registered encryption certificates are outside their validity period.
Your snippet matches the older microservice template. The AuthServer Dockerfile in that template creates the pfx file during the image build:
RUN dotnet dev-certs https -v -ep /app/authserver.pfx -p <your-passphrase>A new certificate created by this command is valid for 365 days. Please check if your Dockerfile still has this line, and check the
NotBeforeandNotAfterdates of the pfx file that the AuthServer loads.I suggest you create your own certificate with the lifetime you need and mount it into the container, instead of creating it during the image build. Use an RSA certificate with a key of at least 2048 bits, and the pfx file must contain the private key. If the certificate has a Key Usage extension, it needs both Digital Signature and Key Encipherment, because you use the same file for signing and encryption. Your code loads the file at startup, so restart the AuthServer after you replace it.
For the monitoring and the alerts, you can add a health check. It reads the certificate that OpenIddict uses and returns
Degradedwhen it expires within 30 days.using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Server; namespace MyCompanyName.MyProjectName; public class OpenIddictCertificateHealthCheck : IHealthCheck { private static readonly TimeSpan WarningThreshold = TimeSpan.FromDays(30); private readonly IOptionsMonitor<OpenIddictServerOptions> _options; public OpenIddictCertificateHealthCheck(IOptionsMonitor<OpenIddictServerOptions> options) { _options = options; } public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { var options = _options.CurrentValue; var signing = GetExpiration(options.SigningCredentials[0].Key); var encryption = GetExpiration(options.EncryptionCredentials[0].Key); var expiration = signing < encryption ? signing : encryption; var remaining = expiration - DateTime.UtcNow; var data = new Dictionary<string, object> { { "expiration", expiration.ToString("u") }, { "remainingDays", (int)remaining.TotalDays } }; if (remaining <= TimeSpan.Zero) { return Task.FromResult(HealthCheckResult.Unhealthy($"The OpenIddict certificate expired on {expiration:u}.", data: data)); } if (remaining <= WarningThreshold) { return Task.FromResult(HealthCheckResult.Degraded($"The OpenIddict certificate expires in {(int)remaining.TotalDays} days.", data: data)); } return Task.FromResult(HealthCheckResult.Healthy($"The OpenIddict certificate expires on {expiration:u}.", data)); } private static DateTime GetExpiration(SecurityKey key) { return key is X509SecurityKey x509SecurityKey ? x509SecurityKey.Certificate.NotAfter.ToUniversalTime() : DateTime.MaxValue; } }Register it in
ConfigureServices:context.Services.AddHealthChecks() .AddCheck<OpenIddictCertificateHealthCheck>("openiddict-certificate");Then add the endpoint to your existing
UseConfiguredEndpointscall inOnApplicationInitialization:app.UseConfiguredEndpoints(endpoints => { endpoints.MapHealthChecks("/health/certificate"); });Your monitoring system can call this endpoint and send the alert. For
Degradedthe endpoint returns HTTP 200 withDegradedin the response body, so the alert has to check the body. It also has to fire on any non-200 response or when the endpoint is unreachable. The endpoint returns 503 or 500 when the certificate is no longer valid.For the pipeline, this command returns exit code 1 when the certificate expires within 30 days:
openssl pkcs12 -in authserver.pfx -passin pass:<your-passphrase> -nokeys | openssl x509 -noout -dates -checkend 2592000It prints both dates, but the exit code checks the expiry date only. Run it against the pfx file that the AuthServer will load.
Before deployment, start the new image with the certificate and call the health check endpoint. It has to return
Healthy. Also test a login against it, because the health check does not issue a token.When you replace the certificate, register the new one together with the old one and restart the AuthServer. The new certificate must already be valid at that time. OpenIddict then uses the one with the later expiry date. Keep the old one registered until the tokens issued with it have expired.
PreConfigure<OpenIddictServerBuilder>(builder => { builder.AddSigningCertificate(oldCertificate); builder.AddEncryptionCertificate(oldCertificate); builder.AddSigningCertificate(newCertificate); builder.AddEncryptionCertificate(newCertificate); });Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)