Having that configuration I found two possible security issues: Steps to reproduce: - I'm logged in as user from tenant1 so my portal domain is tenant1-portal.com - I modify URL in browser to tenant2-portal.com - I'm redirected to portal main page with domain tenant2-portal.com - when I click Login button I'm automatically logged in, still as a tenant1 user but with url tenant2-portal.com That's the first issue. - then when I click Log out button it looks like I'm logged out out and redirected to portal main page - domain tenant2-portal.com - set URL to tenant1-portal.com - try to login - user is automatically logged in as tenant1 user (even though I logged out in previous steps) That's the second issue. Do you have any possible solutions for these issues?
Now everything works fine :) I've got one question about ABP/LeptopX future updates. Now when I've got modified Default.cshtml file in my solution the only way to keep that file up to date is to compare it with newest LeptopX source? There is no automation possible right?
You can try this
Yes, this code correctly adds _tenant parameter, that's half of the success :)
It can hide the tenant field; you can give it a try.
I put that razor code in correct location and now that version of code is displayed during login process. Unfortunately there are still 2 issues: 1. "Login" button is always disabled, even when I provide both user name and password. 2. Logo above login form is not displayed at all. There is only application name displayed but it looks like it's not affected by css style.
it works for me.
It works only when you are already on the login page and you modify the URL but when you are redirected from the portal page then it doesn't work for host (empty tenant). As I previously mentioned I'm using code below to correctly redirect user from portal to the auth server. PortalBlazorModule.cs
options.Events.OnRedirectToIdentityProviderForSignOut = redirectContext =>
{
var currentTenant = redirectContext.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
redirectContext.ProtocolMessage.IssuerAddress += $"?__tenant={currentTenant?.Id}";
return Task.CompletedTask;
};
You can override the account layout page.
I'm using app-pro template, so probably the code that you provided won't work to hide the tenant field on login page?
I've already tried and I don't know how to properly redirect user from portal to AuthServer. I tried to use __tenant parameter, so in PortalBlazorModule.cs i've added:
options.Events.OnRedirectToIdentityProviderForSignOut = redirectContext =>
{
var currentTenant = redirectContext.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
redirectContext.ProtocolMessage.IssuerAddress += $"?__tenant={currentTenant?.Id}";
return Task.CompletedTask;
};
But: - It does not hide Tenant field on login page (only enabling domain tenant resolver hides that field). Tenant is correctly selected but field is still visible. - It does not work with host. __tenant is sent as empty value and then previously selected tenant is selected not the empty tenant/value - host.
I have a question regarding domain/subdomain tenant resolving. Docs (https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver) say that we have to configure portal, api and auth subdomains for each tenant which means that we will use 3 subdomains for every tenant. Is it possible to use less subdomains to achive this kind of resolving? For example we might want to have main portal with subdomains like {tenantName}.portal.com but both API and AuthServer would have only one domain configured like api.com and auth.com. I'm asking about that because on Azure there is a limit of 250 subdomains, so we'd like to use as less domains as it could be for one tenant. If it's somehow possible please feel free to share with some configuration/code examples.
I'm trying to configure domain/subdomain tenant resolving. I'd like to do it on local environment and test it. I saw documentation about that: https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver and I did all needed changes but with no success.
Let's start to configure that on a new project.
abp new MainPortal -t app-pro -u blazor-server -d ef -csf --tiered
127.0.0.1 mydomain.com
127.0.0.1 tenant1.mydomain.com
public override void PreConfigureServices(ServiceConfigurationContext context)
...
if (context.Services.GetHostingEnvironment().IsDevelopment())
{
PreConfigure<AbpHttpClientBuilderOptions>(options =>
{
options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) =>
{
clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
ServerCertificateCustomValidationCallback =
(httpRequestMessage, cert, cetChain, policyErrors) =>
{
return true;
}
});
});
});
}
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
...
.AddAbpOpenIdConnect("oidc", options =>
{
...
if (context.Services.GetHostingEnvironment().IsDevelopment())
{
options.BackchannelHttpHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
}
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddAbpJwtBearer(options =>
...
options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
options.TokenValidationParameters.ValidIssuers = new[]
{
"https://{0}.mydomain.com:44320/",
"https://mydomain.com:44320/" // i had to add this line in order to make it work even though I set TokenWildcardIssuerValidator
};
if (context.Services.GetHostingEnvironment().IsDevelopment())
{
options.TokenValidationParameters.ValidateIssuerSigningKey = false;
options.TokenValidationParameters.SignatureValidator = delegate (string token, TokenValidationParameters parameters)
{
return new JsonWebToken(token);
};
}
I have no idea why TokenWildcardIssuerValidator does not correctly valid issuers and I had to add it manually - that's the important thing that is different than docs said.
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
...
.AddAbpOpenIdConnect("oidc", options =>
...
options.Events.OnRedirectToIdentityProviderForSignOut = redirectContext =>
{
var currentTenant = redirectContext.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
if (currentTenant.IsAvailable)
{
redirectContext.ProtocolMessage.IssuerAddress =
redirectContext.ProtocolMessage.IssuerAddress.Replace("https://", $"https://{currentTenant.Name}.");
}
return Task.CompletedTask;
};
options.Events.OnRedirectToIdentityProvider = options.Events.OnRedirectToIdentityProviderForSignOut;
SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'https://tenant1.mydomain.com:44320/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'null' or validationParameters.ConfigurationManager.CurrentConfiguration.Issuer: 'https://mydomain.com:44320/'
Port 44320 is assigned to the auth server. In my opinion there is something wrong with configuration - auth server probably should set issuer always as "'https://mydomain.com:44320/'" so without tenant name. In the mvc example (https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver/MVC-TIERED) in PortalAuthServerModule.cs there is a configuration which is probably responsible for this:
Configure<IdentityServerOptions>(options =>
{
options.IssuerUri = configuration["App:SelfUrl"];
});
But as I said example is for mvc app and identity server and i've got blazor server and openid.
In my opinion something important is missing in the docs: https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver especially when I looked on example: https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver/MVC-TIERED
I would be really grateful for any help to solve that issue and make Domain/Subdomain Tenant Resolver work :)
CLI (ABP CLI 8.2.0): abp new MainPortal -t app-pro -u blazor-server-d ef -csf --tiered
I added a new "TestPermission" in MainPortalPermissionDefinitionProvider.cs
public class MainPortalPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup(MainPortalPermissions.GroupName);
myGroup.AddPermission(MainPortalPermissions.Dashboard.Host, L("Permission:Dashboard"), MultiTenancySides.Host);
myGroup.AddPermission(MainPortalPermissions.Dashboard.Tenant, L("Permission:Dashboard"), MultiTenancySides.Tenant);
myGroup.AddPermission("TestPermission");
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<MainPortalResource>(name);
}
}
The permission is assigned to admin role:
I'd like to return user permissions in AccessToken. As I've got separate AuthServer I added there a code presented below:
public class PermissionsClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
{
public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
{
var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
var userId = identity?.FindUserId();
if (userId.HasValue)
{
var permissionManager = context.ServiceProvider.GetRequiredService<IPermissionManager>();
var userPermissions = await permissionManager.GetAllForUserAsync(userId.Value);
// only 46 permissions returned - without "TestPermission"
var rolePermissions = await permissionManager.GetAllForRoleAsync("admin");
// only 46 permissions returned - without "TestPermission"
var permissionDefinitionManager = context.ServiceProvider.GetRequiredService<IPermissionDefinitionManager>();
var allPermissions = await permissionDefinitionManager.GetPermissionsAsync();
// only 46 permissions returned - without "TestPermission"
var allGroups = await permissionDefinitionManager.GetGroupsAsync();
// only 3 permission groups returned - without "MainPortal" group
// AbpIdentity, FeatureManagement, Saas
}
}
}
As comments say there is no way to get my TestPermission assigned to admin role. I can only get 3 groups and all permissions assigned to those groups.
I've tried to use
Configure<PermissionManagementOptions>(options =>
{
options.IsDynamicPermissionStoreEnabled = true;
});
And this way I was able to get all permissions (including TestPermission):
var allPermissions = await permissionDefinitionManager.GetPermissionsAsync();
All 75 permissions were returned.
But when I try to get permissions assigned to current user:
var userPermissions = await permissionManager.GetAllForUserAsync(userId.Value);
I've got an exception:
AbpException: Undefined feature: AuditLogging.Enable
Volo.Abp.Features.FeatureDefinitionManager.GetAsync(string name)
Volo.Abp.Features.FeatureChecker.GetOrNullAsync(string name)
Volo.Abp.Features.FeatureCheckerBase.IsEnabledAsync(string name)
Volo.Abp.Features.FeatureCheckerExtensions.IsEnabledAsync(IFeatureChecker featureChecker, bool requiresAll, string[] featureNames)
Volo.Abp.Features.RequireFeaturesSimpleStateChecker<TState>.IsEnabledAsync(SimpleStateCheckerContext<TState> context)
Volo.Abp.SimpleStateChecking.SimpleStateCheckerManager<TState>.InternalIsEnabledAsync(TState state, bool useBatchChecker)
Volo.Abp.SimpleStateChecking.SimpleStateCheckerManager<TState>.IsEnabledAsync(TState state)
Volo.Abp.PermissionManagement.PermissionManager.GetInternalAsync(PermissionDefinition[] permissions, string providerName, string providerKey)
Volo.Abp.PermissionManagement.PermissionManager.GetAllAsync(string providerName, string providerKey)
MainPortal.PermissionsClaimsPrincipalContributor.ContributeAsync(AbpClaimsPrincipalContributorContext context) in MainPortalAuthServerModule.cs
var userPermissions = await permissionManager.GetAllForUserAsync(userId.Value);
Volo.Abp.Security.Claims.AbpClaimsPrincipalFactory.InternalCreateAsync(AbpClaimsPrincipalFactoryOptions options, ClaimsPrincipal existsClaimsPrincipal, bool isDynamic)
Volo.Abp.Security.Claims.AbpClaimsPrincipalFactory.CreateAsync(ClaimsPrincipal existsClaimsPrincipal)
Volo.Abp.Identity.AbpUserClaimsPrincipalFactory.CreateAsync(IdentityUser user)
Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo)
Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue<TResult>.ProceedAsync()
Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter<TInterceptor>.InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed)
Volo.Abp.Identity.IdentityDynamicClaimsPrincipalContributorCache+<>c__DisplayClass23_0+<<GetAsync>b__0>d.MoveNext()
Volo.Abp.Caching.DistributedCache<TCacheItem, TCacheKey>.GetOrAddAsync(TCacheKey key, Func<Task<TCacheItem>> factory, Func<DistributedCacheEntryOptions> optionsFactory, Nullable<bool> hideErrors, bool considerUow, CancellationToken token)
Volo.Abp.Identity.IdentityDynamicClaimsPrincipalContributorCache.GetAsync(Guid userId, Nullable<Guid> tenantId)
Volo.Abp.Identity.IdentityDynamicClaimsPrincipalContributor.ContributeAsync(AbpClaimsPrincipalContributorContext context)
Volo.Abp.Security.Claims.AbpClaimsPrincipalFactory.InternalCreateAsync(AbpClaimsPrincipalFactoryOptions options, ClaimsPrincipal existsClaimsPrincipal, bool isDynamic)
Volo.Abp.Security.Claims.AbpClaimsPrincipalFactory.CreateDynamicAsync(ClaimsPrincipal existsClaimsPrincipal)
Volo.Abp.AspNetCore.Security.Claims.AbpDynamicClaimsMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Volo.Abp.AspNetCore.Uow.AbpUnitOfWorkMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Volo.Abp.AspNetCore.MultiTenancy.MultiTenancyMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Microsoft.AspNetCore.Builder.ApplicationBuilderAbpOpenIddictMiddlewareExtension+<>c__DisplayClass0_0+<<UseAbpOpenIddictValidation>b__0>d.MoveNext()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Volo.Abp.AspNetCore.Security.AbpSecurityHeadersMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Volo.Abp.AspNetCore.Tracing.AbpCorrelationIdMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.RequestLocalization.AbpRequestLocalizationMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
I would be grateful for your help and analyze whether it's a bug or I missed something.