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.
36 Answer(s)
-
0
Hi, after you have configured external providers for the host side:
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):
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
-
-
0
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? -
0
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:-
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 } } }
-
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.
-
-
0
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"); });
-
0
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. -
0
OK, thanks!
-
0
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.
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;
-
0
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"); } } }
-
0
After login url looks like this. I removed some characters from those long values like state. If this helps anything.
https://localhost:44369/Account/Register?isExternalLogin=True&externalLoginAuthSchema=AzureOpenId&returnUrl=%2Fconnect%2Fauthorize%3Fresponse_type%3Dcode%26client_id%3DSCM_App%26state%3DQmRYxUmlDVEhQ;%25252Fdashboard%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A4200%252F%26scope%3Dopenid%2520offline_access%2520SCM%26code_challenge%3D4vcFgDJfIZgrYbPec%26code_challenge_method%3DS256%26nonce%3DQmRKTG9UYxUmlDVEhQ%26culture%3Dfi-FI%26ui-culture%3Dfi-FI%26returnUrl%3D%252Fdashboard -
0
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.
-
0
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?
-
0
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?
-
0
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"; }; })
-
0
-
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; -
Is there possibility to get login working without enabling self-registration when using Azure Entra Id?
-
-
0
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.
-
0
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. -
0
Hi,
You can try:
options.TokenValidationParameters = new TokenValidationParameters() { ValidAudiences = new[] { "....", "...." }// add all audiences that you want to validate };
-
0
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'.
-
0
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?
-
0
Hi,
This is a registration page for external users.
The user just needs to confirm his information
-
0
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 -
0
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 theOnGetAsnc
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(); } }
-
0
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?
-
0
Hi,
Yes, it's tenant-level settings. You can log in as a tenant admin to set it.
-
0
I found it and it almost work. Now my settings for that spesific tenant looks like this.
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?