- ABP Framework version: v8.2.1
- UI Type: Angular
- Database System: EF Core (MySQL)
- Tiered (for MVC) or Auth Server Separated (for Angular): yes, Angular with Microservice Architecture
- Exception message and full stack trace:
- Steps to reproduce the issue:
Hello Team,
We have a microservices based architecture solution for our project. We don't want to use the TenantId in the AbpUsers and AbpRoles tables as per our requirements. For that what we have done is, we have kept separate tables (UserTenantAssociation and RoleTenantAssociation), using these tables we will determine which user belongs to which Tenant. And for the Roles, we will have all the Roles in the AbpRoles table with all records having TenantId as NULL, which implies the Roles will be created only in the Host Tenant and not any other Tenant. The other Tenants will be using the same Roles as Host, and which Tenants have which specific Roles to use in their tenant, that will be determined using our custom RoleTenantAssociation table where RoleId (the Id of the role from the host tenant) and the TenantId of that Tenant will be stored).
Now, displaying the list of Roles and Users on the UI doesn't seem to be a problem as we have already done necessary changes in the Users and Roles repositories in the IdentityService to achiever this feat. But the problem arises when the User logs into the Tenants.
Let's say I have a User which belongs to a Tenant, and the User has a role assigned to it as "admin", now in the AbpUserRoles table, the UserId will be the Id of the User from AbpUsers table, TenantId will be TenantId of the Tenant in which the user is trying to log into and the RoleId will be the Id of the Role "admin" from AbpRoles table but it will have TenantId as NULL as the Role belongs to the Host and the same Role should be used by all the Tenants.
Now if we run the application and when the user logs into a Tenant, it doesn't have any Roles assigned to it in the CurrentUser class, and also the GrantedPolicies will also be empty since there are no roles assigned to the user in the currentUser section of application configuration api call.
I tried to check how the values are assigned to the CurrentUser, and I came to know that it gets the values from the Claims generated during the Authentication and are passed to JWT Token during the authentication.
https://github.com/abpframework/abp/blob/8e20aab617205936c299ed5c3c40e0c529a3f06b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs#L14
this is the code I tried :
public class AbpUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser, IdentityRole>, ITransientDependency { public AbpUserClaimsPrincipalFactory( UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base( userManager, roleManager, options) { }
[UnitOfWork]
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
var principal = await base.CreateAsync(user).ConfigureAwait(false);
if (user.TenantId.HasValue)
{
principal.Identities
.First()
.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
}
return principal;
}
}
(I tried this code in Administration Service Domain project)
but when using it, the login page will just stay there even after clicking the login button with correct credentials, it doesn't redirect to the angular app.
I want to know how exactly the CurrentUser is assigned these values and I want to override it because we have different logic of fetching the roles (from our custom table). I specifically want to know how the roles are assigned to the current user.
Right now, what I have done is, I have manually updated the value of the RoleId in the AbpUserRoles table, I have updated the RoleId with the one which belongs to the host. And because of that when the user logs into the application, there in, the api/abp/application-configuration?includeLocalizationResources=false api is called and in response of that API call, the grantedPolicies in the "auth" section is an empty array and in the "currentUser" section the roles is an empty array.
example : { "auth": { "grantedPolicies": [] }, "currentUser": { "roles": [], }, } So, given the scenario, how exactly can I set these granted policies and the currentUser values in the application when the user logs in?
94 Answer(s)
-
0
ok, I will check it later.
-
0
hi
I can't build your project.
Maybe we are going in the wrong direction. Is your current goal to replace UserManage with your custom service in the entire microservice?
-
0
Yes, we want a way in which our entire microservice template should use our custom UserManager file, and even this custom file will be exactly the same as the ABP's UserManager, but just at few places in the methods we will be overring the code. Wherever there is UserManager file used even at core level, it should use our custom UserManager.
-
0
We are trying to override following files :
In AuthServer : AbpUserClaimsPrincipalFactory.cs
In IdentityService : IdentityDynamicClaimsPrincipalContributor.cs IdentityDynamicClaimsPrincipalContributorCache.cs IdentityUserManager.cs
-
0
I'll again explain you the whole scenario what we are looking for :
What ABP does :
ABP has a host tenant, which has Tenant Id as NULL everywhere. Now let's say we create a role from the host called "app user". And it will be stored in the AbpRoles table where TenantId of the role will be NULL.
We then provide certain permissions to that role. And that information will be stored in the AbpPermissionGrants table, and there as well the TenantId will be NULL.
Now if we create a user in the host, that user will be created in the AbpUsers table and it will have TenantId as NULL. And then if we assign that "app user" role to that new user, that information will be stored in the AbpUserRoles table. Here, we will have UserId as Id from AbpUsers, RoleId as Id from AbpRoles and TenantId as NULL.
Now, wherever the user will login, it will have only those permissions granted to it which are related to the role he's assigned, and what permissions are granted to that user will be available in the "GrantedPolicies" section of the abp app configuration API call response under the "auth" section. Here we also have a section called CurrentUser, in which we will have the property called "roles", inside which we will have list of all the roles assigned to that user. And according to whatever permissions granted to that user, it will be able to perform the actions.
What we want :
Consider the whole ABP scenario explained above again.
Now let's, say I create a Tenant called "test". It will have a certain TenantId.
Now, let's say I create a user in this tenant, and for that the data will be stored in the AbpUsers table, with that same TenantId. We don't want this TenantId to be stored here in this AbpUsers table, instead we will store the TenantId and the Id of the User in the association table that we created, "user_tenant_association" table. So overall, the user will be added in the AbpUsers table with TenantId as NULL, even though it's been added from a tenant. But that user belongs to that Tenant, that information will be stored in our custom table, where the actual TenantId will be stored along with the Id of the user.
Everything fair and square till this point.
Now, in the ABP Scenario, we created a Role called "app user" right, we want to assign that role to this user that we created in the test tenant. This is what we are looking to achieve.
To make this happen, we will have to add the data in the AbpUserRoles table, as follow :
AbpUserRoles table columns with values
UserId : Id of the User from AbpUsers table RoleId : Id of the Role "app user" from AbpRoles table (WHICH HAS TENANT ID AS NULL) TenantId : The ACTUAL TenantId of the Tenant.
Now, keeping this scenario in mind, we want this user created in the test tenant, to have roles as "app user" and permissions granted to it as whatever permissions a user will have if it was created in host tenant with the exact same "app user" role assigned to it.
This is what we want to achieve.
-
0
hi
I will implement these replacements. But can you make your project can be built?
Thanks
In AuthServer : AbpUserClaimsPrincipalFactory.cs In IdentityService : IdentityDynamicClaimsPrincipalContributor.cs IdentityDynamicClaimsPrincipalContributorCache.cs IdentityUserManager.cs
-
0
Can you pull the changes now and build?
-
0
ok, I will add code to replace the services.
-
0
-
0
Hi, I tried the changes you provided. I still couldn't hit the breakpoints. After clicking on login button for the first time, it would redirect to the landing page of the angular app (logged out screen) and again redirect to the login page, and for the next time onwards while clicking on the login button, it will just stay there on the login page. But one thing I observed is that now in the logs, I am not getting that User Not Found error, I am sharing the logs via email, can you check what could be wrong here.
-
0
ok, please share your logs via https://wetransfer.com/
liming.ma@volosoft.com
Thanks
-
0
Shared the logs, please check.
-
0
hi
Please try to disable the
dynamic-claims
and try again, then share the logs.https://abp.io/docs/latest/framework/fundamentals/dynamic-claims?_redirected=B8ABF606AA1BDF5C629883DF1061649A#enabling-the-dynamic-claims
Thanks
-
0
Shared you the logs after disabling the dynamic logs.
-
0
hi
Please try to remove all
DynamicContributors
and try again.public override void ConfigureServices(ServiceConfigurationContext context) { PostConfigure<AbpClaimsPrincipalFactoryOptions>(options => { options.DynamicContributors.Clear(); }); }
Thanks
-
0
I made the following changes in the healthAuthServerModule.cs file of AuthServer
context.Services.PostConfigure<AbpClaimsPrincipalFactoryOptions>(options => { options.DynamicContributors.Clear(); options.DynamicContributors.RemoveAll(x => x == typeof(Volo.Abp.Identity.IdentityDynamicClaimsPrincipalContributor)); });
After adding this, when I click on the login button now, it does redirect to the angular application, but it will go there and keep reloading the same angular landing page (logged out page) again and again. I am sending the logs as well.
-
0
-
0
Sent you the file (test2dev.localhost.har)
-
0
hi
Try to override the
TokenController
and set breakpoint to check theprincipal
andvar user = await UserManager.GetUserAsync(principal);
The AuthServer can't find a user from
principal
using System.Security.Principal; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Volo.Abp.AspNetCore.Controllers; using Volo.Abp.DependencyInjection; using Volo.Abp.OpenIddict.Controllers; [ExposeServices(typeof(TokenController))] public class MyTokenController : TokenController { protected async override Task<IActionResult> HandleAuthorizationCodeAsync(OpenIddictRequest request) { // Retrieve the claims principal stored in the authorization code/device code/refresh token. var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; using (CurrentTenant.Change(principal.FindTenantId())) { // Retrieve the user profile corresponding to the authorization code/refresh token. // Note: if you want to automatically invalidate the authorization code/refresh token // when the user password/roles change, use the following line instead: // var user = _signInManager.ValidateSecurityStampAsync(info.Principal); var user = await UserManager.GetUserAsync(principal); if (user == null) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." })); } // Ensure the user is still allowed to sign in. if (!await PreSignInCheckAsync(user)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." })); } await OpenIddictClaimsPrincipalManager.HandleAsync(request, principal); // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } } }
-
0
I added this file, and tried to debug,
in this line var user = await UserManager.GetUserAsync(principal);
I am getting the value of user as NULL
I tried to check how the value is coming, it's coming from the the UserManager file that we added, from that file
public override async Task<IdentityUser?> FindByIdAsync(string userId) { ThrowIfDisposed(); return await base.FindByIdAsync(userId); }
From this method, it fetches the User, but when I debugged here, it's returning NULL from this method, so I replaced this code to the following code
public override async Task<IdentityUser?> FindByIdAsync(string userId) { ThrowIfDisposed(); return await _repository.FindByIdAsync(new Guid(userId)); }
Here the _repository is the G1.health.IdentityService.Users.IIdentityUserRepository interface which I have injected in the class. And by using that, it does return the user's details, but still in the
var user = await UserManager.GetUserAsync(principal)
line of the MyTokenController, it comes up as NULL
-
0
hi
Check the current tenant id(
CurrentTenant.Change(principal.FindTenantId()
).Make sure the
tenant id
anduser id
are correct.And what are claims in
principal(var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;)
-
0
Hi, the user Id is coming as fine, but the Tenant Id is coming as NULL So when I call the
var user = await UserManager.GetUserAsync(principal);
method inside the using (CurrentTenant.Change(principal.FindTenantId())) block,
it returns the user value as null, whereas if I call the
var user = await UserManager.GetUserAsync(principal);
method outside that CurrentTenant.Change block, it returns the correct value of the user.
But even if I call the get user method outside the Change Tenant block, it will return to the logged out page of the angular application, and even in the application-configuration api call, the user details are coming fine, but there the roles list is empty.
And yes, here are the list of principals we are getting in the
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
"iss: https://test2dev.localhost:44322/" "exp: 1727250876" "iat: 1727250576" "oi_cl_dstn: {""oi_au_id"":[""access_token""],""preferred_username"":[""access_token"",""id_token""],""sub"":[""access_token""],""email"":[""access_token"",""id_token""],""oi_rsrc"":[""access_token""],""oi_scp"":[""access_token""]}" "sub: 3a0daa97-5fba-2079-563c-3e26309bdc81" "preferred_username: admin" "email: safwan@gmail.com" "AspNet.Identity.SecurityStamp: 6DCVHJYKEIHLAYOAR3SQBAPWS23CADJ5" "oi_scp: offline_access" "oi_scp: openid" "oi_scp: profile" "oi_scp: email" "oi_scp: phone" "oi_scp: AccountService" "oi_scp: IdentityService" "oi_scp: AdministrationService" "oi_scp: SaasService" "oi_scp: ProductService" "oi_scp: ClinicService" "oi_scp: AppointmentService" "oi_scp: FormsService" "oi_rsrc: AccountService" "oi_rsrc: IdentityService" "oi_rsrc: AdministrationService" "oi_rsrc: SaasService" "oi_rsrc: ProductService" "oi_rsrc: ClinicService" "oi_rsrc: AppointmentService" "oi_rsrc: FormsService" "oi_prst: Angular" "oi_reduri: http://test2dev.localhost:4200" "oi_cd_chlg: WuYkSCHygkcNN21XgdHesy-ipW6sguTwenSo-yuwiEQ" "oi_cd_chlg_meth: S256" "oi_nce: YXhXcThfRFBXa2ZtU21KWXhBUWhEd1Bkb0dXcEVzX1MyZXRUN0RMRnIwbC5D" "oi_crt_dt: Wed, 25 Sep 2024 07:49:36 GMT" "oi_exp_dt: Wed, 25 Sep 2024 07:54:36 GMT" "oi_au_id: 3a14ecaf-dae0-d535-4201-7197e6e3092e" "oi_tkn_id: 3a153a55-116d-c6bc-9bc4-a717fb205ede" "oi_tkn_typ: authorization_code"
I am also checking in the changes if you want to have a look
-
0
hi
This means the tenantid and role claims are not set correctly.
Please override the AuthorizeController as well.
Then check the
principal(claims)
in everywhere,https://github.com/abpframework/abp/blob/rel-8.2/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs#L56
https://github.com/abpframework/abp/blob/rel-8.2/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs#L83-L84
https://github.com/abpframework/abp/blob/rel-8.2/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs#L157
https://github.com/abpframework/abp/blob/rel-8.2/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs#L225
https://github.com/abpframework/abp/blob/rel-8.2/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs#L260
-
0
I created the MyAuthorizeController file and this is what it looks like
[Dependency(ReplaceServices = true)] [Route("connect/authorize")] [ApiExplorerSettings(IgnoreApi = true)] [ExposeServices(typeof(AuthorizeController))] public class MyAuthorizeController : AbpOpenIdDictControllerBase
But on running the AuthServer, I am getting this exception :
The type 'G1.health.AuthServer.MyAuthorizeController' is not assignable to service 'Volo.Abp.OpenIddict.Controllers.AuthorizeController'.
-
0
I am able to register the controller now, but now again the same loop is going on, it just keeps redirecting to the angular app's logged out page and I have put breakpoints at
protected async override Task<IActionResult> HandleAuthorizationCodeAsync(OpenIddictRequest request)
and
public override async Task<IActionResult> HandleAsync()
It just keeps hitting at these 2 places continuously, I will share you the logs.