Hi,
Yes that is correct.
Hi,
Just get the code up and running and then create the initial db. Log in as admin (host user) create a tenant (which will create new db) and then log in as that tenant user. Go to the 'companies' link in the left nav bar and you'll see that the current tenant is not set correctly otherwise 2 companies would be showing there.
I've also put the Current Tenant Name and ID on the default page when you log in as a tenant and you'll see they don't contain the specific tenant information.
I suspect that you will not be able to get the code up and running as our solution is complicated, so I think you're best bet is to just look at the code in question (probably the login in logic) and tell me what might be wrong that is causing the CurrentTenant not to be set properly.
This still has not worked. I've given you access to our forked source code where this an issue. The branch name 'Integration-BugFixes' in this forked report is the branch that has the code you want to look at.
Thanks.
Exactly where do I make your suggested change and will this set the current tenant correctly so all queries I’ll to the db will be filtered by that tenant automatically?
I have found that everywhere in my blazor wasm app the current tenant is not being set correctly. No idea why.
Exactly where do I make your suggested change and will this set the current tenant correctly so all queries I’ll to the db will be filtered by that tenant automatically?
Hi,
After realizing that I was missing some code in our AuthServer Module code I now get this but as you can see the Current TenantId is still not getting set.
[10:44:13 FTL] Claim: iss Value: https://localhost.structurecloud.com/Auth
[10:44:13 FTL] Claim: exp Value: 1749037452
[10:44:13 FTL] Claim: iat Value: 1749033852
[10:44:13 FTL] Claim: aud Value: Structure
[10:44:13 FTL] Claim: scope Value: openid profile Structure roles email phone
[10:44:13 FTL] Claim: jti Value: 5c8a9744-9af6-4387-a1d5-deb62c48269b
[10:44:13 FTL] Claim: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier Value: 3a1a4978-8cfc-22db-b223-931eeb528231
[10:44:13 FTL] Claim: preferred_username Value: KENCO_admin
[10:44:13 FTL] Claim: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress Value: spospisil109@gmail.com
[10:44:13 FTL] Claim: tenantid Value: 3a1a4978-6b4a-2eaa-01c7-d5abd68df27f
[10:44:13 FTL] Claim: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname Value: Kenco Admin
[10:44:13 FTL] Claim: phone_number_verified Value: False
[10:44:13 FTL] Claim: email_verified Value: True
[10:44:13 FTL] Claim: session_id Value: 673ea7fd-60d4-4e55-8451-84247357b6a9
[10:44:13 FTL] Claim: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name Value: KENCO_admin
[10:44:13 FTL] Claim: editionid Value: 3a1a4870-79bf-c38d-1e33-3c5a09fffd1e
[10:44:13 FTL] Claim: BrowserInfo Value: 13926292-bf7f-4e71-9684-c6fc7e28a316
[10:44:13 FTL] Claim: oi_prst Value: Structure_Blazor
[10:44:13 FTL] Claim: oi_au_id Value: 3a1a497a-9a71-380a-0d63-c9be9944c3cf
[10:44:13 FTL] Claim: client_id Value: Structure_Blazor
[10:44:13 FTL] Claim: oi_tkn_id Value: 3a1a4cb7-747a-af97-ba5f-7c1531707b55
[10:44:13 FTL] Current User: KENCO_admin CurrentUserTenantId: 3a1a4978-6b4a-2eaa-01c7-d5abd68df27f
[10:44:13 FTL] Current ConnectionStringName: Company CurrentTenantId: IsAvailable: False
[10:44:13 FTL] Connection String Resolved: Server=postgresql;Database=KENCO;Port=5432;User Id=sc_kenco_dba_3a1a49786b3b044130998ef1dd29e3a7;Password=a334!1Sz[-yJ;Ssl Mode=allow;
Here is the code in my connection string resolver class.
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IConnectionStringResolver), typeof(MultiTenantConnectionStringResolver))]
public class SWMultiTenantConnectionStringResolver : MultiTenantConnectionStringResolver
{
private readonly ICurrentTenant _currentTenant;
private readonly ICurrentUser _currentUser;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<SWMultiTenantConnectionStringResolver> _logger;
public SWMultiTenantConnectionStringResolver(
IOptionsMonitor<AbpDbConnectionOptions> options,
ICurrentTenant currentTenant,
IServiceProvider serviceProvider,
ICurrentUser currentUser,
ILogger<SWMultiTenantConnectionStringResolver> logger) : base(options, currentTenant, serviceProvider)
{
_currentTenant = currentTenant;
_currentUser = currentUser;
_serviceProvider = serviceProvider;
_logger = logger;
}
public override async Task<string> ResolveAsync(string? connectionStringName = null)
{
string connectionstring = "";
var claims = _currentUser.GetAllClaims().ToList();
foreach (var claim in claims)
{
_logger.LogCritical($"Claim: {claim.Type} Value: {claim.Value}");
}
_logger.LogCritical($"Current User: {_currentUser.UserName} CurrentUserTenantId: {_currentUser.TenantId}");
_logger.LogCritical($"Current ConnectionStringName: {connectionStringName} CurrentTenantId: {_currentTenant.Id} IsAvailable: {_currentTenant.IsAvailable}");
//Force Current Tenant to write to Host DB for the below functions
switch (connectionStringName)
{
case "Default":
case LanguageManagementDbProperties.ConnectionStringName:
case AbpFeatureManagementDbProperties.ConnectionStringName:
case AbpPermissionManagementDbProperties.ConnectionStringName:
case AbpOpenIddictDbProperties.ConnectionStringName:
case AbpBackgroundJobsDbProperties.ConnectionStringName:
case AbpBlobStoringDatabaseDbProperties.ConnectionStringName:
case AbpIdentityDbProperties.ConnectionStringName:
case AbpCmsKitDbProperties.ConnectionStringName:
case AbpSettingManagementDbProperties.ConnectionStringName:
case SaasDbProperties.ConnectionStringName:
case ChatDbProperties.ConnectionStringName:
case TextTemplateManagementDbProperties.ConnectionStringName:
case PaymentDbProperties.ConnectionStringName:
case TemplateDbProperties.ConnectionStringName:
case SecurityDbProperties.ConnectionStringName:
case LexiconDbProperties.ConnectionStringName:
case LicensingDbProperties.ConnectionStringName:
using (_currentTenant.Change(null))
{
connectionstring = await base.ResolveAsync(connectionStringName);
_logger.LogCritical($"Connection String Resolved: {connectionstring}");
return await Task.FromResult(connectionstring);
}
default:
if (_currentTenant.Id is null || _currentUser.TenantId is not null)
{
using (_currentTenant.Change(_currentUser.TenantId))
{
connectionstring = await base.ResolveAsync(connectionStringName);
}
}
else
{
connectionstring = await base.ResolveAsync(connectionStringName);
}
_logger.LogCritical($"Connection String Resolved: {connectionstring}");
return await Task.FromResult(connectionstring);
}
}
Here is my code in my auth server module
private static void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication()
.AddGoogle(GoogleDefaults.AuthenticationScheme, _ => { })
.WithDynamicOptions<GoogleOptions, GoogleHandler>(
GoogleDefaults.AuthenticationScheme,
options =>
{
options.WithProperty(x => x.ClientId);
options.WithProperty(x => x.ClientSecret, isSecret: true);
}
)
.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
{
//Personal Microsoft accounts as an example.
options.AuthorizationEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
options.TokenEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
})
.WithDynamicOptions<MicrosoftAccountOptions, MicrosoftAccountHandler>(
MicrosoftAccountDefaults.AuthenticationScheme,
options =>
{
options.WithProperty(x => x.ClientId);
options.WithProperty(x => x.ClientSecret, isSecret: true);
}
)
.AddTwitter(TwitterDefaults.AuthenticationScheme, options => options.RetrieveUserDetails = true)
.WithDynamicOptions<TwitterOptions, TwitterHandler>(
TwitterDefaults.AuthenticationScheme,
options =>
{
options.WithProperty(x => x.ConsumerKey);
options.WithProperty(x => x.ConsumerSecret, isSecret: true);
}
)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
options.Audience = "Structure";
});
}
Hi,
Here are the claims associated with the current logged in tenant user
[09:55:10 FTL] Claim: email_verified Value: True [09:55:10 FTL] Claim: phone_number_verified Value: False [09:55:10 FTL] Claim: editionid Value: 3a1a4870-79bf-c38d-1e33-3c5a09fffd1e [09:55:10 FTL] Claim: client_id Value: Structure_Blazor [09:55:10 FTL] Claim: oi_tkn_id Value: 3a1a4c89-cfb7-9302-f739-76a2df5e99ea
The following shows the current user, current user TenantId (which is correct), but then the CurrentTenantId is null and the IsAvailable is False.
[09:55:10 FTL] Current User: KENCO_admin CurrentUserTenantId: 3a1a4978-6b4a-2eaa-01c7-d5abd68df27f [09:55:10 FTL] Current ConnectionStringName: Company CurrentTenantId: IsAvailable: False [09:55:10 FTL] Connection String Resolved: Server=postgresql;Database=KENCO;Port=5432;User Id=sc_kenco_dba_3a1a49786b3b044130998ef1dd29e3a7;Password=a334!1Sz[-yJ;Ssl Mode=allow;
Can you tell me the things I need to check? Again the issue seems to be in any abp layered modules that I create the currenttenant is always null/blank even though I'm logged in as the tenant? Since I can't reproduce this issue I need guidance from ABP in order to check what might be wrong.
It's been 3 weeks of solely focused on this and I still have no resolution.
Hi,
See my below code for the login. I have verified at least with my custom login page that the resolver exists as it should. Is there something I am doing wrong with replacing the login logic?
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Owl.reCAPTCHA;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Account.ExternalProviders;
using Volo.Abp.Account.Public.Web;
using Volo.Abp.Account.Public.Web.Pages.Account;
using Volo.Abp.Account.Public.Web.Security.Recaptcha;
using Volo.Abp.Account.Security.Recaptcha;
using Volo.Abp.Account.Settings;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Identity.Settings;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
using Volo.Abp.Security.Claims;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
using Volo.Abp.Validation;
using Volo.Saas.Tenants;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace CFData.Structure.Web.Pages.Account;
[DisableAuditing]
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(MyLoginModel), typeof(LoginModel))]
public class MyLoginModel : LoginModel
{
[Inject]
public ICurrentTenantAccessor CurrentTenantAccessor { get; set; }
[Inject]
public ITenantStore TenantStore { get; set; }
private readonly ITenantRepository _tenantRepository;
protected IDataFilter DataFilter { get; }
public MyLoginModel(
IAuthenticationSchemeProvider schemeProvider,
IOptions<AbpAccountOptions> accountOptions,
IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
IAccountExternalProviderAppService accountExternalProviderAppService,
ICurrentPrincipalAccessor currentPrincipalAccessor,
IOptions<IdentityOptions> identityOptions,
IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions,
ITenantRepository tenantRepository,
DataFilter dataFilter) : base(
schemeProvider,
accountOptions,
recaptchaValidatorFactory,
accountExternalProviderAppService,
currentPrincipalAccessor,
identityOptions,
reCaptchaOptions)
{
//ReCaptchaOptions = reCaptchaOptions;
_tenantRepository = tenantRepository;
DataFilter = dataFilter;
}
public override async Task<IActionResult> OnGetAsync()
{
return await base.OnGetAsync();
}
[UnitOfWork] //TODO: Will be removed when we implement action filter
public override async Task<IActionResult> OnPostAsync(string action)
{
try
{
await ReCaptchaVerification();
}
catch (UserFriendlyException e)
{
if (e is ScoreBelowThresholdException)
{
var onScoreBelowThresholdResult = OnRecaptchaScoreBelowThreshold();
if (onScoreBelowThresholdResult != null)
{
return await onScoreBelowThresholdResult;
}
}
Alerts.Danger(GetLocalizeExceptionMessage(e));
return Page();
}
//ValidateModel();
await IdentityOptions.SetAsync();
var localLoginResult = await CheckLocalLoginAsync();
if (localLoginResult != null)
{
return localLoginResult;
}
IsSelfRegistrationEnabled = false; //await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled);
await ReplaceEmailToUsernameOfInputIfNeeds();
var getUser = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
if (getUser == null)
{
Alerts.Danger("User does not exist");
return Page();
}
TenantConfiguration tenant = null;
if (getUser.TenantId is not null)
{
tenant = await TenantStore.FindAsync((Guid)getUser.TenantId);
CurrentTenantAccessor.Current = new BasicTenantInfo(
tenant?.Id,
tenant?.Name);
}
IsLinkLogin = await VerifyLinkTokenAsync();
var result = new Microsoft.AspNetCore.Identity.SignInResult();
using (DataFilter.Disable<IMultiTenant>())
{
result = await SignInManager.PasswordSignInAsync(
LoginInput.UserNameOrEmailAddress,
LoginInput.Password,
LoginInput.RememberMe,
true);
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = result.ToIdentitySecurityLogAction(),
UserName = LoginInput.UserNameOrEmailAddress
});
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./SendSecurityCode", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = LoginInput.RememberMe,
linkUserId = LinkUserId,
linkTenantId = LinkTenantId,
linkToken = LinkToken
});
}
if (result.IsLockedOut)
{
var lockedUser = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
await StoreLockedUser(lockedUser);
return RedirectToPage("./LockedOut", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash
});
}
if (result.IsNotAllowed)
{
var notAllowedUser = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
if (!await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password))
{
Alerts.Danger(L["InvalidUserNameOrPassword"]);
return Page();
}
if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser))
{
await StoreChangePasswordUser(notAllowedUser);
return RedirectToPage("./ChangePassword", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
RememberMe = LoginInput.RememberMe
});
}
if (notAllowedUser.IsActive)
{
await StoreConfirmUser(notAllowedUser);
return RedirectToPage("./ConfirmUser", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash
});
}
Alerts.Danger(L["LoginIsNotAllowed"]);
return Page();
}
if (!result.Succeeded)
{
Alerts.Danger(L["InvalidUserNameOrPassword"]);
return Page();
}
var user = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
if (IsLinkLogin)
{
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
{
await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentityProSecurityLogActionConsts.LinkUser,
UserName = user.UserName,
ExtraProperties =
{
{ IdentityProSecurityLogActionConsts.LinkTargetTenantId, LinkTenantId },
{ IdentityProSecurityLogActionConsts.LinkTargetUserId, LinkUserId }
}
});
using (CurrentTenant.Change(LinkTenantId))
{
var targetUser = await UserManager.GetByIdAsync(LinkUserId.Value);
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(targetUser)))
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentityProSecurityLogActionConsts.LinkUser,
UserName = targetUser.UserName,
ExtraProperties =
{
{ IdentityProSecurityLogActionConsts.LinkTargetTenantId, targetUser.TenantId },
{ IdentityProSecurityLogActionConsts.LinkTargetUserId, targetUser.Id }
}
});
}
}
return RedirectToPage("./LinkLogged", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
targetLinkUserId = LinkUserId,
targetLinkTenantId = LinkTenantId
});
}
}
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
protected override async Task ReplaceEmailToUsernameOfInputIfNeeds()
{
if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress))
{
return;
}
var userByUsername = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
if (userByUsername != null)
{
return;
}
var userByEmail = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
if (userByEmail == null)
{
return;
}
LoginInput.UserNameOrEmailAddress = userByEmail.UserName;
}
protected override async Task<IdentityUser> GetIdentityUser(string userNameOrEmailAddress)
{
IdentityUser identityUser = null;
using (CurrentTenant.Change(null))
{
identityUser = await base.UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress);
if (identityUser == null)
{
identityUser = await base.UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
}
}
IdentityUser user = identityUser;
Debug.Assert(user != null, "user != null");
return user;
}
protected virtual async Task<IdentityUser> FindUserAsync(string uniqueUserNameOrEmailAddress)
{
IdentityUser user = null;
using (CurrentTenant.Change(null))
{
user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
if (user != null)
{
return user;
}
}
foreach (var tenant in await _tenantRepository.GetListAsync())
{
using (CurrentTenant.Change(tenant.Id))
{
user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
if (user != null)
{
return user;
}
}
}
return null;
}
}