Open Closed

Configure external providers on tenant level #9034


User avatar
0
LW created

How to configure different external login providers for different tenants? Based on this answer https://github.com/abpframework/abp/discussions/19743, I suppose this is possible somehow but there is no place in the tenant side to configure this. Please provide instructions, how can we configure this.
image.png
image.png


36 Answer(s)
  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi, after you have configured external providers for the host side:

    image.png

    Then, you can see the "external provider" tab in the settings -> account section for the tenant (you may need to login as the admin user of the related tenant):

    image.png

    As you can see from the figure above, "amazon" is the tenant name and the "admin" is the username of the tenant admin, and it's possible to configure the client-id and client-secret for the related external provider.

    • You can use the host settings by checking the Use host settings checkbox,

    • Or override the client-id and client-secret per tenant

  • User Avatar
    0
    LW created

    Thanks for the answer!
    We are aiming to enable Microsoft Entra Id SSO login for some of our customers, that use our (Abp) application. How is that going to work when we cannot configure the azure tenant in the tenant's external login provider configuration? If only the client id and the secret are configurable, how can the customer point the configuration to their Microsoft Entra Id?

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Thanks for the answer!
    We are aiming to enable Microsoft Entra Id SSO login for some of our customers, that use our (Abp) application. How is that going to work when we cannot configure the azure tenant in the tenant's external login provider configuration? If only the client id and the secret are configurable, how can the customer point the configuration to their Microsoft Entra Id?

    You're right to point out that simply configuring Client ID and Secret might not be enough for Microsoft Entra ID (now known as Microsoft Entra ID, formerly Azure AD) in a multi-tenant scenario. Each customer using their own Microsoft Entra ID will indeed have a unique Tenant ID (also known as Directory ID) that your application needs to target for authentication.

    For that purpose, you should implement dynamic configuration using ICoonfigureOptions<>. Here is what you can do:

    1. Implement IConfigureOptions<OpenIdConnectOptions> for Azure AD:

    Since Microsoft Entra ID uses the OpenID Connect protocol, you'll need to implement IConfigureOptions<OpenIdConnectOptions>.

    public class AzureAdTenantOptionsProvider : IConfigureOptions<OpenIdConnectOptions>, ITransientDependency
    {
        private readonly ICurrentTenant _currentTenant;
        private readonly ITenantAzureAdSettingsService _tenantAzureAdSettingsService; //NOTE: you need to implement this service
    
        public AzureAdTenantOptionsProvider(ICurrentTenant currentTenant, ITenantAzureAdSettingsService tenantAzureAdSettingsService)
        {
            _currentTenant = currentTenant;
            _tenantAzureAdSettingsService = tenantAzureAdSettingsService;
        }
    
        public void Configure(OpenIdConnectOptions options)
        {
            if (_currentTenant.Id.HasValue)
            {
                var tenantId = _currentTenant.Id.Value;
                var azureAdSettings = _tenantAzureAdSettingsService.GetAzureAdSettings(tenantId); // Implement this
    
                if (azureAdSettings != null && azureAdSettings.IsEnabled)
                {
                    options.ClientId = azureAdSettings.ClientId;
                    options.ClientSecret = azureAdSettings.ClientSecret;
                    options.Authority = $"https://login.microsoftonline.com/{azureAdSettings.TenantId}/v2.0";
                    options.ResponseType = "code"; // Or your preferred response type
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    // Add other necessary options as per your requirements
                }
                else
                {
                    // Or configure default behavior
                }
            }
            else
            {
                // Configure default Azure AD settings for the host if needed
            }
        }
    }
    
    1. Register the option dynamically in your module class (inside ConfigureServices method):

    services.ConfigureOptions<AzureAdTenantOptionsProvider>();
    

    Note that, this requires additional implementation on your side and in the above example, I have just tried to provide you an approach. Since this is not fully related to ABP, you may need to check additional articles on the web.


    In summary, to enable Microsoft Entra ID SSO login for different tenants:

    • You need to implement a dynamic option as I have described above, then create a service that returns tenantId from each customer for their Azure AD.

    • Our current system, only allows single-tenant configuration for EntraID, but by providing dynamic options, you can implement according to your business.

    Regards.

  • User Avatar
    0
    LW created

    Thanks, we will try to implement this.
    How the "AddOpenIdConnect" part should go in this case, where the configuration is defined dynamically? Here is a general example how that normally goes.

            context.Services.AddAuthentication()
                .AddOpenIdConnect("ABP2AzureADScheme", "Logon with Azure AD", options =>
                {
                    
                    options.Authority = configuration["AzureAd:Instance"] + configuration["AzureAd:TenantId"] + "/v2.0/";
                    options.ClientId = configuration["AzureAd:ClientId"];
                    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                    options.CallbackPath = configuration["AzureAd:CallbackPath"];
                    options.ClientSecret = configuration["AzureAd:ClientSecret"];
                    options.RequireHttpsMetadata = false;
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.Scope.Add("email");
    
                    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    
                });
    
  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Thanks, we will try to implement this.
    How the "AddOpenIdConnect" part should go in this case, where the configuration is defined dynamically? Here is a general example how that normally goes.

            context.Services.AddAuthentication() 
                .AddOpenIdConnect("ABP2AzureADScheme", "Logon with Azure AD", options => 
                { 
                     
                    options.Authority = configuration["AzureAd:Instance"] + configuration["AzureAd:TenantId"] + "/v2.0/"; 
                    options.ClientId = configuration["AzureAd:ClientId"]; 
                    options.ResponseType = OpenIdConnectResponseType.CodeIdToken; 
                    options.CallbackPath = configuration["AzureAd:CallbackPath"]; 
                    options.ClientSecret = configuration["AzureAd:ClientSecret"]; 
                    options.RequireHttpsMetadata = false; 
                    options.SaveTokens = true; 
                    options.GetClaimsFromUserInfoEndpoint = true; 
                    options.Scope.Add("email"); 
     
                    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); 
     
                }); 
    

    Hi, actually this part can stay exactly like this. Since you will implement the IConfigureOptions<> it will update the configuration dynamically.

  • User Avatar
    0
    LW created

    OK, thanks!

  • User Avatar
    0
    JanneHarju created

    Hello,
    I'm collegue of LW and started to implement this feature. Now I have several problems for which I need your help.
    First problem is about AzureEntraIdTenantOptionsProvider. I managed to implement it and when debuging I noticed that it goes to constructor of provider but never goes to Configure method so authserver is not even trying to get dynamic OpenIdConnectOptions from provider and uses what was in appsettings.json.

    Other problem is when trying to use Azure AD button. I managed to configure Entra Id login settings to our appsetting.json from where AddOpenIdConnect is getting default settings. And it redirects to microsoft site and after awile it goes back to register page as seen below. Is this normal routine when using external identity provider? How can I get Register button enable if it is required to get external login to work.
    output.gif

    And last notice is that I only get login to work this much when I se this setting to None. It doesn't feel right thing to do. But is there any other way?
    options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;

  • User Avatar
    0
    JanneHarju created

    Forget to give you my ConfigureServices code. It is quite same as you suggested and what there was earlier.

    context.Services.AddOptions()
    .ConfigureOptions<AzureEntraIdTenantOptionsProvider>();
    var authenticationBuilder = context.Services.AddAuthentication();
    authenticationBuilder.AddOpenIdConnect("AzureOpenId", "Azure AD", options =>
    {
        options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/";
        options.ClientId = configuration["AzureAd:ClientId"];
        options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
        options.CallbackPath = configuration["AzureAd:CallbackPath"];
        options.ClientSecret = configuration["AzureAd:ClientSecret"];
        options.RequireHttpsMetadata = false;
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("email");
        
        options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    });
    

    And here is out provider. We assumed that currentTenant is set here so our global ef filter should work here and tenantId parameter is added to query automaticly.

    public class AzureEntraIdTenantOptionsProvider : IConfigureOptions, ITransientDependency
    	{
    		private readonly IServiceProvider _serviceProvider;
    
    
    		public AzureEntraIdTenantOptionsProvider(IServiceProvider serviceProvider)
    		{
    			_serviceProvider = serviceProvider;
    		}
    
    		public void Configure(OpenIdConnectOptions options)
    		{
    			ISaasTenantIdentityProviderSettingsRepository saasTenantIdentityProviderSettingsService = _serviceProvider.GetRequiredService();
    			var azureAdSettings = saasTenantIdentityProviderSettingsService.GetIdentityProviderSettings(IdentityProviderType.ENTRA_ID).Result;
    			var deserializedSettings = azureAdSettings?.Setting != null
    				? JsonConvert.DeserializeObject(azureAdSettings?.Setting)
    				: null;
    
    			if (azureAdSettings != null && deserializedSettings.IsEnabled)
    			{
    				options.ClientId = deserializedSettings.ClientId;
    				options.ClientSecret = deserializedSettings.ClientSecret;
    				options.Authority = $"https://login.microsoftonline.com/{deserializedSettings.AzureTenantId}/v2.0";
    				options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    				options.SaveTokens = true;
    				options.GetClaimsFromUserInfoEndpoint = true;
    				options.CallbackPath = "/signin-azure";
    				options.RequireHttpsMetadata = false;
    				options.Scope.Add("email");
    				options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    			}
    			else
    			{
    				// Or configure default behavior
    				throw new UserFriendlyException("Azure Ad is not configured for you");
    			}
    		}
    	}
    
  • User Avatar
    0
    JanneHarju created
  • User Avatar
    0
    JanneHarju created

    After I found correct place to enable self registration I managed to get login working with static appsettings.json settings . So now we need to get dynamic config working.

  • User Avatar
    0
    JanneHarju created

    After talking with my team enabling registration is not good for us. So is there possibility to get it working like making "registration" automatically if user is logged in through Azure Entra Id?

  • User Avatar
    0
    LW created

    Can you please shed some light to the problems that we are facing. The main problem is that the dynamic configuration service's "Configure" method is never called. How is the structure supposed to work? When we are debugging the login, when is the execution supposed to call the configure method: when the tenant is changes from the login page **or ** when the "Login with Azure AD" -button is clicked? As the configure is called in neither case, what could be the problem here?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    This is because IOptions<..> will cache the option instance. So it will not trigger every time.

    You can try another way:

    .AddOpenIdConnect("AzureOpenId", "Azure AD", options =>
    {
        options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/";
        options.ClientId = configuration["AzureAd:ClientId"];
        options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
        options.CallbackPath = configuration["AzureAd:CallbackPath"];
        options.ClientSecret = configuration["AzureAd:ClientSecret"];
        options.RequireHttpsMetadata = false;
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("email");
        
        options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    
        // this event will call every time when redirect to azure.
        options.Events.OnRedirectToIdentityProvider = async redirectContext =>
        {
            redirectContext.ProtocolMessage.ClientId = "xxxx";
            redirectContext.ProtocolMessage.ClientSecret = "xxx";
        };
    })
    
  • User Avatar
    0
    JanneHarju created
    1. I only get login to work when I set this setting to None. It doesn't feel right thing to do. But is there any other way?
      options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;

    2. Is there possibility to get login working without enabling self-registration when using Azure Entra Id?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    I only get login to work when I set this setting to None. It doesn't feel right thing to do. But is there any other way?
    options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;

    Are you using HTTPS?

    Is there possibility to get login working without enabling self-registration when using Azure Entra Id?

    Yes, you can disable self-registration, ABP will automatically register external users.

  • User Avatar
    0
    JanneHarju created

    Now I managed to get login to work but now token validation fails. It seems that it validates with old ClientId value and token contains updated clientId as it should. Here is error message what I received.

    SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: 'b7de466e-7846-45c4-bc91-099f3c89fdff'. Did not match: validationParameters.ValidAudience: '275a3f7d-b568-4865-92a5-6bf5df627e46' or validationParameters.ValidAudiences: 'null'.
    Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ValidateTokenUsingHandlerAsync(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters)
    Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
    
    Show raw exception details
    AuthenticationFailureException: An error was encountered while handling the remote login.
    Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
    Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
    Volo.Abp.AspNetCore.Tracing.AbpCorrelationIdMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
    Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<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+<b__0>d.MoveNext()
    Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
    

    And here is current solution what works this much.

    authenticationBuilder.AddOpenIdConnect("AzureOpenId", "Azure AD", options =>
    			{
    				options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/";
    				options.ClientId = configuration["AzureAd:ClientId"];
    				options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    				options.SignInScheme = IdentityConstants.ExternalScheme;
    				options.CallbackPath = "/signin-azuread-oidc";
    				options.RequireHttpsMetadata = false;
    				options.SaveTokens = true;
    				options.GetClaimsFromUserInfoEndpoint = true;
    				options.Scope.Add("email");
    				options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    				EfCoreSaasTenantIdentityProviderSettingRepository saasTenantIdentityProviderSettingsService =
    					context.Services.GetRequiredService();
    				options.Events.OnTokenResponseReceived = async receivedContext =>
    				{
    					await Task.FromResult("");
    				};
    				options.Events.OnRedirectToIdentityProvider = async redirectContext =>
    				{
    					var azureAdSettings =
    						await saasTenantIdentityProviderSettingsService.GetIdentityProviderSettings(IdentityProviderType.ENTRA_ID);
    					var deserializedSettings = azureAdSettings?.Setting != null
    						? JsonConvert.DeserializeObject(azureAdSettings?.Setting)
    						: null;
    
    					if (azureAdSettings != null && deserializedSettings.IsEnabled)
    					{
    						redirectContext.ProtocolMessage.ClientId = deserializedSettings.ClientId;
    						redirectContext.ProtocolMessage.ClientSecret = deserializedSettings.ClientSecret;
    						redirectContext.ProtocolMessage.IssuerAddress =
    							redirectContext.ProtocolMessage.IssuerAddress.Replace(configuration["AzureAd:TenantId"],
    								deserializedSettings.AzureTenantId);
    
    						redirectContext.Options.ClientId = deserializedSettings.ClientId;
    						redirectContext.Options.ClientSecret = deserializedSettings.ClientSecret;
    						redirectContext.Options.Authority = redirectContext.Options.Authority.Replace(configuration["AzureAd:TenantId"],
    							deserializedSettings.AzureTenantId);
    						redirectContext.Options.MetadataAddress = redirectContext.Options.MetadataAddress.Replace(
    							configuration["AzureAd:TenantId"],
    							deserializedSettings.AzureTenantId);
    					}
    				};
    			});
    

    Should I implement some other Event listener for token validation?
    I tried to implement OnTokenResponseReceived but exception occures before it goes there.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can try:

    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidAudiences = new[] { "....", "...." }// add all audiences that you want to validate
    };
    
  • User Avatar
    0
    JanneHarju created

    Thanks I get further. Here is new code:

    authenticationBuilder.AddOpenIdConnect("AzureOpenId", "Azure AD", options =>
    			{
    				options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/";
    				options.ClientId = configuration["AzureAd:ClientId"];
    				options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    				options.SignInScheme = IdentityConstants.ExternalScheme;
    				options.CallbackPath = "/signin-azuread-oidc";
    				options.RequireHttpsMetadata = false;
    				options.SaveTokens = true;
    				options.GetClaimsFromUserInfoEndpoint = true;
    				options.Scope.Add("email");
    				options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    				EfCoreSaasTenantIdentityProviderSettingRepository saasTenantIdentityProviderSettingsService =
    					context.Services.GetRequiredService();
    				options.Events.OnRedirectToIdentityProvider = async redirectContext =>
    				{
    					var azureAdSettings =
    						await saasTenantIdentityProviderSettingsService.GetIdentityProviderSettings(IdentityProviderType.ENTRA_ID);
    					var deserializedSettings = azureAdSettings?.Setting != null
    						? JsonConvert.DeserializeObject(azureAdSettings?.Setting)
    						: null;
    
    					if (azureAdSettings != null && deserializedSettings.IsEnabled)
    					{
    						redirectContext.ProtocolMessage.ClientId = deserializedSettings.ClientId;
    						redirectContext.ProtocolMessage.ClientSecret = deserializedSettings.ClientSecret;
    						redirectContext.ProtocolMessage.IssuerAddress =
    							redirectContext.ProtocolMessage.IssuerAddress.Replace(configuration["AzureAd:TenantId"],
    								deserializedSettings.AzureTenantId);
    
    						redirectContext.Options.ClientId = deserializedSettings.ClientId;
    						redirectContext.Options.ClientSecret = deserializedSettings.ClientSecret;
    						redirectContext.Options.Authority = redirectContext.Options.Authority.Replace(configuration["AzureAd:TenantId"],
    							deserializedSettings.AzureTenantId);
    						redirectContext.Options.MetadataAddress = redirectContext.Options.MetadataAddress.Replace(
    							configuration["AzureAd:TenantId"],
    							deserializedSettings.AzureTenantId);
    						redirectContext.Options.TokenValidationParameters
    							= new TokenValidationParameters()
    							{
    								ValidAudiences = new[] { deserializedSettings.ClientId },
    								ValidIssuers = new[]
    									{ redirectContext.Options.Authority.TrimEnd('/') }
    							};
    					}
    				};
    			});
    

    And here is new result:

    OpenIdConnectProtocolException: Message contains error: 'invalid_grant', error_description: 'AADSTS700005: Provided Authorization Code is intended to use against other tenant, thus rejected. Trace ID: 4f9f68a2-47dc-4e26-9f26-20a03abf5d00 Correlation ID: 1b736868-e6ee-463a-8169-c074e8ad94b4 Timestamp: 2025-04-10 09:28:58Z', error_uri: 'https://login.microsoftonline.com/error?code=700005'.
    
  • User Avatar
    0
    JanneHarju created

    I did find away to get it working. Here is what I need to add.

    var configRetriever = new OpenIdConnectConfigurationRetriever();
    						redirectContext.Options.ConfigurationManager = new ConfigurationManager(
    							redirectContext.Options.MetadataAddress,
    							configRetriever
    						);
    

    But it returned me back to registration page even you said that "ABP will automatically register external users". Is there some setting to set to get that automatic functionality to work?
    image.png

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    This is a registration page for external users.

    The user just needs to confirm his information

  • User Avatar
    0
    JanneHarju created

    But if we don't want to enable self registration? You said earlier that it will automatically register user after external login?
    I looked at your code and find this property. Where this value is coming from?
    https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs#L35

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    The value has come from the login callback.

    This is the design that lets external users confirm their information.

    You can override the register page to create a user in the OnGetAsnc method if you want.

    For example:

    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(RegisterModel))]
    public class MyRegisterModel : RegisterModel
    {
        public override async Task<IActionResult> OnGetAsync()
        {
            ExternalProviders = await GetExternalProviders();
    
            if (IsExternalLogin)
            {
                await TrySetEmailAsync();
                var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
                if (externalLoginInfo == null)
                {
                    Logger.LogWarning("External login info is not available");
                    return RedirectToPage("./Login");
                }
    
                if (Input.UserName.IsNullOrWhiteSpace())
                {
                    Input.UserName = await UserManager.GetUserNameFromEmailAsync(Input.EmailAddress);
                }
                user = await RegisterExternalUserAsync(externalLoginInfo, Input.UserName, Input.EmailAddress);
                
                await SignInManager.SignInAsync(user, isPersistent: true);
    
                // Clear the dynamic claims cache.
                await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
    
                return Redirect(ReturnUrl ?? "/");
            }
            
            if (!await CheckSelfRegistrationAsync())
            {
                if (IsExternalLoginOnly)
                {
                    return await OnPostExternalLogin(ExternalLoginScheme);
                }
    
                Alerts.Warning(L["SelfRegistrationDisabledMessage"]);
                return Page();
            }
    
            await SetUseCaptchaAsync();
    
            return Page();
    
            
            await TrySetEmailAsync();
        }
    }
    
  • User Avatar
    0
    JanneHarju created

    So if we enable self registration users can register by them self locally(i.e. without AzureAD)? I started think that maybe this page where user can confirm information is ok if users cannot register locally without azureAD if AzureAD is enabled for them. So is there tenant spesific setting to disable local registration?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Yes, it's tenant-level settings. You can log in as a tenant admin to set it.

  • User Avatar
    0
    JanneHarju created

    I found it and it almost work. Now my settings for that spesific tenant looks like this. image.png
    But after I change tenant it automatically directs to azure AD and after that back to registration page. As it should but now in this scenario Registration button is disabled eventhough it is clearly enabled in settings. What might be wrong now?
    image.png

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
Do you need assistance from an ABP expert?
Schedule a Meeting
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v9.3.0-preview. Updated on April 16, 2025, 12:13