Skip to content

Commit

Permalink
Merge pull request #1027 from colinin/open-api-validation
Browse files Browse the repository at this point in the history
refactor(open-api): get api key from the request header
  • Loading branch information
colinin authored Oct 25, 2024
2 parents 81638c0 + df3355b commit db22a50
Show file tree
Hide file tree
Showing 18 changed files with 216 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Volo.Abp;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.AspNetCore.WebClientInfo;
using Volo.Abp.Clients;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Json;
Expand All @@ -24,23 +23,23 @@ public class OpenApiAuthorizationService : IOpenApiAuthorizationService, ITransi
{
private readonly IAppKeyStore _appKeyStore;
private readonly AbpOpenApiOptions _openApiOptions;
private readonly ICurrentClient _currentClient;
private readonly IWebClientInfoProvider _clientInfoProvider;
private readonly INonceStore _nonceStore;
private readonly IClientChecker _clientChecker;
private readonly IIpAddressChecker _ipAddressChecker;
private readonly AbpExceptionHandlingOptions _exceptionHandlingOptions;

public OpenApiAuthorizationService(
INonceStore nonceStore,
IAppKeyStore appKeyStore,
ICurrentClient currentClient,
IClientChecker clientChecker,
IIpAddressChecker ipAddressChecker,
IWebClientInfoProvider clientInfoProvider,
IOptionsMonitor<AbpOpenApiOptions> options,
IOptions<AbpExceptionHandlingOptions> exceptionHandlingOptions)
{
_nonceStore = nonceStore;
_appKeyStore = appKeyStore;
_currentClient = currentClient;
_clientChecker = clientChecker;
_ipAddressChecker = ipAddressChecker;
_clientInfoProvider = clientInfoProvider;
Expand All @@ -55,7 +54,7 @@ public async virtual Task<bool> AuthorizeAsync(HttpContext httpContext)
return true;
}

if (!await ValidateClient(httpContext))
if (!await ValidateClientIpAddress(httpContext))
{
return false;
}
Expand All @@ -73,18 +72,8 @@ public async virtual Task<bool> AuthorizeAsync(HttpContext httpContext)
return true;
}

protected async virtual Task<bool> ValidateClient(HttpContext httpContext)
protected async virtual Task<bool> ValidateClientIpAddress(HttpContext httpContext)
{
if (_currentClient.IsAuthenticated && !await _clientChecker.IsGrantAsync(_currentClient.Id, httpContext.RequestAborted))
{
var exception = new BusinessException(
AbpOpenApiConsts.InvalidAccessWithClientId,
$"Client Id {_currentClient.Id} Not Allowed",
$"Client Id {_currentClient.Id} Not Allowed");
await Unauthorized(httpContext, exception);
return false;
}

if (!string.IsNullOrWhiteSpace(_clientInfoProvider.ClientIpAddress) &&
!await _ipAddressChecker.IsGrantAsync(_clientInfoProvider.ClientIpAddress, httpContext.RequestAborted))
{
Expand Down Expand Up @@ -114,9 +103,11 @@ protected async virtual Task<bool> ValidateQueryString(HttpContext httpContext)

protected async virtual Task<bool> ValidatAppDescriptor(HttpContext httpContext)
{
httpContext.Request.Query.TryGetValue(AbpOpenApiConsts.AppKeyFieldName, out var appKey);
httpContext.Request.Query.TryGetValue(AbpOpenApiConsts.SignatureFieldName, out var sign);
httpContext.Request.Query.TryGetValue(AbpOpenApiConsts.TimeStampFieldName, out var timeStampString);
httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.AppKeyFieldName, out var appKey);
httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.SignatureFieldName, out var sign);
httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.NonceFieldName, out var nonce);
httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.TimeStampFieldName, out var timeStampString);


if (StringValues.IsNullOrEmpty(appKey))
{
Expand All @@ -128,6 +119,17 @@ protected async virtual Task<bool> ValidatAppDescriptor(HttpContext httpContext)
return false;
}

if (StringValues.IsNullOrEmpty(nonce))
{
var exception = new BusinessException(
AbpOpenApiConsts.InvalidAccessWithNonceNotFound,
$"{AbpOpenApiConsts.NonceFieldName} Not Found",
$"{AbpOpenApiConsts.NonceFieldName} Not Found");

await Unauthorized(httpContext, exception);
return false;
}

if (StringValues.IsNullOrEmpty(sign))
{
var exception = new BusinessException(
Expand Down Expand Up @@ -161,6 +163,26 @@ protected async virtual Task<bool> ValidatAppDescriptor(HttpContext httpContext)
return false;
}

if (!await _nonceStore.TrySetAsync(nonce.ToString(), httpContext.RequestAborted))
{
var exception = new BusinessException(
AbpOpenApiConsts.InvalidAccessWithNonceRepeated,
$"Request {nonce} has repeated",
$"Request {nonce} has repeated");
await Unauthorized(httpContext, exception);
return false;
}

if (!await _clientChecker.IsGrantAsync(appKey.ToString(), httpContext.RequestAborted))
{
var exception = new BusinessException(
AbpOpenApiConsts.InvalidAccessWithClientId,
$"Client Id {appKey} Not Allowed",
$"Client Id {appKey} Not Allowed");
await Unauthorized(httpContext, exception);
return false;
}

var appDescriptor = await _appKeyStore.FindAsync(appKey.ToString(), httpContext.RequestAborted);
if (appDescriptor == null)
{
Expand All @@ -178,13 +200,13 @@ protected async virtual Task<bool> ValidatAppDescriptor(HttpContext httpContext)
var queryStringCollection = httpContext.Request.Query;
foreach (var queryString in queryStringCollection)
{
if (queryString.Key.Equals(AbpOpenApiConsts.SignatureFieldName))
{
continue;
}
queryDictionary.Add(queryString.Key, queryString.Value.ToString());
}
queryDictionary.TryAdd("appKey", appDescriptor.AppKey);
queryDictionary.TryAdd("appSecret", appDescriptor.AppSecret);
queryDictionary.TryAdd("nonce", nonce.ToString());
queryDictionary.TryAdd("t", timeStampString.ToString());

var requiredSign = CalculationSignature(httpContext.Request.Path.Value, queryDictionary);
if (!string.Equals(requiredSign, sign.ToString()))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Nullable>enable</Nullable>
<RootNamespace />
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public IdentityServerAppKeyStore(
Logger = NullLogger<IdentityServerAppKeyStore>.Instance;
}

public async virtual Task<AppDescriptor> FindAsync(string appKey, CancellationToken cancellationToken = default)
public async virtual Task<AppDescriptor?> FindAsync(string appKey, CancellationToken cancellationToken = default)
{
var client = await _clientRepository.FindByClientIdAsync(appKey, cancellationToken: cancellationToken);
if (client != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Nullable>enable</Nullable>
<RootNamespace />
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public OpenIddictAppKeyStore(
_guidGenerator = guidGenerator;
}

public async virtual Task<AppDescriptor> FindAsync(string appKey, CancellationToken cancellationToken = default)
public async virtual Task<AppDescriptor?> FindAsync(string appKey, CancellationToken cancellationToken = default)
{
var application = await _appStore.FindByClientIdAsync(appKey, cancellationToken);
if (application != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Nullable>enable</Nullable>
<RootNamespace />
</PropertyGroup>

Expand All @@ -22,6 +23,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Volo.Abp.Caching" />
<PackageReference Include="Volo.Abp.Security" />
<PackageReference Include="Volo.Abp.Localization" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,54 @@ public static class AbpOpenApiConsts
{
public const string SecurityChecking = "_AbpOpenApiSecurityChecking";

public const string AppKeyFieldName = "appKey";
public const string SignatureFieldName = "sign";
public const string TimeStampFieldName = "t";
public const string AppKeyFieldName = "X-API-APPKEY";
public const string SignatureFieldName = "X-API-SIGN";
public const string NonceFieldName = "X-API-NONCE";
public const string TimeStampFieldName = "X-API-TIMESTAMP";

public const string KeyPrefix = "AbpOpenApi";

/// <summary>
/// 无效的应用标识 {AppKey}.
/// </summary>
public const string InvalidAccessWithAppKey = KeyPrefix + ":9100";
/// <summary>
/// 未携带应用标识(appKey).
/// </summary>
public const string InvalidAccessWithAppKeyNotFound = KeyPrefix + ":9101";

/// <summary>
/// 无效的签名 sign.
/// </summary>
public const string InvalidAccessWithSign = KeyPrefix + ":9110";
/// <summary>
/// 未携带签名(sign).
/// </summary>
public const string InvalidAccessWithSignNotFound = KeyPrefix + ":9111";

/// <summary>
/// 请求超时或会话已过期.
/// </summary>
public const string InvalidAccessWithTimestamp = KeyPrefix + ":9210";
/// <summary>
/// 未携带时间戳标识.
/// </summary>
public const string InvalidAccessWithTimestampNotFound = KeyPrefix + ":9211";

/// <summary>
/// 重复发起的请求.
/// </summary>
public const string InvalidAccessWithNonceRepeated = KeyPrefix + ":9220";
/// <summary>
/// 未携带随机数.
/// </summary>
public const string InvalidAccessWithNonceNotFound = KeyPrefix + ":9221";

/// <summary>
/// 客户端不在允许的范围内.
/// </summary>
public const string InvalidAccessWithClientId = KeyPrefix + ":9300";
/// <summary>
/// 客户端IP不在允许的范围内.
/// </summary>
public const string InvalidAccessWithIpAddress = KeyPrefix + ":9400";
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using LINGYUN.Abp.OpenApi.ConfigurationStore;
using LINGYUN.Abp.OpenApi.Localization;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Caching;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity;
Expand All @@ -10,6 +11,7 @@
namespace LINGYUN.Abp.OpenApi;

[DependsOn(
typeof(AbpCachingModule),
typeof(AbpSecurityModule),
typeof(AbpLocalizationModule))]
public class AbpOpenApiModule : AbpModule
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
namespace LINGYUN.Abp.OpenApi;
using System;

namespace LINGYUN.Abp.OpenApi;

public class AbpOpenApiOptions
{
/// <summary>
/// 启用Api签名检查
/// </summary>
/// <remarks>
/// 默认: true
/// </remarks>
public bool IsEnabled { get; set; }
/// <summary>
/// 请求随机数过期时间
/// </summary>
/// <remarks>
/// 默认: 10分钟
/// </remarks>
public TimeSpan RequestNonceExpireIn { get; set; }

public AbpOpenApiOptions()
{
IsEnabled = true;

RequestNonceExpireIn = TimeSpan.FromMinutes(10);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ public DefaultAppKeyStore(IOptionsMonitor<AbpDefaultAppKeyStoreOptions> options)
_options = options.CurrentValue;
}

public Task<AppDescriptor> FindAsync(string appKey, CancellationToken cancellationToken = default)
public Task<AppDescriptor?> FindAsync(string appKey, CancellationToken cancellationToken = default)
{
return Task.FromResult(Find(appKey));
}

public AppDescriptor Find(string appKey)
public AppDescriptor? Find(string appKey)
{
return _options.AppDescriptors?.FirstOrDefault(t => t.AppKey == appKey);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;

namespace LINGYUN.Abp.OpenApi;
public class DefaultNonceStore : INonceStore, ITransientDependency
{
private const string CacheKeyFormat = "open-api,n:{0}";

private readonly IDistributedCache<NonceStateCacheItem> _nonceCache;
private readonly AbpOpenApiOptions _options;

public DefaultNonceStore(
IDistributedCache<NonceStateCacheItem> nonceCache,
IOptions<AbpOpenApiOptions> options)
{
_nonceCache = nonceCache;
_options = options.Value;
}

public async virtual Task<bool> TrySetAsync(string nonce, CancellationToken cancellationToken = default)
{
var cacheKey = string.Format(CacheKeyFormat, nonce);

var cacheItem = await _nonceCache.GetAsync(cacheKey, token: cancellationToken);
if (cacheItem == null)
{
await _nonceCache.SetAsync(
cacheKey,
new NonceStateCacheItem(nonce),
options: new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _options.RequestNonceExpireIn,
},
token: cancellationToken);

return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace LINGYUN.Abp.OpenApi;

public interface IAppKeyStore
{
Task<AppDescriptor> FindAsync(string appKey, CancellationToken cancellationToken = default);
Task<AppDescriptor?> FindAsync(string appKey, CancellationToken cancellationToken = default);

Task StoreAsync(AppDescriptor descriptor, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading;
using System.Threading.Tasks;

namespace LINGYUN.Abp.OpenApi;
public interface INonceStore
{
Task<bool> TrySetAsync(string nonce, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"AbpOpenApi:9111": "sign not found.",
"AbpOpenApi:9210": "Request timed out or the session expired.",
"AbpOpenApi:9211": "timestamp not found.",
"AbpOpenApi:9220": "Repeatedly initiated requests.",
"AbpOpenApi:9221": "nonce not found.",
"AbpOpenApi:9300": "The client is not within the allowed range.",
"AbpOpenApi:9400": "The client IP is not within the allowed range."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"AbpOpenApi:9111": "未携带签名(sign).",
"AbpOpenApi:9210": "请求超时或会话已过期.",
"AbpOpenApi:9211": "未携带时间戳标识.",
"AbpOpenApi:9220": "重复发起的请求.",
"AbpOpenApi:9221": "未携带随机数.",
"AbpOpenApi:9300": "客户端不在允许的范围内.",
"AbpOpenApi:9400": "客户端IP不在允许的范围内."
}
Expand Down
Loading

0 comments on commit db22a50

Please sign in to comment.