482 lines
15 KiB
C#
482 lines
15 KiB
C#
using System;
|
|
using System.Linq;
|
|
using xIds.Helpers;
|
|
using System.Threading;
|
|
using xCommons.Extensions;
|
|
using xIdentityModels.Dtos;
|
|
using xExceptions.Constants;
|
|
using System.Threading.Tasks;
|
|
using xIdentityModels.Models;
|
|
using System.Collections.Generic;
|
|
using xIdentityModels.Extensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace xIds.Providers
|
|
{
|
|
public partial class XApplicationProvider
|
|
{
|
|
//
|
|
#region Custom Actions ...
|
|
/// <summary>
|
|
/// Generate a New ApiKey and Add It to DB ...
|
|
/// </summary>
|
|
/// <param name="applicationId"></param>
|
|
/// <param name="expiration"></param>
|
|
/// <param name="scopes"></param>
|
|
/// <param name="allowedIPs"></param>
|
|
/// <param name="rateLimit"></param>
|
|
/// <param name="userInfo"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
public async Task<XApiKeyDto> CreateApiKey(
|
|
int applicationId,
|
|
TimeSpan? expiration = null,
|
|
IEnumerable<string> scopes = null,
|
|
IEnumerable<string> allowedIPs = null,
|
|
int? rateLimit = null,
|
|
XUserClaimsInfoDto userInfo = null,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (
|
|
userInfo.IsNullOrDefault() ||
|
|
!applicationId.IsValidIntId()
|
|
)
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
// Check Application Exists ...
|
|
var application = await GetApplicationAsync(
|
|
id: applicationId,
|
|
userInfo: userInfo,
|
|
includeBuilder: null,
|
|
ignoreSoftDeleteds: true,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
if (application.IsNullOrDefault())
|
|
{
|
|
XException.NotFound.Throw();
|
|
}
|
|
|
|
//
|
|
// Validate Owner ...
|
|
if (!IsOwned(application, userInfo))
|
|
{
|
|
XException.NotAllowed.Throw();
|
|
}
|
|
|
|
//
|
|
// Normalizing ...
|
|
if (!rateLimit.HasValue)
|
|
{
|
|
rateLimit = apiKeyConfiguration.DefaultRateLimit;
|
|
}
|
|
if (!expiration.HasValue)
|
|
{
|
|
expiration = TimeSpan.FromMinutes(apiKeyConfiguration.DefaultExpirationMinutes);
|
|
}
|
|
else if (expiration.Value > TimeSpan.FromMinutes(apiKeyConfiguration.MaxExpirationMinutes))
|
|
{
|
|
expiration = TimeSpan.FromMinutes(apiKeyConfiguration.MaxExpirationMinutes);
|
|
}
|
|
|
|
//
|
|
// Generate New Key ...
|
|
(string plainKey, string keyHash, string prefix) = XApiKeyHelper.Generate(apiKeyConfiguration.ApiKeyPrefix);
|
|
var expireAt = DateTime.UtcNow.Add(expiration.Value);
|
|
|
|
//
|
|
var _allowedScopes = scopes.ToJSON();
|
|
var _allowedIPs = allowedIPs.ToListString();
|
|
|
|
//
|
|
// Instance Dto Item ...
|
|
var result = new XApiKeyDto
|
|
{
|
|
RevokedAt = null,
|
|
KeyHash = keyHash,
|
|
LastUsedAt = null,
|
|
KeyPrefix = prefix,
|
|
ExpiresAt = expireAt,
|
|
RevokedBy = string.Empty,
|
|
AllowedIPs = _allowedIPs,
|
|
CreatedOn = DateTime.UtcNow,
|
|
ApplicationId = applicationId,
|
|
AllowedScopes = _allowedScopes,
|
|
RateLimitPerMinute = rateLimit.Value
|
|
};
|
|
|
|
//
|
|
// Add Item to Database ...
|
|
result = await AddApiKeyAsync(
|
|
item: result,
|
|
userInfo: userInfo,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Revoke Specified Api Key ...
|
|
/// </summary>
|
|
/// <param name="apiKeyId"></param>
|
|
/// <param name="userInfo"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
public async Task<bool> RevokeApiKey(
|
|
Guid apiKeyId,
|
|
XUserClaimsInfoDto userInfo = null,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (
|
|
apiKeyId.IsDefaultGuid() ||
|
|
userInfo.IsNullOrDefault())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
// Retrieve ApiKey ...
|
|
var item = await GetApiKeyAsync(
|
|
id: apiKeyId,
|
|
userInfo: userInfo,
|
|
includeBuilder: null,
|
|
ignoreSoftDeleteds: true,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
if (item.IsNullOrDefault())
|
|
{
|
|
XException.NotFound.Throw();
|
|
}
|
|
|
|
//
|
|
// Permission Checking ...
|
|
var application = await GetApplicationAsync(
|
|
userInfo: userInfo,
|
|
includeBuilder: null,
|
|
id: item.ApplicationId,
|
|
ignoreSoftDeleteds: true,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
if (application.IsNullOrDefault())
|
|
{
|
|
XException.NotFound.Throw();
|
|
}
|
|
if (!IsOwned(application, userInfo))
|
|
{
|
|
XException.NotAllowed.Throw();
|
|
}
|
|
|
|
//
|
|
// Update Revoke Info ...
|
|
item.RevokedAt = DateTime.UtcNow;
|
|
item.RevokedBy = userInfo.UserId;
|
|
|
|
//
|
|
item = await UpdateApiKeyAsync(
|
|
id: item.Id,
|
|
item: item,
|
|
userInfo: userInfo,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
var result = !item.IsNullOrDefault();
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renew Specified Api Key ...
|
|
/// </summary>
|
|
/// <param name="apiKeyId"></param>
|
|
/// <param name="newExpiration"></param>
|
|
/// <param name="userInfo"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
public async Task<XApiKeyDto> RotateApiKey(
|
|
Guid apiKeyId,
|
|
TimeSpan? newExpiration = null,
|
|
XUserClaimsInfoDto userInfo = null,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (
|
|
apiKeyId.IsDefaultGuid() ||
|
|
userInfo.IsNullOrDefault()
|
|
)
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
// Retrieve Item ...
|
|
var result = await GetApiKeyAsync(
|
|
id: apiKeyId,
|
|
userInfo: userInfo,
|
|
includeBuilder: null,
|
|
ignoreSoftDeleteds: true,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
if (result.IsNullOrDefault())
|
|
{
|
|
XException.NotFound.Throw();
|
|
}
|
|
|
|
//
|
|
// Expired Date ...
|
|
DateTime? expiredDate = null;
|
|
if (!newExpiration.HasValue)
|
|
{
|
|
//
|
|
newExpiration = TimeSpan
|
|
.FromMinutes(apiKeyConfiguration.DefaultExpirationMinutes);
|
|
expiredDate = DateTime.UtcNow.Add(newExpiration.Value);
|
|
}
|
|
if (!expiredDate.HasValue)
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
// Update Item ...
|
|
result.ExpiresAt = expiredDate.Value;
|
|
|
|
//
|
|
result = await UpdateApiKeyAsync(
|
|
item: result,
|
|
id: result.Id,
|
|
userInfo: userInfo,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve All Api Keys for Specified Application ...
|
|
/// </summary>
|
|
/// <param name="applicationId"></param>
|
|
/// <param name="userInfo"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
public async Task<IEnumerable<XApiKeyDto>> GetApplicationApiKeys(
|
|
int applicationId,
|
|
XUserClaimsInfoDto userInfo = null,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (
|
|
userInfo.IsNullOrDefault() ||
|
|
!applicationId.IsValidIntId()
|
|
)
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
// Permission Checking ...
|
|
var application = await GetApplicationAsync(
|
|
userInfo: userInfo,
|
|
includeBuilder: null,
|
|
id: applicationId,
|
|
ignoreSoftDeleteds: true,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
if (application.IsNullOrDefault())
|
|
{
|
|
XException.NotFound.Throw();
|
|
}
|
|
if (!IsOwned(application, userInfo))
|
|
{
|
|
XException.NotAllowed.Throw();
|
|
}
|
|
|
|
//
|
|
var result = await FindManyApiKeyAsync(
|
|
userInfo: userInfo,
|
|
includeBuilder: null,
|
|
ignoreSoftDeleteds: true,
|
|
cancellationToken: cancellationToken,
|
|
orderBuilder: x => x.OrderBy(y => y.Id),
|
|
predicate: x => x.ApplicationId == applicationId
|
|
);
|
|
|
|
//
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate Specified Api Key in based on ClientIP ...
|
|
/// </summary>
|
|
/// <param name="apiKey"></param>
|
|
/// <param name="clientIP"></param>
|
|
/// <param name="requiredScope"></param>
|
|
/// <param name="userInfo"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
public async Task<XApiKeyValidationResult> ValidateApiKey(
|
|
string apiKey,
|
|
string clientIP,
|
|
string requiredScope = null,
|
|
XUserClaimsInfoDto userInfo = null,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
var result = new XApiKeyValidationResult();
|
|
|
|
//
|
|
// Validate ...
|
|
var isValid =
|
|
!apiKey.IsNullOrEmpty() &&
|
|
!clientIP.IsNullOrEmpty();
|
|
if (!isValid)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
//
|
|
// Detect Specified Api Key ...
|
|
var apiKeyHash = XApiKeyHelper.ComputeHash(apiKey);
|
|
var keyModel = await dbContext.ApiKeys
|
|
.FirstOrDefaultAsync(
|
|
ak => ak.KeyHash == apiKeyHash,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
isValid = !keyModel.IsNullOrDefault();
|
|
if (!isValid)
|
|
{
|
|
//
|
|
result.Errors.Add("ApiKey not found ...");
|
|
return result;
|
|
}
|
|
|
|
//
|
|
// Here we Have to Parse Allowed Ips and Scopes ...
|
|
var allowedIps = (keyModel.AllowedIPs ?? string.Empty)
|
|
.ParseListString<string>();
|
|
var allowedScopes = (keyModel.AllowedScopes ?? string.Empty)
|
|
.FromJSON<IEnumerable<string>>();
|
|
result.Scopes = [..allowedScopes];
|
|
|
|
//
|
|
// Check Application is Exists and Active ...
|
|
var application = await dbContext.Applications
|
|
.FirstOrDefaultAsync(x => x.Id == keyModel.ApplicationId);
|
|
if (!application.IsNullOrDefault())
|
|
{
|
|
result.OwnerId = application.OwnerId;
|
|
result.ApplicationId = application.Id;
|
|
}
|
|
|
|
//
|
|
// Check Application is Exists and Active or not ...
|
|
var isApplicationActive =
|
|
//
|
|
// Application Exists ...
|
|
!application.IsNullOrDefault() &&
|
|
//
|
|
// Check Application is Active ...
|
|
application.IsActive
|
|
//
|
|
;
|
|
if (!isApplicationActive)
|
|
{
|
|
result.Errors.Add("Related Application is Inactive ...");
|
|
}
|
|
|
|
//
|
|
// Check Key is Active or not ...
|
|
var isKeyActive =
|
|
//
|
|
// Check Api Key is Active ...
|
|
// Not Revoked ...
|
|
// Not Expired ...
|
|
keyModel.IsActive()
|
|
//
|
|
;
|
|
if (!isKeyActive)
|
|
{
|
|
//
|
|
if (keyModel.IsExpired())
|
|
{
|
|
result.Errors.Add("ApiKey is Expired ...");
|
|
}
|
|
|
|
//
|
|
if (keyModel.IsRevoked())
|
|
{
|
|
result.Errors.Add("ApiKey is Revoked ...");
|
|
}
|
|
}
|
|
|
|
//
|
|
// Checking Rate Limit ...
|
|
var isRateLimitPassed = CheckRateLimit(keyModel.Id, keyModel.RateLimitPerMinute);
|
|
if (!isRateLimitPassed)
|
|
{
|
|
result.Errors.Add("ApiKey Rate Limit Reached ...");
|
|
}
|
|
|
|
//
|
|
// Audit Log ...
|
|
|
|
//
|
|
// Check Scope and IP is Valid or not ...
|
|
var isIpValid =
|
|
!allowedIps.HasChild() ||
|
|
allowedIps.Contains(clientIP);
|
|
var isScopeValid =
|
|
!allowedScopes.HasChild() ||
|
|
requiredScope.IsNullOrEmpty() ||
|
|
allowedScopes.Contains(requiredScope);
|
|
var isScopeIpValid =
|
|
isIpValid &&
|
|
isScopeValid
|
|
;
|
|
if (!isScopeIpValid)
|
|
{
|
|
//
|
|
if (!isIpValid)
|
|
{
|
|
result.Errors.Add("Client IP not Allowed to Use Resource ...");
|
|
}
|
|
|
|
//
|
|
if (!isScopeValid)
|
|
{
|
|
result.Errors.Add("Required Scoped is Invalid ...");
|
|
}
|
|
}
|
|
|
|
//
|
|
isValid =
|
|
isKeyActive &&
|
|
isScopeIpValid &&
|
|
isRateLimitPassed &&
|
|
isApplicationActive;
|
|
if (isValid)
|
|
{
|
|
result.Errors.Clear();
|
|
}
|
|
|
|
//
|
|
return result;
|
|
}
|
|
#endregion
|
|
}
|
|
} |