Skip to main content

Overview

Gallery.CredentialExpiration is a scheduled background job (console executable) that monitors NuGet Gallery user API keys approaching or past their expiration date and dispatches targeted email notifications. It serves two notification categories per user:
  • Expiring — keys that will expire within a configurable warning window (e.g., 10 days).
  • Expired — keys that crossed their expiration timestamp since the last successful job run.
The job is designed to run on a recurring schedule (managed externally by NSSM or a similar service wrapper). It persists a JSON cursor to Azure Blob Storage after every run so that expiry windows are tracked incrementally and duplicate notifications are avoided even when the job is delayed or restarts.
The job uses a WhatIf configuration flag. When set to true, all database queries and cursor updates execute normally, but no emails are dispatched. This is intended for development and integration environments to prevent accidental user spam.

Role in the System

Gallery Database Consumer

Queries the Credentials table in the NuGet Gallery SQL database for apikey.v3 and apikey.v4 credential types whose expiry falls within the job’s computed time window.

Email Pipeline Producer

Publishes outbound email messages to an Azure Service Bus topic using the shared NuGet.Services.Messaging infrastructure. Actual delivery is handled downstream by a separate email worker.

Cursor-Driven Incremental Processing

Reads and writes a cursorv2.json file in Azure Blob Storage to track the last processed timestamp, enabling safe re-runs and gap recovery.

Standalone NuGet Job

Extends JsonConfigurationJob from NuGet.Jobs.Common, following the standard job pattern used across the NuGetGallery background job fleet.

Key Files and Classes

File PathClass / TypePurpose
Program.csProgramEntry point; creates Job and hands off to JobRunner.Run().
Job.csJobMain job class. Initialises services in Init(), orchestrates the full run loop in Run().
GalleryCredentialExpiration.csGalleryCredentialExpirationImplements ICredentialExpirationExporter. Executes the SQL query and filters results into expiring vs. expired buckets.
ICredentialExpirationExporter.csICredentialExpirationExporterInterface contract for credential retrieval and categorisation.
CredentialExpirationEmailBuilder.csCredentialExpirationEmailBuilderExtends MarkdownEmailBuilder. Constructs subject lines and Markdown bodies for both expiry states.
JobRunTimeCursor.csJobRunTimeCursorSerialisable cursor POCO holding JobCursorTime and MaxProcessedCredentialsTime, persisted as cursorv2.json.
Configuration/InitializationConfiguration.csInitializationConfigurationStrongly-typed configuration POCO bound from appsettings.json.
Models/ExpiredCredentialData.csExpiredCredentialDataDTO populated directly from the SQL result set.
Models/CredentialExpirationJobMetadata.csCredentialExpirationJobMetadataImmutable value object combining run time, cursor, and warn-days into a single context.
LogEvents.csLogEventsStructured log event IDs (600–651) for failed email, failed credential handling, and job lifecycle failures.
Strings.resxEmbedded resource containing email subject/body templates and the raw SQL query for credential retrieval.

Dependencies

Internal Project References

ProjectRole
NuGet.Jobs.CommonBase JsonConfigurationJob, JobRunner, GalleryDbConfiguration, QueryWithRetryAsync, DI wiring.
NuGet.Services.StorageAzureStorageFactory, AzureStorage, BlobServiceClientFactory — blob cursor persistence.

Key NuGet Dependencies

PackageUsage
NuGet.Services.MessagingAsynchronousEmailMessageService, EmailMessageEnqueuer, MarkdownEmailBuilder.
NuGet.Services.ServiceBusTopicClientWrapper, ServiceBusMessageSerializer — Service Bus email dispatch.
Azure.IdentityManaged-identity blob storage auth.
Newtonsoft.JsonCursor serialisation (strict MissingMemberHandling.Error).
System.Data.SqlClientGallery SQL connection.

SQL Query

The credential retrieval query is stored in Strings.resx:
SELECT cr.[Type], cr.[Created], cr.[Expires], cr.[Description],
       u.[EmailAddress], u.[Username]
FROM [Credentials] AS cr
INNER JOIN Users AS u ON u.[Key] = cr.[UserKey]
WHERE u.[EmailAllowed] = 1
  AND u.[EmailAddress] <> ''
  AND Expires IS NOT NULL
  AND cr.[Expires] <= CONVERT(datetime, @MaxNotificationDate)
  AND cr.[Expires] >= CONVERT(datetime, @MinNotificationDate)
  AND (cr.[Type] = 'apikey.v3' OR cr.[Type] = 'apikey.v4')
  AND cr.[RevocationSourceKey] IS NULL
ORDER BY u.[Username]
Only apikey.v3 and apikey.v4 credential types are targeted. Revoked credentials (RevocationSourceKey IS NOT NULL) are explicitly excluded. Users must have EmailAllowed = 1 and a non-empty email address.

Notable Patterns and Quirks

The cursor file is named cursorv2.json. The v2 suffix implies a breaking schema change from a prior cursor format. Deserialisation uses MissingMemberHandling.Error, meaning any cursor file written by an older schema version will cause the job to throw and halt rather than silently misprocess notifications.
Non-scoped API keys (those with a null or empty Description) are backfilled with the string "Full access API key" before grouping. This is defined as Constants.NonScopedApiKeyDescription in ExpiredCredentialData.cs.
The expiring-credentials boundary uses MaxProcessedCredentialsTime from the cursor rather than JobRunTime when the cursor’s value is ahead of the current run time. This guards against sending duplicate expiry warnings if the job runs more frequently than the warning window.

Email Aggregation Pattern

Credentials are grouped per user before emailing. Each user receives at most two emails per run — one for expiring keys and one for expired keys — with all affected key names listed as bullet points inside a single message body. This avoids per-key email storms for users with many API keys.

Configuration Reference

PropertyDescription
ContainerNameAzure Blob container name for cursor storage.
DataStorageAccountUrlBlob service endpoint URL (used with managed identity).
EmailPublisherConnectionStringAzure Service Bus namespace connection string.
EmailPublisherTopicNameService Bus topic name for outbound email messages.
GalleryAccountUrlURL inserted into email bodies pointing users to their API key management page.
GalleryBrandBrand name string (e.g., "NuGet") interpolated into email subjects and bodies.
WarnDaysBeforeExpirationNumber of days ahead of expiry to begin sending warnings.
WhatIfWhen true, suppresses actual email dispatch (safe for non-production).