docs/en/Community-Articles/2025-11-15-building-an-api-key-management-system/post.md
API keys are one of the most common authentication methods for APIs, especially for machine-to-machine communication. In this article, I'll explain what API key authentication is, when to use it, and how to implement a complete API key management system using ABP Framework.
An API key is a unique identifier used to authenticate requests to an API. Unlike user credentials (username/password) or OAuth tokens, API keys are designed for:
While modern authentication methods like OAuth2 and JWT are excellent for user authentication, API keys offer distinct advantages in certain scenarios:
Simplicity: No complex OAuth flows or token refresh mechanisms. Just include the key in your request header.
Long-lived: Unlike JWT tokens that expire in minutes/hours, API keys can remain valid for months or years, making them ideal for automated systems.
Revocable: You can instantly revoke a compromised key without affecting user credentials.
Granular Control: Different keys for different purposes (read-only, admin, specific services).
Here are some practical scenarios where API key authentication shines:
Your mobile app needs to call your backend APIs. Instead of storing user credentials or managing token refresh flows, use an API key.
// Mobile app configuration
var apiClient = new ApiClient("https://api.yourapp.com");
apiClient.SetApiKey("sk_mobile_prod_abc123...");
Service A needs to call Service B's protected endpoints.
// Order Service calling Inventory Service
var request = new HttpRequestMessage(HttpMethod.Get, "https://inventory-service/api/products");
request.Headers.Add("X-Api-Key", _configuration["InventoryService:ApiKey"]);
You're providing APIs to external partners or customers.
# Customer's integration script
curl -H "X-Api-Key: pk_partner_xyz789..." \
https://api.yourplatform.com/api/orders
Now let's see how to build a complete API key management system using ABP Framework. I've created an open-source implementation that you can use in your projects.
The implementation consists of:
The solution follows ABP's modular architecture with four main layers:
┌─────────────────────────────────────────────┐
│ Web Layer (UI) │
│ • Razor Pages for CRUD operations │
│ • JavaScript for client interactions │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ AspNetCore Layer (Middleware) │
│ • Authentication Handler │
│ • API Key Resolver (Header/Query) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Application Layer (Business Logic) │
│ • ApiKeyAppService (CRUD operations) │
│ • DTO mappings and validations │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Domain Layer (Core Business) │
│ • ApiKey Entity & Manager │
│ • IApiKeyRepository │
│ • Domain services & events │
└─────────────────────────────────────────────┘
public class ApiKey : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; }
public virtual Guid UserId { get; protected set; }
public virtual string Name { get; protected set; }
public virtual string Prefix { get; protected set; }
public virtual string KeyHash { get; protected set; }
public virtual DateTime? ExpiresAt { get; protected set; }
public virtual bool IsActive { get; protected set; }
// Key format: {prefix}_{key}
// Only the hash is stored, never the actual key
}
Key Design Decisions:
prefix_actualkey. The prefix is indexed for fast database lookups.Here's how authentication works when a request arrives:
// 1. Extract API key from request
var apiKey = httpContext.Request.Headers["X-Api-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(apiKey)) return AuthenticateResult.NoResult();
// 2. Split prefix and key
var parts = apiKey.Split('_', 2);
var prefix = parts[0];
var key = parts[1];
// 3. Find key by prefix (cached)
var apiKeyEntity = await _apiKeyRepository.FindByPrefixAsync(prefix);
if (apiKeyEntity == null) return AuthenticateResult.Fail("Invalid API key");
// 4. Verify hash
var keyHash = HashHelper.ComputeSha256(key);
if (apiKeyEntity.KeyHash != keyHash)
return AuthenticateResult.Fail("Invalid API key");
// 5. Check expiration and active status
if (apiKeyEntity.ExpiresAt < DateTime.UtcNow || !apiKeyEntity.IsActive)
return AuthenticateResult.Fail("API key expired or inactive");
// 6. Create claims principal with user identity
var claims = new List<Claim>
{
new Claim(AbpClaimTypes.UserId, apiKeyEntity.UserId.ToString()),
new Claim(AbpClaimTypes.TenantId, apiKeyEntity.TenantId?.ToString() ?? ""),
new Claim("ApiKeyId", apiKeyEntity.Id.ToString())
};
return AuthenticateResult.Success(ticket);
Creating a new key:
public class ApiKeyManager : DomainService
{
public async Task<(ApiKey, string)> CreateAsync(
Guid userId,
string name,
DateTime? expiresAt = null)
{
// Generate unique prefix
var prefix = await GenerateUniquePrefixAsync();
// Generate secure random key
var key = GenerateSecureRandomString(32);
// Hash the key for storage
var keyHash = HashHelper.ComputeSha256(key);
var apiKey = new ApiKey(
GuidGenerator.Create(),
userId,
name,
prefix,
keyHash,
expiresAt,
CurrentTenant.Id
);
await _apiKeyRepository.InsertAsync(apiKey);
// Return both entity and the full key (prefix_key)
// This is the ONLY time the actual key is visible
return (apiKey, $"{prefix}_{key}");
}
}
Important: The actual key is returned only once during creation. After that, only the hash is stored.
Once created, clients can use the API key to authenticate:
HTTP Header (Recommended):
curl -H "X-Api-Key: sk_prod_abc123def456..." \
https://api.example.com/api/products
JavaScript:
const response = await fetch('https://api.example.com/api/products', {
headers: {
'X-Api-Key': 'sk_prod_abc123def456...'
}
});
C# HttpClient:
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "sk_prod_abc123def456...");
var response = await client.GetAsync("https://api.example.com/api/products");
Python:
import requests
headers = {'X-Api-Key': 'sk_prod_abc123def456...'}
response = requests.get('https://api.example.com/api/products', headers=headers)
API keys inherit the user's permissions, but you can further restrict them:
This allows scenarios like:
// Check if current request is authenticated via API key
if (CurrentUser.FindClaim("ApiKeyId") != null)
{
var apiKeyId = CurrentUser.FindClaim("ApiKeyId").Value;
// Additional API key specific logic
}
The implementation uses several optimizations:
1. Prefix-based indexing: Database lookups are done by prefix (indexed column), not the full key hash.
2. Distributed caching: API keys are cached after first lookup, dramatically reducing database queries.
// Cache configuration
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "ApiKey:";
});
3. Cache invalidation: When a key is modified or deleted, cache is automatically invalidated.
Typical Performance:
When implementing API key authentication, follow these guidelines:
✅ Always use HTTPS - Never send API keys over unencrypted connections
✅ Use different keys per environment - Separate keys for dev, staging, production
❌ Don't log the full key - Only log the prefix for debugging
The complete source code is available on GitHub:
Repository: github.com/salihozkara/AbpApikeyManagement
To integrate it into your ABP project:
/ApiKeyManagement to start managing keys// In your Web module
[DependsOn(typeof(ApiKeyManagementWebModule))]
public class YourWebModule : AbpModule
{
// ...
}
// In your HttpApi.Host module
[DependsOn(typeof(ApiKeyManagementHttpApiModule))]
public class YourHttpApiHostModule : AbpModule
{
// ...
}
API key authentication remains a crucial part of modern API security, especially for machine-to-machine communication. While it shouldn't replace user authentication methods like OAuth2 for user-facing applications, it's perfect for:
The implementation shown here demonstrates how ABP Framework's modular architecture, DDD principles, and built-in features (multi-tenancy, caching, permissions) can be leveraged to build a production-ready API key management system.
The solution is open-source and ready to be integrated into your ABP projects. Feel free to explore the code, suggest improvements, or adapt it to your specific needs.
Resources:
Happy coding! 🚀