Dear Support Team,
We understand that API Key support has been discussed previously and is not currently a priority for ASP.NET Zero. However, due to client demand, we have developed a potential solution to add API Key functionality and would appreciate your feedback on its security and performance implications.
Background
We reviewed related discussions, including:
- GitHub issue: https://github.com/aspnetzero/aspnet-zero-core/issues/1728
- Support question: https://support.aspnetzero.com/QA/Questions/11251/Create-static-access-token-valid-for-365-days
- OpenIddict documentation
Based on this, we implemented a potential solution that aligns with ASP.NET Zero’s existing authentication and authorization mechanisms.
Our Proposed Solution
- API Tokens as JWTs: API Tokens are implemented as JWTs, leveraging the same mechanism as the
/api/TokenAuth/Authenticateendpoint for access and refresh tokens. - Customizable Expiry: Users can specify an expiry longer than 24 hours during token creation.
- Permission Scoping: Tokens can be assigned specific permissions, restricted to those already granted to the user. If a token’s permissions exceed the user’s at the time of use, access is denied to minimize security risks. Permissions are encoded as additional claims.
- Authorization Header: API Tokens use the
ApiKeyAuthorization header to distinguish them fromBearertokens. - Token Tracking: Tokens are tracked in the
AbpUserTokenstable (like access/refresh tokens) and a newApiTokenstable for additional metadata. Tokens themselves are never stored in the database. - Token Validation: Tokens are validated identically as access and refresh token, utilizing TokenValidity claim
- Token Revocation: Revoking a token removes its records from cache and from both
AbpUserTokensandApiTokenstables. - Permission Validation: We extended
PermissionChecker.csto validate API Key permissions, as shown below:
public override async Task<bool> IsGrantedAsync(long userId, string permissionName)
{
Logger.Info($"PermissionChecker.IsGrantedAsync called - UserId: {userId}, Permission: {permissionName}");
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext?.User?.Identity?.IsAuthenticated == true)
{
var tokenTypeClaim = httpContext.User.FindFirst("TokenType");
Logger.Info($"PermissionChecker - TokenType claim: {tokenTypeClaim?.Value}");
if (tokenTypeClaim?.Value == "ApiKey")
{
Logger.Info("PermissionChecker - API Key authentication detected, checking API key permissions");
// Check if the user has the permission
var userHasPermission = await base.IsGrantedAsync(userId, permissionName);
Logger.Info($"PermissionChecker - User permission check result: {userHasPermission} for permission: {permissionName}");
if (!userHasPermission)
{
Logger.Info($"PermissionChecker - User {userId} does not have permission {permissionName}, denying API key access");
return false;
}
// Check API Key permissions
var apiKeyPermissionsClaim = httpContext.User.FindFirst("ApiKeyPermissions");
if (apiKeyPermissionsClaim != null && !string.IsNullOrEmpty(apiKeyPermissionsClaim.Value))
{
try
{
var apiKeyPermissions = JsonConvert.DeserializeObject<List<string>>(apiKeyPermissionsClaim.Value);
if (apiKeyPermissions.Contains(DenvrDashboardConsts.ApiKey.AllUserPermissions))
{
Logger.Info($"PermissionChecker - API key has 'AllUserPermissions', granting access to permission: {permissionName}");
return true;
}
bool apiKeyHasPermission = apiKeyPermissions.Contains(permissionName);
Logger.Info($"PermissionChecker - API key permission check result: {apiKeyHasPermission} for permission: {permissionName}");
return apiKeyHasPermission;
}
catch (JsonException ex)
{
Logger.Error($"PermissionChecker - Error deserializing API key permissions: {ex.Message}");
return false;
}
}
else
{
Logger.Warn("PermissionChecker - No ApiKeyPermissions claim found for API key authentication");
return false;
}
}
}
Logger.Info("PermissionChecker - Using default permission checking logic");
return await base.IsGrantedAsync(userId, permissionName);
}
- Middleware: We added
ApiAuthenticationMiddleware.csafterJwtTokenMiddlewareinStartup.csto validate API Keys and extract claims. It only processes requests with theApiKeyheader, minimizing performance impact:
app.UseAuthentication();
app.UseJwtTokenMiddleware();
app.UseMiddleware<ApiKeyAuthenticationMiddleware>();
Questions for Support
We believe this solution aligns with ASP.NET Zero’s architecture and addresses client needs for API Key support. We’d appreciate your feedback on:
- Potential security risks, particularly around token revocation or JWT payload handling.
- Performance implications of the middleware and permission validation logic.
- Any suggestions for improving compatibility with ASP.NET Zero’s ecosystem.
If needed, we can share a GitHub repository or detailed documentation for further review. Please let us know how you’d like to proceed (e.g., via a pull request or further discussion).
Best regards,
Peja
2 Answer(s)
-
0
Hello @kylem
Thank you for sharing your detailed solution and explanation. While API Key support is not currently a priority in ASP.NET Zero’s roadmap, you’ve developed a well thought out approach to meet your needs.
Your implementation appears to align well with our existing architecture, particularly in the following aspects
JWT based token mechanism reuse Reusing the
/api/TokenAuth/Authenticateprocess is a positive choice in terms of compatibility with the existing security structure.Permission scoping Ensuring that an API Key cannot exceed the user’s existing permissions is critical and the right decision from a security perspective.
Token revocation and cache clearing Removing the token from both the database and the cache helps minimize the risk of unauthorized usage.
Middleware order Placing your
ApiKeyAuthenticationMiddlewareafterJwtTokenMiddlewarehelps reduce unnecessary processing for performance reasons.Our feedback and recommendations:
Token Revocation Revocation is always challenging with JWT since tokens are stateless. You’ve addressed this using the
AbpUserTokens+ApiTokenstables and cache, but you should pay close attention to cache lifetime and ensuring expired tokens are not revalidated. We recommend validating theTokenValidityclaim both in middleware and at thePermissionCheckerlevel.Token Claims Validation Since you store
ApiKeyPermissionsas JSON in claims, parsing errors could affect performance and security. Therefore:- Add JSON schema validation when setting the claim.
- Perform deserialization outside of a try catch block by first validating the string.
Performance Impact Since the additional middleware only runs when the
ApiKeyheader is present, its impact on overall traffic should be minimal. However, during permission checks, you validate both the user’s and the API Key’s permissions. For scenarios with a large number of permissions, we recommend using aHashSet<string>to optimize lookup performance.Standards Alignment To ensure easier integration if ASP.NET Zero adds native API Key support in the future.
- Keep the middleware modular.
- Manage claim names through constants.
- Consider adding an option to support non user bound (system level) API Keys.
If you have any other questions or would like more information about the status of the feature you’ve added, please don’t hesitate to contact us.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Dear Support Team,
Thank you for your quick response. We greatly appreciate your feedback and the recommended improvements, as well as the points to pay attention to. We will review these suggestions carefully and prioritize their implementation to ensure our solution remains secure and efficient.
Regarding your suggestion for non-user-bound (system-level) API keys, we currently recommend that our clients create a system user as a regular user with two-factor authentication disabled, as this aligns with our existing architecture and simplifies integration. We plan to maintain this approach in the short term but will monitor your release notes for updates on native API Key support and how this functionality is addressed in future ASP.NET Zero releases.
Feel free to close the ticket and thanks again!
Best regards, Peja
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)