Hi, thank you for the response. My problem is that the authentication is not coming from a user, but from an application with client credentials flow, and with this authentication flow IAbpClaimsPrincipalContributor is not invoked :-(
Hi @maliming, thank you for your feedback.
The final solution is the following:
1️⃣ OpenIddict – Custom Claims Handler
Implemented TourOperatorClientCredentialsClaimsHandler and updated InventoryHttpApiHostModule to inject the claim into JWT tokens:
public class TourOperatorClientCredentialsClaimsHandler :
IOpenIddictServerHandler<OpenIddictServerEvents.ProcessSignInContext>,
ITransientDependency
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly ILogger<TourOperatorClientCredentialsClaimsHandler> _logger;
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.ProcessSignInContext>()
.UseScopedHandler<TourOperatorClientCredentialsClaimsHandler>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictServerHandlerType.Custom)
.Build();
public TourOperatorClientCredentialsClaimsHandler(
IOpenIddictApplicationManager applicationManager,
ILogger<TourOperatorClientCredentialsClaimsHandler> logger)
{
_applicationManager = applicationManager;
_logger = logger;
}
public async ValueTask HandleAsync(OpenIddictServerEvents.ProcessSignInContext context)
{
// Check if this is a client_credentials grant
if (context.Request?.GrantType != OpenIddictConstants.GrantTypes.ClientCredentials)
{
return;
}
var clientId = context.Request.ClientId;
if (string.IsNullOrEmpty(clientId))
{
_logger.LogWarning("No client_id found in request");
return;
}
_logger.LogInformation($"Processing client_credentials sign-in for client: {clientId}");
// Load application by client_id
var application = await _applicationManager.FindByClientIdAsync(clientId, context.CancellationToken);
if (application == null)
{
_logger.LogWarning($"Application not found for client_id: {clientId}");
return;
}
// Read your extra property from the OpenIddict application "Properties" JSON
var properties = await _applicationManager.GetPropertiesAsync(application, context.CancellationToken);
_logger.LogInformation($"Application properties count: {properties.Count}");
if (!properties.TryGetValue(ExtraPropertyConsts.TourOperatorCodeClaimName, out var tourOperatorCodeElement))
{
_logger.LogWarning($"No {ExtraPropertyConsts.TourOperatorCodeClaimName} property found for client: {clientId}");
return;
}
string? tourOperatorCode = null;
if (tourOperatorCodeElement.ValueKind == JsonValueKind.String)
{
tourOperatorCode = tourOperatorCodeElement.GetString();
}
if (string.IsNullOrWhiteSpace(tourOperatorCode))
{
_logger.LogWarning($"Empty {ExtraPropertyConsts.TourOperatorCodeClaimName} property for client: {clientId}");
return;
}
_logger.LogInformation($"Found tour_operator_code: '{tourOperatorCode}' for client: {clientId}");
// Add the claim to the principal
var identity = context.Principal?.Identity as ClaimsIdentity;
if (identity == null)
{
_logger.LogError($"No identity found in principal for client: {clientId}");
return;
}
// Create the claim with explicit destinations set to AccessToken
var claim = new Claim(ExtraPropertyConsts.TourOperatorCodeClaimName, tourOperatorCode);
claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(claim);
_logger.LogInformation($"Successfully added {ExtraPropertyConsts.TourOperatorCodeClaimName} claim with value '{tourOperatorCode}' and AccessToken destination for client: {clientId}");
}
}
PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.AddEventHandler(TourOperatorClientCredentialsClaimsHandler.Descriptor);
});
2️⃣ Claim-based Filtering Infrastructure
Added ITourOperatorCodeFilter interface
Added TourOperatorCodeFilterProvider
Added ExtraPropertyConsts for claim-based filtering logic
public class TourOperatorCodeFilterProvider : ITourOperatorCodeFilter, ITransientDependency
{
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor;
public TourOperatorCodeFilterProvider(ICurrentPrincipalAccessor currentPrincipalAccessor)
{
_currentPrincipalAccessor = currentPrincipalAccessor;
}
public string? TourOperatorCode
{
get
{
var principal = _currentPrincipalAccessor.Principal;
if (principal == null || principal.Identity?.IsAuthenticated != true)
{
return null;
}
var claim = principal.Claims?.FirstOrDefault(c => c.Type == ExtraPropertyConsts.TourOperatorCodeClaimName);
return claim?.Value;
}
}
public bool IsEnabled => !string.IsNullOrWhiteSpace(TourOperatorCode);
}
3️⃣ Automatic Query Filtering
Introduced BlockspaceQueryFilterExtensions
Updated the repository to automatically apply the filter to all Blockspace queries
public override async Task<IQueryable<Blockspace>> WithDetailsAsync()
{
var query = await base.WithDetailsAsync();
return query.ApplyTourOperatorCodeFilter(_dataFilter, _tourOperatorCodeFilter);
}
public override async Task<IQueryable<Blockspace>> GetQueryableAsync()
{
var query = await base.GetQueryableAsync();
return query.ApplyTourOperatorCodeFilter(_dataFilter, _tourOperatorCodeFilter);
}
🔜 Remaining Task
The only thing missing is the UI in Angular for managing OpenIddict Application Properties.
[AI-Bot] said: Yes — using an extra property on
OpenIddictApplicationis a good place to store your Tour Operator code, and yourObjectExtensionManager.Instance.Modules().ConfigureOpenIddict(... ConfigureApplication ...)approach is valid for persisting that metadata.But that alone does not automatically put it into the access token. You must do both:
- Store the value on the OpenIddict application (as you’re doing)
- Copy it into a claim when tokens are issued, and mark that claim’s destination as
AccessTokenBelow is a complete, ABP-supported way to do step (2).
1) Keep your OpenIddict application extra property (what you already did)
Your extension config is fine for adding an extra property to the OpenIddict application entity.
Then you set the value for each client (Tour Operator) in
/openiddict/ApplicationsUI or via code.
2) Add the extra property value into the access token as a custom claim
2.1 Create a claim destinations handler (ABP documented hook)
Create a handler that ensures your custom claim is actually included in the access token (otherwise OpenIddict may drop it depending on destinations).
using System.Threading.Tasks; using OpenIddict.Abstractions; using Volo.Abp.DependencyInjection; using Volo.Abp.OpenIddict; public class TourOperatorClaimDestinationsHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency { public Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context) { foreach (var claim in context.Principal.Claims) { if (claim.Type == ExtraPropertyConsts.TourOperatorCodePropertyName) { claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); } } return Task.CompletedTask; } }Register it:
Configure<AbpOpenIddictClaimsPrincipalOptions>(options => { options.ClaimsPrincipalHandlers.Add<TourOperatorClaimDestinationsHandler>(); });2.2 Add the claim to the principal during token issuance
You now need to add the claim value itself to the principal when the token is being created.
ABP’s OpenIddict docs recommend using claims principal handlers to control token claim destinations, but adding the claim value is typically done by contributing to the principal generation pipeline (implementation detail depends on whether you issue tokens for users or clients).
For client_credentials (machine-to-machine), you can add the claim during OpenIddict’s sign-in processing using an OpenIddict server event handler (this is also a known ABP approach in support answers for token claim customization).
Conceptually:
- Determine the client application (
client_id)- Load its extra property
TourOperatorCode- Add it as a claim onto the principal that becomes the access token principal
(Your storage choice—extra property on OpenIddictApplication—is fine; you’ll just need to load it via OpenIddict’s application manager or your own repository/table.)
3) Enforce filtering in your API using the claim
Once the access token contains
TourOperatorCode, you can filter:var tourOperatorCode = CurrentUser.FindClaimValue(ExtraPropertyConsts.TourOperatorCodePropertyName); // apply query restriction based on tourOperatorCodeThis makes the filter non-bypassable (because it is derived from the authenticated client identity, not request parameters).
Sources (validated)
- https://abp.io/docs/latest/modules/openiddict#updating-claims-in-access_token-and-id_token (ABP:
IAbpOpenIddictClaimsPrincipalHandler, destinations, registration)- https://documentation.openiddict.com/configuration/claim-destinations.html (OpenIddict claim destinations)
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
can you suggest the implementation for 2.2
In which project should I define TourOperatorClaimDestinationsHandler?
The extra property on OpenIddictApplication does not appear in the Angular UI.
It is not clear hot to "Add a custom claim to the access token (recommended)".
Can I use the extra property on [OpenIddictApplications] /openiddict/Applications ? I add the following code ObjectExtensionManager.Instance.Modules() .ConfigureOpenIddict(auth => { auth.ConfigureApplication(application => { //voglio impostare la maxlenght della property application.AddOrUpdateProperty<string>( ExtraPropertyConsts.TourOperatorCodePropertyName, property => { property.DefaultValue = string.Empty; property.Attributes.Add(new MaxLengthAttribute(ExtraPropertyConsts.MaxTourOperatorCodeLength)); // Imposta la lunghezza massima property.Attributes.Add(new DataTypeAttribute(DataType.Text)); }); }); });
Hi team, I have an IMS application built with ABP Commercial 9.0.4, configured with OpenIddict. I created an external machine-to-machine application to allow a third‑party software to integrate with one of my backend APIs. In IMS I have an entity called Blockspace. Each Blockspace belongs to one or more Tour Operators, but every Tour Operator should only be able to read Blockspaces that contain its own tourName. My question is: How can I filter API access so that the external application of a specific Tour Operator can retrieve only its own Blockspaces? More specifically:
Should I store the Tour Operator identifier in the OpenIddict application? Should I add a custom claim to the access token and filter by claim inside the application service/repository? Is there a recommended ABP pattern for applying this type of tenant-like filtering when it’s not a real tenant but a business domain filter?
Any guidance or best practices would be appreciated. Thank you!
Hi guys, is there on abp framework the Dynamic Property System feature? The same as aspnetzero https://docs.aspnetzero.com/aspnet-core-angular/latest/Feature-Dynamic-Entity-Parameters-Angular
many thanks
Hi liangshiwei, I had already tried, I added the following reference to the .EntityFrameworkCore.Tests.csproj <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.8" />
Dear abp team, I recently updated a module project to v8.0.4, but when I run the test on an azure pipeline on linux environment, I receive the following exception on dotnet test. On windows env the test run successfull.
Exception:
Neos.Rvu.Application -> /agent/_work/3/s/src/Neos.Rvu.Application/bin/Debug/net8.0/Neos.Rvu.Application.dll Neos.Rvu.EntityFrameworkCore.Tests -> /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/bin/Debug/net8.0/linux-x64/Neos.Rvu.EntityFrameworkCore.Tests.dll Test run for /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/bin/Debug/net8.0/linux-x64/Neos.Rvu.EntityFrameworkCore.Tests.dll (.NETCoreApp,Version=v8.0) Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64) Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait... A total of 1 test files matched the specified pattern. [xUnit.net 00:00:00.75] Neos.Rvu.Schedules.LegRepositoryTests.GetListAsync [FAIL] [xUnit.net 00:00:00.79] Neos.Rvu.Blockspaces.BlockspaceRepositoryTests.GetCountAsync [FAIL] [xUnit.net 00:00:00.81] Neos.Rvu.Schedules.LegRepositoryTests.GetCountAsync [FAIL] [xUnit.net 00:00:00.84] Neos.Rvu.Blockspaces.BlockspaceRepositoryTests.GetListAsync [FAIL] [xUnit.net 00:00:00.85] Neos.Rvu.Schedules.FlightRepositoryTests.GetCountAsync [FAIL] [xUnit.net 00:00:00.87] Neos.Rvu.NumericAvailabilities.NumericAvailabilityRepositoryTests.GetListAsync [FAIL] [xUnit.net 00:00:00.88] Neos.Rvu.Schedules.FlightRepositoryTests.GetListAsync [FAIL] [xUnit.net 00:00:00.89] Neos.Rvu.NumericAvailabilities.NumericAvailabilityRepositoryTests.GetCountAsync [FAIL] Failed Neos.Rvu.Schedules.LegRepositoryTests.GetListAsync [1 ms] Error Message: Volo.Abp.AbpInitializationException : An error occurred during ConfigureServices phase of the module Neos.Rvu.EntityFrameworkCore.RvuEntityFrameworkCoreTestModule, Neos.Rvu.EntityFrameworkCore.Tests, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null. See the inner exception for details. ---- System.TypeInitializationException : The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. -------- System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation. ------------ System.DllNotFoundException : Unable to load shared library 'e_sqlite3' or one of its dependencies. In order to help diagnose loading problems, consider using a tool like strace. If you're using glibc, consider setting the LD_DEBUG environment variable: /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/bin/Debug/net8.0/linux-x64/e_sqlite3.so: cannot open shared object file: No such file or directory /agent/_work/_tool/dotnet/shared/Microsoft.NETCore.App/8.0.2/e_sqlite3.so: cannot open shared object file: No such file or directory /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/bin/Debug/net8.0/linux-x64/libe_sqlite3.so) /agent/_work/_tool/dotnet/shared/Microsoft.NETCore.App/8.0.2/libe_sqlite3.so: cannot open shared object file: No such file or directory /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/bin/Debug/net8.0/linux-x64/e_sqlite3: cannot open shared object file: No such file or directory /agent/_work/_tool/dotnet/shared/Microsoft.NETCore.App/8.0.2/e_sqlite3: cannot open shared object file: No such file or directory /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/bin/Debug/net8.0/linux-x64/libe_sqlite3: cannot open shared object file: No such file or directory /agent/_work/_tool/dotnet/shared/Microsoft.NETCore.App/8.0.2/libe_sqlite3: cannot open shared object file: No such file or directory
Stack Trace:
at Volo.Abp.AbpApplicationBase.ConfigureServices()
at Volo.Abp.AbpApplicationBase..ctor(Type startupModuleType, IServiceCollection services, Action1 optionsAction) at Volo.Abp.AbpApplicationWithExternalServiceProvider..ctor(Type startupModuleType, IServiceCollection services, Action1 optionsAction)
at Volo.Abp.AbpApplicationFactory.Create(Type startupModuleType, IServiceCollection services, Action1 optionsAction) at Volo.Abp.AbpApplicationFactory.Create[TStartupModule](IServiceCollection services, Action1 optionsAction)
at Microsoft.Extensions.DependencyInjection.ServiceCollectionApplicationExtensions.AddApplication[TStartupModule](IServiceCollection services, Action1 optionsAction) at Volo.Abp.Testing.AbpIntegratedTest1..ctor()
at Neos.Rvu.RvuTestBase`1..ctor()
at Neos.Rvu.EntityFrameworkCore.RvuEntityFrameworkCoreTestBase..ctor()
at Neos.Rvu.Schedules.LegRepositoryTests..ctor() in /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/Schedules/LegRepositoryTests.cs:line 15
at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
----- Inner Stack Trace -----
at Microsoft.Data.Sqlite.SqliteConnection..ctor(String connectionString)
at Neos.Rvu.EntityFrameworkCore.RvuEntityFrameworkCoreTestModule.CreateDatabaseAndGetConnection() in /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/EntityFrameworkCore/RvuEntityFrameworkCoreTestModule.cs:line 41
at Neos.Rvu.EntityFrameworkCore.RvuEntityFrameworkCoreTestModule.ConfigureServices(ServiceConfigurationContext context) in /agent/_work/3/s/test/Neos.Rvu.EntityFrameworkCore.Tests/EntityFrameworkCore/RvuEntityFrameworkCoreTestModule.cs:line 27
at Volo.Abp.AbpApplicationBase.ConfigureServices()
----- Inner Stack Trace -----
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
at Microsoft.Data.Sqlite.SqliteConnection..cctor()
----- Inner Stack Trace -----
at SQLitePCL.SQLite3Provider_e_sqlite3.NativeMethods.sqlite3_libversion_number()
at SQLitePCL.SQLite3Provider_e_sqlite3.SQLitePCL.ISQLite3Provider.sqlite3_libversion_number()
at SQLitePCL.raw.SetProvider(ISQLite3Provider imp)
at SQLitePCL.Batteries_V2.Init()
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Did you know
The missing section in dynamic-env.json was AbpAccountPublic. We solve adding the following section inside the file.
{
"production": true,
"application": {
"baseUrl":"http://localhost:4200",
"name": "App",
"logoUrl": ""
},
"oAuthConfig": {
"issuer": "https://localhost:44391/",
"redirectUri": "http://localhost:4200",
"clientId": "App_App",
"responseType": "code",
"scope": "offline_access openid profile email phone App"
},
"apis": {
"default": {
"url": "https://localhost:44391",
"rootNamespace": "App"
},
"AbpAccountPublic": {
"url": "https://localhost:44391/",
"rootNamespace": "AbpAccountPublic"
}
}
}