The Data Protection API is one of ASP.NET Core's most underappreciated features. It powers cookie encryption, anti-forgery tokens, and TempData behind the scenes. But it's also a first-class API you can use directly for encrypting and decrypting arbitrary data — without needing to manage cryptographic primitives yourself.

How It Works

The Data Protection API uses authenticated encryption (AES-256-CBC with HMACSHA256 by default). It manages key generation, rotation, and retirement automatically. Each key has a 90-day activation period, and old keys are kept around for decryption.

Basic Usage

Inject IDataProtectionProvider and create a protector with a purpose string:

SecureTokenService.cs
public class SecureTokenService
{
    private readonly IDataProtector _protector;

    public SecureTokenService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("SecureTokenService.v1");
    }

    public string Protect(string plainText)
    {
        return _protector.Protect(plainText);
    }

    public string? Unprotect(string protectedText)
    {
        try
        {
            return _protector.Unprotect(protectedText);
        }
        catch (CryptographicException)
        {
            return null;
        }
    }
}

The purpose string is critical. It creates cryptographic isolation between different parts of your application. A protector created with purpose "Tokens" cannot decrypt data protected with purpose "Cookies". This prevents one component from accidentally (or maliciously) reading another's data.

Time-Limited Protection

For tokens that should expire — password reset links, email confirmation tokens — use ITimeLimitedDataProtector:

PasswordResetService.cs
public class PasswordResetService
{
    private readonly ITimeLimitedDataProtector _protector;

    public PasswordResetService(IDataProtectionProvider provider)
    {
        _protector = provider
            .CreateProtector("PasswordReset.v1")
            .ToTimeLimitedDataProtector();
    }

    public string GenerateResetToken(string userId)
    {
        return _protector.Protect(userId, lifetime: TimeSpan.FromHours(1));
    }

    public string? ValidateResetToken(string token)
    {
        try
        {
            return _protector.Unprotect(token);
        }
        catch (CryptographicException)
        {
            // Token is invalid or expired
            return null;
        }
    }
}

After the specified lifetime, Unprotect throws a CryptographicException. No need to store expiry times in a database or parse timestamps from the token yourself.

Configuring Key Storage for Production

By default, keys are stored in the local file system and are specific to the machine. This falls apart immediately in multi-server deployments — a token encrypted on server A can't be decrypted on server B.

For production, persist keys to a shared location:

Program.cs
builder.Services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(new Uri(
        builder.Configuration["DataProtection:BlobUri"]!))
    .ProtectKeysWithAzureKeyVault(
        new Uri(builder.Configuration["DataProtection:KeyVaultKeyUri"]!),
        new DefaultAzureCredential())
    .SetApplicationName("MyApp");

SetApplicationName is essential when multiple applications need to share protected data. Without it, each application gets its own isolated key ring even if they share the same storage.

Other storage options include:

Program.cs
// Redis
builder.Services.AddDataProtection()
    .PersistKeysToStackExchangeRedis(
        ConnectionMultiplexer.Connect(redisConnection),
        "DataProtection-Keys");

// Entity Framework Core
builder.Services.AddDataProtection()
    .PersistKeysToDbContext<DataProtectionDbContext>();

// File share
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys"));

Key Rotation and Revocation

The API handles key rotation automatically. New keys are generated before old ones expire, and old keys remain available for decryption. If you need to revoke a key immediately (perhaps it's been compromised), you can do so:

Program.cs
app.MapPost("/admin/revoke-keys", (IKeyManager keyManager) =>
{
    keyManager.RevokeAllKeys(
        DateTimeOffset.UtcNow,
        reason: "Security incident - rotating all keys");

    return Results.Ok("All keys revoked");
});

Be aware that revoking keys invalidates all data protected with those keys. Every active session cookie, every pending reset token — gone. This is the nuclear option.

Common Mistakes

Using Data Protection for long-term storage. Keys are rotated and eventually retired. If you encrypt data today and try to decrypt it two years from now, the key may no longer exist. For long-term encryption, use a dedicated key management solution.

Ignoring purpose string versioning. If you change the purpose string, previously protected data becomes unreadable. Version your purpose strings (e.g., "Tokens.v1") so you can migrate gracefully.

Not configuring key storage in containers. Container file systems are ephemeral. Without external key storage, every deployment generates new keys, invalidating all existing protected data.

Wrapping Up

The Data Protection API gives you production-grade encryption without the footguns of raw cryptography. Configure shared key storage for multi-instance deployments, use purpose strings for isolation, and reach for ITimeLimitedDataProtector when you need tokens that expire. It's one of those APIs where the defaults are sensible and the configuration surface is just right.