diff --git a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj
index d1283634918..b96da9be3e0 100644
--- a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj
+++ b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj
@@ -17,6 +17,7 @@
+
diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs
index 7ab97c49fe5..bf499e7b5f6 100644
--- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs
+++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Caching;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
@@ -7,7 +8,8 @@ namespace Volo.Abp.IdentityModel
{
[DependsOn(
typeof(AbpThreadingModule),
- typeof(AbpMultiTenancyModule)
+ typeof(AbpMultiTenancyModule),
+ typeof(AbpCachingModule)
)]
public class AbpIdentityModelModule : AbpModule
{
diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs
index 17040ff1dbe..5f2f4af573a 100644
--- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs
+++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using IdentityModel;
namespace Volo.Abp.IdentityModel
@@ -81,21 +82,32 @@ public bool RequireHttps
get => this.GetOrDefault(nameof(RequireHttps))?.To() ?? true;
set => this[nameof(RequireHttps)] = value.ToString().ToLowerInvariant();
}
-
+
+ ///
+ /// Absolute expiration duration (as seconds) for the access token cache.
+ /// Default: 1800 seconds (30 minutes)
+ ///
+ public int CacheAbsoluteExpiration
+ {
+ get => this.GetOrDefault(nameof(CacheAbsoluteExpiration ))?.To() ?? 60 * 30;
+ set => this[nameof(CacheAbsoluteExpiration)] = value.ToString(CultureInfo.InvariantCulture);
+ }
+
public IdentityClientConfiguration()
{
-
+
}
public IdentityClientConfiguration(
string authority,
string scope,
- string clientId,
- string clientSecret,
+ string clientId,
+ string clientSecret,
string grantType = OidcConstants.GrantTypes.ClientCredentials,
string userName = null,
string userPassword = null,
- bool requireHttps = true)
+ bool requireHttps = true,
+ int cacheAbsoluteExpiration = 60 * 30)
{
this[nameof(Authority)] = authority;
this[nameof(Scope)] = scope;
@@ -105,6 +117,7 @@ public IdentityClientConfiguration(
this[nameof(UserName)] = userName;
this[nameof(UserPassword)] = userPassword;
this[nameof(RequireHttps)] = requireHttps.ToString().ToLowerInvariant();
+ this[nameof(CacheAbsoluteExpiration)] = cacheAbsoluteExpiration.ToString(CultureInfo.InvariantCulture);
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs
index 5e8c67a9abf..0fb4c32648b 100644
--- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs
+++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs
@@ -10,6 +10,8 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Distributed;
+using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
@@ -26,18 +28,24 @@ public class IdentityModelAuthenticationService : IIdentityModelAuthenticationSe
protected IHttpClientFactory HttpClientFactory { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IdentityModelHttpRequestMessageOptions IdentityModelHttpRequestMessageOptions { get; }
+ protected IDistributedCache TokenCache { get; }
+ protected IDistributedCache DiscoveryDocumentCache { get; }
public IdentityModelAuthenticationService(
IOptions options,
ICancellationTokenProvider cancellationTokenProvider,
IHttpClientFactory httpClientFactory,
ICurrentTenant currentTenant,
- IOptions identityModelHttpRequestMessageOptions)
+ IOptions identityModelHttpRequestMessageOptions,
+ IDistributedCache tokenCache,
+ IDistributedCache discoveryDocumentCache)
{
ClientOptions = options.Value;
CancellationTokenProvider = cancellationTokenProvider;
HttpClientFactory = httpClientFactory;
CurrentTenant = currentTenant;
+ TokenCache = tokenCache;
+ DiscoveryDocumentCache = discoveryDocumentCache;
IdentityModelHttpRequestMessageOptions = identityModelHttpRequestMessageOptions.Value;
Logger = NullLogger.Instance;
}
@@ -70,27 +78,34 @@ protected virtual async Task GetAccessTokenOrNullAsync(string identityCl
public virtual async Task GetAccessTokenAsync(IdentityClientConfiguration configuration)
{
- var discoveryResponse = await GetDiscoveryResponse(configuration);
- if (discoveryResponse.IsError)
+ var cacheKey = CalculateTokenCacheKey(configuration);
+ var tokenCacheItem = await TokenCache.GetAsync(cacheKey);
+ if (tokenCacheItem == null)
{
- throw new AbpException($"Could not retrieve the OpenId Connect discovery document! ErrorType: {discoveryResponse.ErrorType}. Error: {discoveryResponse.Error}");
- }
-
- var tokenResponse = await GetTokenResponse(discoveryResponse, configuration);
+ var tokenResponse = await GetTokenResponse(configuration);
- if (tokenResponse.IsError)
- {
- if (tokenResponse.ErrorDescription != null)
+ if (tokenResponse.IsError)
{
- throw new AbpException($"Could not get token from the OpenId Connect server! ErrorType: {tokenResponse.ErrorType}. Error: {tokenResponse.Error}. ErrorDescription: {tokenResponse.ErrorDescription}. HttpStatusCode: {tokenResponse.HttpStatusCode}");
+ if (tokenResponse.ErrorDescription != null)
+ {
+ throw new AbpException($"Could not get token from the OpenId Connect server! ErrorType: {tokenResponse.ErrorType}. " +
+ $"Error: {tokenResponse.Error}. ErrorDescription: {tokenResponse.ErrorDescription}. HttpStatusCode: {tokenResponse.HttpStatusCode}");
+ }
+
+ var rawError = tokenResponse.Raw;
+ var withoutInnerException = rawError.Split(new string[] { "" }, StringSplitOptions.RemoveEmptyEntries);
+ throw new AbpException(withoutInnerException[0]);
}
- var rawError = tokenResponse.Raw;
- var withoutInnerException = rawError.Split(new string[] { "" }, StringSplitOptions.RemoveEmptyEntries);
- throw new AbpException(withoutInnerException[0]);
+ tokenCacheItem = new IdentityModelTokenCacheItem(tokenResponse.AccessToken);
+ await TokenCache.SetAsync(cacheKey, tokenCacheItem,
+ new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(configuration.CacheAbsoluteExpiration)
+ });
}
- return tokenResponse.AccessToken;
+ return tokenCacheItem.AccessToken;
}
protected virtual void SetAccessToken(HttpClient client, string accessToken)
@@ -110,8 +125,33 @@ private IdentityClientConfiguration GetClientConfiguration(string identityClient
ClientOptions.IdentityClients.Default;
}
- protected virtual async Task GetDiscoveryResponse(
- IdentityClientConfiguration configuration)
+ protected virtual async Task GetTokenEndpoint(IdentityClientConfiguration configuration)
+ {
+ //TODO: Can use (configuration.Authority + /connect/token) directly?
+
+ var tokenEndpointUrlCacheKey = CalculateDiscoveryDocumentCacheKey(configuration);
+ var discoveryDocumentCacheItem = await DiscoveryDocumentCache.GetAsync(tokenEndpointUrlCacheKey);
+ if (discoveryDocumentCacheItem == null)
+ {
+ var discoveryResponse = await GetDiscoveryResponse(configuration);
+ if (discoveryResponse.IsError)
+ {
+ throw new AbpException($"Could not retrieve the OpenId Connect discovery document! " +
+ $"ErrorType: {discoveryResponse.ErrorType}. Error: {discoveryResponse.Error}");
+ }
+
+ discoveryDocumentCacheItem = new IdentityModelDiscoveryDocumentCacheItem(discoveryResponse.TokenEndpoint);
+ await DiscoveryDocumentCache.SetAsync(tokenEndpointUrlCacheKey, discoveryDocumentCacheItem,
+ new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(configuration.CacheAbsoluteExpiration)
+ });
+ }
+
+ return discoveryDocumentCacheItem.TokenEndpoint;
+ }
+
+ protected virtual async Task GetDiscoveryResponse(IdentityClientConfiguration configuration)
{
using (var httpClient = HttpClientFactory.CreateClient(HttpClientName))
{
@@ -128,10 +168,10 @@ protected virtual async Task GetDiscoveryResponse(
}
}
- protected virtual async Task GetTokenResponse(
- DiscoveryDocumentResponse discoveryResponse,
- IdentityClientConfiguration configuration)
+ protected virtual async Task GetTokenResponse(IdentityClientConfiguration configuration)
{
+ var tokenEndpoint = await GetTokenEndpoint(configuration);
+
using (var httpClient = HttpClientFactory.CreateClient(HttpClientName))
{
AddHeaders(httpClient);
@@ -140,12 +180,12 @@ protected virtual async Task GetTokenResponse(
{
case OidcConstants.GrantTypes.ClientCredentials:
return await httpClient.RequestClientCredentialsTokenAsync(
- await CreateClientCredentialsTokenRequestAsync(discoveryResponse, configuration),
+ await CreateClientCredentialsTokenRequestAsync(tokenEndpoint, configuration),
CancellationTokenProvider.Token
);
case OidcConstants.GrantTypes.Password:
return await httpClient.RequestPasswordTokenAsync(
- await CreatePasswordTokenRequestAsync(discoveryResponse, configuration),
+ await CreatePasswordTokenRequestAsync(tokenEndpoint, configuration),
CancellationTokenProvider.Token
);
default:
@@ -154,11 +194,11 @@ await CreatePasswordTokenRequestAsync(discoveryResponse, configuration),
}
}
- protected virtual Task CreatePasswordTokenRequestAsync(DiscoveryDocumentResponse discoveryResponse, IdentityClientConfiguration configuration)
+ protected virtual Task CreatePasswordTokenRequestAsync(string tokenEndpoint, IdentityClientConfiguration configuration)
{
var request = new PasswordTokenRequest
{
- Address = discoveryResponse.TokenEndpoint,
+ Address = tokenEndpoint,
Scope = configuration.Scope,
ClientId = configuration.ClientId,
ClientSecret = configuration.ClientSecret,
@@ -172,13 +212,11 @@ protected virtual Task CreatePasswordTokenRequestAsync(Dis
return Task.FromResult(request);
}
- protected virtual Task CreateClientCredentialsTokenRequestAsync(
- DiscoveryDocumentResponse discoveryResponse,
- IdentityClientConfiguration configuration)
+ protected virtual Task CreateClientCredentialsTokenRequestAsync(string tokenEndpoint, IdentityClientConfiguration configuration)
{
var request = new ClientCredentialsTokenRequest
{
- Address = discoveryResponse.TokenEndpoint,
+ Address = tokenEndpoint,
Scope = configuration.Scope,
ClientId = configuration.ClientId,
ClientSecret = configuration.ClientSecret
@@ -209,5 +247,15 @@ protected virtual void AddHeaders(HttpClient client)
client.DefaultRequestHeaders.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
}
}
+
+ protected virtual string CalculateDiscoveryDocumentCacheKey(IdentityClientConfiguration configuration)
+ {
+ return IdentityModelDiscoveryDocumentCacheItem.CalculateCacheKey(configuration);
+ }
+
+ protected virtual string CalculateTokenCacheKey(IdentityClientConfiguration configuration)
+ {
+ return IdentityModelTokenCacheItem.CalculateCacheKey(configuration);
+ }
}
}
diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs
new file mode 100644
index 00000000000..3a07cb3735b
--- /dev/null
+++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs
@@ -0,0 +1,27 @@
+using System;
+using Volo.Abp.MultiTenancy;
+
+namespace Volo.Abp.IdentityModel
+{
+ [Serializable]
+ [IgnoreMultiTenancy]
+ public class IdentityModelDiscoveryDocumentCacheItem
+ {
+ public string TokenEndpoint { get; set; }
+
+ public IdentityModelDiscoveryDocumentCacheItem()
+ {
+
+ }
+
+ public IdentityModelDiscoveryDocumentCacheItem(string tokenEndpoint)
+ {
+ TokenEndpoint = tokenEndpoint;
+ }
+
+ public static string CalculateCacheKey(IdentityClientConfiguration configuration)
+ {
+ return configuration.Authority.ToLower().ToMd5();
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelTokenCacheItem.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelTokenCacheItem.cs
new file mode 100644
index 00000000000..e8f8dfb8dba
--- /dev/null
+++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelTokenCacheItem.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Linq;
+using Volo.Abp.MultiTenancy;
+
+namespace Volo.Abp.IdentityModel
+{
+ [Serializable]
+ [IgnoreMultiTenancy]
+ public class IdentityModelTokenCacheItem
+ {
+ public string AccessToken { get; set; }
+
+ public IdentityModelTokenCacheItem()
+ {
+
+ }
+
+ public IdentityModelTokenCacheItem(string accessToken)
+ {
+ AccessToken = accessToken;
+ }
+
+ public static string CalculateCacheKey(IdentityClientConfiguration configuration)
+ {
+ return string.Join(",", configuration.Select(x => x.Key + ":" + x.Value)).ToMd5();
+ }
+ }
+}