Understood... Thank you.
Unfortunately there is no better way
Well, maybe Ocelot Gateway project would help it? Will it work to merge localizations from all running sites (i.e. merge the "application-localization" endpoints results) and finally get the full list at the site with Role Management page, will the page "see" all those?
I do have localization for permissions, but the translations are stored in the resource files (in Domain.Shared
project) of the respective applications: A, B, C.
So to show the display names of all of them on a central Permission Management page, I need to retrieve the translations from those projects.
And to my knowledge, there are only two options here:
Domain.Shared
projects - which are now published as Nuget package) in the solution where the Permission Management page is;I don't like variant #1 because of the reasons described previously. And I don't like variant #2 much either, because there could be a lot of excessive data - /api/abp/application-localization endpoint only accepts the language parameter, no any resource key mask...
I thought that maybe there's a better way?
I cannot hardcode display name of the permission in the property, if this is what you meant, because I have a multi-language app, so the permission display name needs to be changed depending on the current UI language.
What is the project of this error log? You can try to add your code to Domain module.
https://abp.io/support/questions/7699/Random-exception-after-switching-from-IdentityServer-to-OpenDict#answer-3a147d57-7267-7eaf-1d74-ba36bdfea929
Sorry, I am not sure I am following you. The first exception (DI issue) needs to be reproduced yet, I am waiting for my colleague trying to assist me, because I did not manage to get this exception. So I cannot say anything here yet.
But instead I received another exception related to the transaction level. According to your recommendation, I have overriden all the stores with the code you provided for the older version of ABP - in order to override a transaction level. After adding this code, I can see that the Store
property is properly filled with my custom class. All the constructors of all three custom stores did invoke - so the code was applied as expected. Still, I received the same exception again at some point. I cannot see how this may ever happen, if all my front-end applications interact with the same OpenId Server where I made the override and - I expect - there is only one "entry point" to reach out the TokenCleanupBackgroundWorker
where the exception happens and this "entry point" is the interaction between front-end applications and OpenId Server?
It was related to using .First
inside the loop. And we had many permissions. Replaced IEnumerable with the Dictionary and now it's fine.
Your code is no problem, that strange. Can you share a test project?
Unfortunately I cannot. I only can follow some recommendations. So from what you say I can deduce, that some of the custom store or all of them are not invoked, right? Where should I put the breakpoint in debug?
using AbxEps.MyApp.AuthServer.Grants;
using AbxEps.MyApp.AuthServer.Stores;
using AbxEps.MyApp.Cryptography;
using AbxEps.MyApp.EntityFrameworkCore;
using AbxEps.MyApp.Filters;
using AbxEps.MyApp.Localization;
using AbxEps.MyApp.Middlewares;
using AbxEps.MyApp.MultiTenancy;
using AbxEps.LanguageCodeConvertor.Extensions;
using AbxEps.Middleware.AbxRequestContext.Extensions;
using AbxEps.Middleware.CookieAccessToken.Extensions;
using Localization.Resources.AbpUi;
using Medallion.Threading;
using Medallion.Threading.Oracle;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;
using Serilog;
using StackExchange.Redis;
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Account.Public.Web;
using Volo.Abp.Account.Public.Web.Impersonation;
using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpApplicationPath;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Auditing;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.Caching;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.DistributedLocking;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Identity;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.OpenIddict;
using Volo.Abp.OpenIddict.Applications;
using Volo.Abp.OpenIddict.Authorizations;
using Volo.Abp.OpenIddict.ExtensionGrantTypes;
using Volo.Abp.OpenIddict.Tokens;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Ui.LayoutHooks;
using Volo.Abp.UI.Navigation.Urls;
using Volo.Abp.VirtualFileSystem;
using Volo.Saas.Host;
using AbxEps.CentralTools.AuthServer.Grants;
using AbxEps.CentralTools.AuthServer.Stores;
using AbxEps.CentralTools.Cryptography;
using AbxEps.CentralTools.Filters;
using AbxEps.CentralTools.Middlewares;
using Twilio.Rest.Microvisor.V1;
namespace AbxEps.MyApp;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpCachingStackExchangeRedisModule),
typeof(AbpDistributedLockingModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpAccountPublicWebOpenIddictModule),
typeof(AbpAccountPublicHttpApiModule),
typeof(AbpAspNetCoreMvcUiLeptonThemeModule),
typeof(AbpAccountPublicApplicationModule),
typeof(AbpAccountPublicWebImpersonationModule),
typeof(SaasHostApplicationContractsModule),
typeof(MyAppEntityFrameworkCoreModule)
)]
public class MyAppAuthServerModule : AbpModule
{
private const string DefaultCorsPolicyName = "Default";
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
//var logger = context.Services.GetInitLogger<MyAppAuthServerModule>();
Log.Information($"*** {nameof(MyAppAuthServerModule)}.{nameof(PreConfigureServices)}: HostingEnvironment: {hostingEnvironment.EnvironmentName}");
PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
{
// https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
if (!hostingEnvironment.IsDevelopment())
{
options.AddDevelopmentEncryptionAndSigningCertificate = false;
}
});
PreConfigure<OpenIddictServerBuilder>(builder =>
{
// https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
// not needed because default of AddDevelopmentEncryptionAndSigningCertificate is true in Development...
/*if (hostingEnvironment.IsDevelopment())
{
builder
.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
}*/
if (!hostingEnvironment.IsDevelopment())
{
// Encryption
string encryptionCertThumbPrint = configuration["Certificates:Encryption:ThumbPrint"];
string encryptionCertFile = configuration["Certificates:Encryption:File"];
string encryptionCertPassword = configuration["Certificates:Encryption:Password"];
if (string.IsNullOrEmpty(encryptionCertThumbPrint) && string.IsNullOrEmpty(encryptionCertFile))
{
string message = "Neither Certificates:Encryption:ThumbPrint nor Certificates:Encryption:File is set!";
Log.Error($"*** {message}!");
throw new NotSupportedException(message);
}
if (!string.IsNullOrEmpty(encryptionCertFile) && !File.Exists(encryptionCertFile))
{
string message = $"Cannot find encryption certificate '{encryptionCertFile}'!";
Log.Error($"*** {message}!");
throw new NotSupportedException(message);
}
X509Certificate2 certificate = X509Helper.GetCertificate(encryptionCertThumbPrint, encryptionCertFile, encryptionCertPassword, Log.Logger);
if (certificate == null)
{
// throw exception, stop service...
string cert = string.IsNullOrEmpty(encryptionCertThumbPrint) ? encryptionCertFile : encryptionCertThumbPrint;
throw new NotSupportedException($"Encryption Certificate '{cert}' not found!");
}
else
{
Log.Debug($"Encryption Certificate: Issuer is '{certificate.Issuer}'");
builder.AddEncryptionCertificate(certificate);
}
// Signing Certificate
string signingCertThumbPrint = configuration["Certificates:Signing:ThumbPrint"];
string signingCertFile = configuration["Certificates:Signing:file"];
string signingCertPassword = configuration["Certificates:Signing:Password"];
if (string.IsNullOrEmpty(signingCertThumbPrint) && string.IsNullOrEmpty(signingCertFile))
{
string message = "Neither Certificates:Signing:ThumbPrint nor Certificates:Signing:File is set!";
Log.Error($"*** {message}!");
throw new NotSupportedException(message);
}
if (!string.IsNullOrEmpty(signingCertFile) && !File.Exists(signingCertFile))
{
string message = $"Cannot find signing certificate '{signingCertFile}'!";
Log.Error($"*** {message}!");
throw new NotSupportedException(message);
}
certificate = X509Helper.GetCertificate(signingCertThumbPrint, signingCertFile, signingCertPassword, Log.Logger);
if (certificate == null)
{
// throw exception, stop service...
string cert = string.IsNullOrEmpty(signingCertThumbPrint) ? signingCertFile : signingCertThumbPrint;
throw new NotSupportedException($"Signing Certificate '{cert}' not found!");
}
else
{
Log.Debug($"Signing Certificate: Issuer is '{certificate.Issuer}'");
builder.AddSigningCertificate(certificate);
}
}
builder.Configure(options =>
{
// adobu TODO
// https://documentation.openiddict.com/configuration/token-formats.html#disabling-jwt-access-token-encryption
// DisableAccessTokenEncryption is set to true in Volo.Abp.OpenIddict.AbpOpenIddictAspNetCoreModule.AddOpenIddictServer
// options.DisableAccessTokenEncryption = false;
//options.GrantTypes.Add(AbxLoginGrant.ExtensionGrantName);
options.GrantTypes.Add(SwitchToTenantGrant.ExtensionGrantName);
});
});
PreConfigure<OpenIddictBuilder>(builder =>
{
builder.AddValidation(options =>
{
options.AddAudiences("MyApp");
options.UseLocalServer();
options.UseAspNetCore();
});
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
if (!Convert.ToBoolean(configuration["App:DisablePII"]))
{
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
}
if (!Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]))
{
Configure<OpenIddictServerAspNetCoreOptions>(options =>
{
options.DisableTransportSecurityRequirement = true;
});
}
context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
// This is added for a correct root path in js files
Configure<AbpLayoutHookOptions>(options =>
{
options.Add(LayoutHooks.Head.Last,
typeof(AbpApplicationPathViewComponent));
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<MyAppResource>()
.AddBaseTypes(
typeof(AbpUiResource)
);
});
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(
LeptonThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/global-styles.css");
}
);
});
Configure<AbpAuditingOptions>(options =>
{
//options.IsEnabledForGetRequests = true;
options.ApplicationName = "AuthServer";
});
if (hostingEnvironment.IsDevelopment())
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.ReplaceEmbeddedByPhysical<MyAppDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}AbxEps.MyApp.Domain.Shared", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<MyAppDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}AbxEps.MyApp.Domain", Path.DirectorySeparatorChar)));
});
}
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.Applications["Angular"].RootUrl = configuration["App:AngularUrl"];
options.Applications["Angular"].Urls[AccountUrlNames.PasswordReset] = "account/reset-password";
options.Applications["Angular"].Urls[AccountUrlNames.EmailConfirmation] = "account/email-confirmation";
});
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false;
});
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "Abx:";
});
Configure<MvcOptions>(options =>
{
options.Filters.Add(typeof(TokenExpiredExceptionFilter));
});
var dataProtectionBuilder = context.Services.AddDataProtection().SetApplicationName("MyApp");
if (!hostingEnvironment.IsDevelopment())
{
var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
dataProtectionBuilder.PersistKeysToStackExchangeRedis(redis, "AuthServer-Protection-Keys");
}
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
//var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
//return new RedisDistributedSynchronizationProvider(connection.GetDatabase());
return new OracleDistributedSynchronizationProvider(configuration["ConnectionStrings:Default"]);
});
ConfigureCors(context, configuration);
ConfigureAuth(context, configuration);
ConfigureOpenIdDict(context, configuration);
ConfigureStores(context, configuration);
context.Services.Configure<AbpAccountOptions>(options =>
{
options.TenantAdminUserName = "admin";
options.ImpersonationTenantPermission = SaasHostPermissions.Tenants.Impersonation;
options.ImpersonationUserPermission = IdentityPermissions.Users.Impersonation;
});
Configure<PermissionManagementOptions>(options =>
{
options.IsDynamicPermissionStoreEnabled = true;
});
Configure<FeatureManagementOptions>(options =>
{
options.IsDynamicFeatureStoreEnabled = true;
});
}
private static void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddCors(options =>
{
options.AddPolicy(DefaultCorsPolicyName, builder =>
{
builder
.WithOrigins(
configuration["App:CorsOrigins"]
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.Trim().RemovePostFix("/"))
.ToArray()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
}
private static void ConfigureAuth(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication();
}
private static void ConfigureStores(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddOpenIddict()
.AddCore(builder =>
{
builder
.AddApplicationStore<AbxAbpOpenIddictApplicationStore>()
.AddAuthorizationStore<AbxAbpOpenIddictAuthorizationStore>()
.AddTokenStore<AbxAbpOpenIddictTokenStore>();
});
}
private void ConfigureOpenIdDict(ServiceConfigurationContext context, IConfiguration configuration)
{
// Configure<TokenCleanupOptions>(options => { });
Configure<AbpOpenIddictExtensionGrantsOptions>(options =>
{
//options.Grants.Add(AbxLoginGrant.ExtensionGrantName, new AbxLoginGrant());
options.Grants.Add(SwitchToTenantGrant.ExtensionGrantName, new SwitchToTenantGrant());
});
// This is required for working of AbxRequestContext
context.Services.AddHttpContextAccessor();
context.Services.InitLanguageCodeConvertor(configuration);
context.Services.InitAbxRequestContext();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAbpRequestLocalization();
if (!env.IsDevelopment())
{
app.UseErrorPage();
}
app.UseCorrelationId();
app.UseAbpSecurityHeaders();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(DefaultCorsPolicyName);
app.UseAuthentication();
//app.UseAbpOpenIddictValidation(); // UseIdentityServer
app.UseCookieAccessTokenDelete(HeaderNames.Authorization);
/* https://docs.abp.io/en/abp/7.0/Migration-Guides/IdentityServer_To_OpenIddict
app.UseJwtTokenMiddleware();*/
if (MultiTenancyConsts.IsEnabled)
{
app.UseMultiTenancy();
}
app.UseUnitOfWork();
app.UseMiddleware<SessionEndAuditingMiddleware>();
app.UseMiddleware<TenantSwitchMiddleware>();
app.UseAbpOpenIddictValidation(); // UseIdentityServer
app.UseAuthorization();
app.UseAuditing();
app.UseAbpSerilogEnrichers();
app.UseConfiguredEndpoints();
}
}
So while the initial exception reproducing still pending, I received this again:
> [09:26:33 INF] Lock is acquired for TokenCleanupBackgroundWorker
> [09:26:33 INF] Start cleanup.
> [09:26:33 INF] Start cleanup tokens.
> fail: Microsoft.EntityFrameworkCore.Infrastructure[0]
> 2024-08-19 09:26:34.569195 ThreadID:500 (ERROR) OracleExecutionStrategy.ExecuteAsync() : System.ArgumentException: IsolationLevel must be ReadCommitted or Serializable (Parameter 'isolationLevel')
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginTransaction(IsolationLevel isolationLevel)
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginDbTransaction(IsolationLevel isolationLevel)
> at System.Data.Common.DbConnection.BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> --- End of stack trace from previous location ---
> at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
> [09:26:34 ERR] IsolationLevel must be ReadCommitted or Serializable (Parameter 'isolationLevel')
> System.ArgumentException: IsolationLevel must be ReadCommitted or Serializable (Parameter 'isolationLevel')
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginTransaction(IsolationLevel isolationLevel)
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginDbTransaction(IsolationLevel isolationLevel)
> at System.Data.Common.DbConnection.BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> --- End of stack trace from previous location ---
> at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.CreateDbContextAsync(IUnitOfWork unitOfWork)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.CreateDbContextAsync(IUnitOfWork unitOfWork, String connectionStringName, String connectionString)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.GetDbContextAsync()
> at Volo.Abp.Domain.Repositories.EntityFrameworkCore.EfCoreRepository`2.GetDbSetAsync()
> at Volo.Abp.Domain.Repositories.EntityFrameworkCore.EfCoreRepository`2.GetQueryableAsync()
> at Volo.Abp.OpenIddict.Tokens.EfCoreOpenIddictTokenRepository.PruneAsync(DateTime date, CancellationToken cancellationToken)
> at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)
> at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()
> at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
> at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
> at Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenStore.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
> at Volo.Abp.OpenIddict.Tokens.TokenCleanupService.CleanAsync()
And it is happening after I replaced all the stores as you suggested. The code of the stores was copy-pasted from yours:
context.Services.AddOpenIddict()
.AddCore(builder =>
{
builder
.AddApplicationStore<AbxAbpOpenIddictApplicationStore>()
.AddAuthorizationStore<AbxAbpOpenIddictAuthorizationStore>()
.AddTokenStore<AbxAbpOpenIddictTokenStore>();
});
Maybe I am adding this code too late inside ConfigureServices
if the order matters at all here? What is it supposed to follow in OpenIDServer module?
Please share full request and response logs.
Sorry - my bad (due to very late time here). I've updated the message above. So it is seen the time is spent here. But there is no more information:
2024-08-16 04:25:17.610 -05:00 [DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Roles 2024-08-16 04:25:55.009 -05:00 [DBG] Added 0 entity changes to the current audit log