Open Closed

Multi-tenant authentication provider selection based on route parameter. #10739


User avatar
0
alex.oommen created

Hi Team,

We are investigating a multi-tenant authentication approach and would like some guidance on the recommended ABP implementation.

Scenario

We have a single Blazor application and AuthServer.

We would like to support tenant-specific URLs such as:

https://portal.auth.com/tenant1 https://portal.auth.com/tenant2 https://portal.auth.com/tenant3

The tenant should be resolved from the route segment.

Requirements

  • Resolve the tenant from the route parameter.
  • Allow each tenant to have its own authentication configuration.
  • Support different authentication providers per tenant, for example: Tenant1 → Entra ID + MyID Tenant2 → Challenge Tenant3 → Facebook + Local Login
  • Once a user accesses the application through a tenant-specific endpoint, we would like the tenant context to be fixed.

Questions

  • What is the recommended way to resolve the tenant from a route parameter such as /Tenant1 in AuthServer?
  • Does ABP provide built-in support for mapping tenants to different external authentication providers?
  • Can external authentication providers (Entra, OpenID Connect providers, Facebook, etc.) be enabled/disabled per tenant using the existing Account/OpenIddict modules?
  • If different tenants require different OpenID Connect configurations (different ClientIds, Authorities, ClientSecrets), what is the recommended ABP approach?
  • Is there an example of implementing tenant-specific authentication providers while still using the standard ABP Account and OpenIddict modules?

Any guidance, documentation, or sample implementations would be greatly appreciated.

Thank you.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

4 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Resolve the tenant from a route parameter such as /tenant1 in AuthServer
      1. Use ABP’s route tenant resolver (RouteTenantResolveContributor) by adding the special route parameter name {__tenant} to your routes.
        • For MVC/Razor Pages endpoints on the AuthServer, the pattern is:
    [Route("{__tenant}/[controller]")]
           public class MyController : Controller
           {
           }
    
       or for Razor Pages:
    
    @page "{__tenant?}/Account/Login"
    
    1. If you want it globally (so you don’t have to edit every page/controller route), you can apply a convention to prepend {__tenant} to all routes (controllers/pages) as shown in the referenced article.
    2. If you also need correct link generation/navigation behavior when tenant is in the first path segment, the referenced article shows a middleware approach using PathBase and a custom route contributor derived from RouteTenantResolveContributor.
    • Fix tenant context once a user enters via tenant-specific endpoint

      • The “fixed tenant” behavior is effectively achieved by only resolving tenant from the route (and not from headers/cookies/querystring), i.e. configure tenant resolve contributors to only use your preferred resolver. The route-based approach ensures subsequent requests under that base path keep resolving the same tenant.
    • Mapping tenants to different external authentication providers / enabling-disabling per tenant

      • No verified source available for built-in “tenant → provider mapping” that automatically enables/disables external providers per tenant inside the standard Account/OpenIddict UI out of the box.
      • Practically, to achieve “Tenant1 uses Entra+MyID, Tenant2 uses Challenge, Tenant3 uses Facebook+Local”، you typically need to customize the Account module login UI/flow and decide which schemes to show/challenge based on ICurrentTenant (tenant resolved before hitting /Account/Login via {__tenant} route).
    • Different OpenID Connect configurations per tenant (ClientId/Authority/Secret)

      • No verified source available for an official ABP sample implementing per-tenant external provider options (authority/client id/secret) while still using standard ABP Account + OpenIddict as-is.
      • The generally aligned approach in ABP apps is:
        1. Store per-tenant provider settings (e.g., in your own table or ABP Setting Management per-tenant).
        2. At runtime, when the tenant is resolved, configure/challenge the correct external scheme for that tenant (or implement dynamic options resolution).
        3. Customize the login page to only present the allowed providers for the current tenant.
    • Example / sample

      • I couldn’t find a verified official ABP sample that demonstrates “tenant-specific external authentication provider selection + per-tenant OIDC options” for AuthServer using standard Account/OpenIddict modules without customization.
      • The closest verified, relevant implementation guidance for tenant resolution from route is the community article below.

    Links (validated):

    • https://abp.io/community/articles/ah7oru97
    • https://docs.abp.io/en/abp/latest/Multi-Tenancy
    • https://abp.io/docs/latest/framework/architecture/multi-tenancy

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Let me walk through this in two parts: route-based tenant resolution and per-tenant external providers. Both are doable with the existing Account Pro and OpenIddict modules.

    1) Resolving the tenant from a route segment like /tenant1

    ABP has a built-in RouteTenantResolveContributor that reads the {__tenant} route value. The non-trivial part is getting {__tenant} to apply to every route in the AuthServer (Razor Pages, controllers) and making links/redirects keep the prefix.

    I wrote a community article that covers the full setup — route conventions for pages and controllers, a PathBase middleware so generated links automatically include /tenantX, a custom RouteTenantResolveContributor that reads the tenant from PathBase, and AbpThemingOptions.BaseUrl so the client-side abp.appPath is correct:

    https://abp.io/community/articles/Resolving-Tenant-from-Route-in-ABP-Framework/ah7oru97

    The AuthServer is just a Razor Pages app under the hood, so the same conventions apply to /Account/Login, /Account/Register, the OpenIddict authorize endpoints, etc. Once the article's pieces are wired up, hitting https://portal.auth.com/tenant1/Account/Login resolves tenant1 automatically and the context stays "fixed" for the rest of the request chain — subsequent links and cookies stay under /tenant1.

    If you also want to limit resolution to only the route (so cookies/headers can't override the tenant), you can configure AbpTenantResolveOptions and clear the other contributors:

    Configure<AbpTenantResolveOptions>(options =>
    {
        options.TenantResolvers.Clear();
        options.TenantResolvers.Add(new MyRouteTenantResolveContributor());
    });
    

    2) Per-tenant external authentication providers (Entra ID, Facebook, Google, …)

    The Account Pro module already supports this scenario out of the box — each tenant can enable/disable an external provider and configure its own ClientId/ClientSecret/Authority from the Settings page. You don't need to write a custom dynamic options provider for the built-in schemes; the module wires IOptionsMonitor<TOptions> per tenant for you.

    The pattern is: call the standard .AddXxx(scheme, …) and chain .WithDynamicOptions<TOptions, THandler>(scheme, …) to mark which option properties should come from the per-tenant settings.

    Documentation: https://abp.io/docs/latest/modules/account-pro#sociallong-external-logins

    For Entra ID, use OpenIdConnect. Here's the full sample from the docs (works the same way for any OIDC provider — different ClientId / Authority / ClientSecret per tenant):

    context.Services.AddAuthentication()
        .AddOpenIdConnect("AzureOpenId", "Azure AD", options =>
        {
            options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
            options.RequireHttpsMetadata = false;
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Add("email");
            options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
            options.CallbackPath = configuration["AzureAd:CallbackPath"];
        })
        .WithDynamicOptions<OpenIdConnectOptions, OpenIdConnectHandler>("AzureOpenId",
            options =>
            {
                options.WithProperty(x => x.Authority);
                options.WithProperty(x => x.ClientId);
                options.WithProperty(x => x.ClientSecret, isSecret: true);
            });
    
    // OIDC needs metadata to be re-resolved when Authority changes
    public class OpenIdConnectPostConfigureAccountExternalProviderOptions
        : IPostConfigureAccountExternalProviderOptions<OpenIdConnectOptions>
    {
        private readonly IEnumerable<IPostConfigureOptions<OpenIdConnectOptions>> _postConfigureOptions;
    
        public OpenIdConnectPostConfigureAccountExternalProviderOptions(
            IEnumerable<IPostConfigureOptions<OpenIdConnectOptions>> postConfigureOptions)
        {
            _postConfigureOptions = postConfigureOptions;
        }
    
        public Task PostConfigureAsync(string name, OpenIdConnectOptions options)
        {
            foreach (var configureOption in _postConfigureOptions)
            {
                configureOption.PostConfigure(name, options);
            }
            return Task.CompletedTask;
        }
    }
    
    context.Services.TryAddEnumerable(
        ServiceDescriptor.Singleton<
            IPostConfigureAccountExternalProviderOptions<OpenIdConnectOptions>,
            OpenIdConnectPostConfigureAccountExternalProviderOptions>());
    

    After this, an admin signs in to the host, switches the management UI to the target tenant, and fills in Authority / ClientId / ClientSecret per tenant on the External Providers settings page. The login page automatically shows only the providers enabled for the current tenant — so combined with part 1, a user landing on /tenant1/Account/Login only sees Entra + MyID, /tenant2 only sees Challenge, etc.

    The template already wires Google / Microsoft / Twitter this way — see MyProjectNameAuthServerModule.ConfigureExternalProviders(...) in your AuthServer project as a working reference.

    3) Tiered solution — important

    Because you have a separated AuthServer + Blazor host (tiered), both projects need the .WithDynamicOptions<…>(…) registration. The AuthServer runs the actual login/challenge flow, but the host also needs the dynamic registration so external login callbacks resolve correctly. You can copy the same .WithDynamicOptions<…>(…) block into the host module without the static .AddOpenIdConnect(…) call (the host doesn't need to challenge directly).

    4) MyID / Challenge

    If "MyID" is an OIDC-compliant IdP, the snippet above works as-is — register one more scheme like .AddOpenIdConnect("MyID", …).WithDynamicOptions<OpenIdConnectOptions, OpenIdConnectHandler>("MyID", …). Could you share more details about what the "Challenge" and "MyID" providers actually are (OAuth 2.0? OIDC? SAML? a custom protocol?) so we can suggest the right approach for those two specifically?

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    alex.oommen created

    Hi,

    Scenario

    We need to determine both the user type and authentication provider from the URL structure. Example URLs: /tenant1/internal/entraid /tenant1/internal/myid /tenant1/external/local /tenant1/external/facebook

    /tenant2/internal/entraid /tenant2/external/local

    For this question, please assume the tenant has already been resolved before authentication begins.

    Requirements

    1. User Type Resolution The URL should determine the user type: /internal or /external Example: /tenant1/internal/entraid should resolve: UserType = Internal and /tenant1/external/facebook should resolve: UserType = External

    2. Authentication Provider Resolution The authentication provider should be selected from the route. Example: /tenant1/internal/entraid should automatically initiate authentication using: Entra ID while, /tenant1/external/facebook should automatically initiate authentication using: Facebook and, /tenant1/external/local should display the standard username/password login page.

    Authentication Rules Each tenant may have different authentication configurations. Example: Tenant1 Internal: - Entra ID - MyID

    External:
        - Local Login
        - Facebook
    

    Tenant2 Internal: - Entra ID

    External: - Local Login

    The available providers are not globally fixed and may vary by tenant.

    Questions

    • Does ABP provide a supported mechanism for dynamically selecting an external authentication provider based on the route?
    • Is it possible to automatically trigger a specific authentication challenge based on a route parameter without first displaying the standard login page? For example: /tenant1/internal/entraid, should immediately redirect to the Entra ID authentication flow.

    What is the recommended way to model tenant-specific authentication provider configurations?

    Would this typically be implemented using:

    • Tenant settings
    • External provider settings
    • OpenIddict applications
    • A custom configuration store
    • Can ABP support different external provider configurations (ClientId, ClientSecret, Authority, etc.) per tenant?
    • How can we ensure that users entering through a URL such as: /tenant1/internal/entraid remain bound to: Internal user type Entra ID provider throughout the authentication flow and cannot switch to another provider that is not configured for that entry point?

    Are there any existing ABP samples, best practices, or recommended architectures for implementing URL-driven authentication provider selection?

    Any guidance or recommendations would be greatly appreciated.

    Thank you.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    There's no built-in "route → provider" switch in ABP, but all the pieces are there. The pattern below is one custom LoginModel plus a small URL-rewrite middleware. A couple of subtleties to call out up front:

    • /external/local should not invoke Challenge("local") — local is not a registered scheme. The override below special-cases it and falls through to the standard login page.
    • The local login page should hide external providers that aren't in the current tenant's whitelist. The override of GetExternalProviders below handles that.
    • Both [ExposeServices(typeof(LoginModel))] and [Dependency(ReplaceServices = true)] are required to fully replace the default LoginModel.

    Custom LoginModel

    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Options;
    using Owl.reCAPTCHA;
    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.Security.Recaptcha;
    using Volo.Abp.Account.Web.Pages.Account;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.OpenIddict;
    using Volo.Abp.Security.Claims;
    using Volo.Saas.Tenants;
    
    namespace MyCompanyName.MyProjectName.Pages.Account;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LoginModel))]
    public class MyLoginModel : OpenIddictSupportedLoginModel
    {
        public string UserType { get; private set; }
        public string Provider { get; private set; }
    
        protected ITenantRepository TenantRepository { get; }
    
        public MyLoginModel(
            IAuthenticationSchemeProvider schemeProvider,
            IOptions<AbpAccountOptions> accountOptions,
            IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
            IAccountExternalProviderAppService accountExternalProviderAppService,
            ICurrentPrincipalAccessor currentPrincipalAccessor,
            IOptions<IdentityOptions> identityOptions,
            IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions,
            AbpOpenIddictRequestHelper openIddictRequestHelper,
            ITenantRepository tenantRepository)
            : base(schemeProvider, accountOptions, recaptchaValidatorFactory,
                   accountExternalProviderAppService, currentPrincipalAccessor,
                   identityOptions, reCaptchaOptions, openIddictRequestHelper)
        {
            TenantRepository = tenantRepository;
        }
    
        public override async Task<IActionResult> OnGetAsync()
        {
            UserType = RouteData.Values["userType"]?.ToString()
                       ?? Request.Query["userType"].ToString();
            Provider = RouteData.Values["provider"]?.ToString()
                       ?? Request.Query["provider"].ToString();
    
            // /tenant1/external/local → render the standard local login page; do not challenge.
            if (string.Equals(Provider, "local", System.StringComparison.OrdinalIgnoreCase))
            {
                return await base.OnGetAsync();
            }
    
            if (!string.IsNullOrWhiteSpace(Provider))
            {
                if (!await IsProviderAllowedForCurrentTenantAsync(Provider, UserType))
                {
                    Alerts.Danger(L["The provider is not allowed for this entry point."]);
                    return await base.OnGetAsync();
                }
                return await OnPostExternalLogin(Provider);
            }
    
            return await base.OnGetAsync();
        }
    
        public override async Task<IActionResult> OnPostExternalLogin(string provider)
        {
            var redirectUrl = Url.Page("./Login",
                pageHandler: "ExternalLoginCallback",
                values: new { ReturnUrl, ReturnUrlHash, LinkTenantId, LinkUserId, LinkToken });
    
            var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            properties.Items["scheme"] = provider;
    
            // Bind UserType into the auth properties so the callback can validate it.
            if (!string.IsNullOrWhiteSpace(UserType))
            {
                properties.Items["UserType"] = UserType;
            }
    
            if (CurrentTenant.Id.HasValue)
            {
                properties.Items[TenantResolverConsts.DefaultTenantKey] = CurrentTenant.Id.ToString();
            }
    
            return Challenge(properties, provider);
        }
    
        public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string remoteError = null)
        {
            var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
            if (loginInfo != null)
            {
                string userType = null;
                loginInfo.AuthenticationProperties?.Items.TryGetValue("UserType", out userType);
    
                // If you require all external callbacks to come from a URL-driven entry point,
                // reject when UserType is missing.
                if (string.IsNullOrWhiteSpace(userType))
                {
                    Alerts.Danger(L["External login must be initiated from a tenant entry point."]);
                    return await InitPageAsync() ?? Page();
                }
    
                if (!await IsProviderAllowedForCurrentTenantAsync(loginInfo.LoginProvider, userType))
                {
                    Alerts.Danger(L["The provider is not allowed for this entry point."]);
                    return await InitPageAsync() ?? Page();
                }
    
                // Optionally promote UserType to a user claim / extra property here.
            }
    
            return await base.OnGetExternalLoginCallbackAsync(remoteError);
        }
    
        // Hide non-whitelisted external providers on the standard login page (used when /external/local is opened).
        protected override async Task<List<ExternalProviderModel>> GetExternalProviders()
        {
            var providers = await base.GetExternalProviders();
            if (string.IsNullOrWhiteSpace(UserType))
            {
                return providers;
            }
            var result = new List<ExternalProviderModel>();
            foreach (var p in providers)
            {
                if (await IsProviderAllowedForCurrentTenantAsync(p.AuthenticationScheme, UserType))
                {
                    result.Add(p);
                }
            }
            return result;
        }
    
        protected virtual async Task<bool> IsProviderAllowedForCurrentTenantAsync(string provider, string userType)
        {
            if (!CurrentTenant.Id.HasValue || string.IsNullOrWhiteSpace(userType))
            {
                return true; // host, or no UserType bound → don't restrict
            }
    
            // Tenant entity lives in host scope; switch to host before querying (same pattern ABP's own TenantStore uses).
            var tenantId = CurrentTenant.Id.Value;
            Tenant tenant;
            using (CurrentTenant.Change(null))
            {
                tenant = await TenantRepository.FindAsync(tenantId);
            }
            if (tenant == null)
            {
                return false;
            }
    
            // ExtraProperties may hold the value as a JSON string or as a nested object — handle both.
            var raw = tenant.ExtraProperties.GetOrDefault("AllowedProviders");
            if (raw == null)
            {
                return false;
            }
            var json = raw is string s ? s : System.Text.Json.JsonSerializer.Serialize(raw);
            if (string.IsNullOrWhiteSpace(json))
            {
                return false;
            }
    
            using var doc = System.Text.Json.JsonDocument.Parse(json);
            if (!doc.RootElement.TryGetProperty(userType, out var arr))
            {
                return false;
            }
            return arr.EnumerateArray()
                .Any(x => string.Equals(x.GetString(), provider, System.StringComparison.OrdinalIgnoreCase));
        }
    }
    

    Notes:

    1. OnPostExternalLogin is a plain virtual method, so calling it from OnGetAsync works.
    2. UserType is added to AuthenticationProperties.Items. ASP.NET Core protects this state when the OIDC handler builds the authorize URL, and it comes back via the external login info on callback — so the value is tamper-resistant for the OAuth round-trip.
    3. Reading ExtraProperties requires ITenantRepository, not ITenantStore — TenantConfiguration does not expose ExtraProperties. The Tenant entity lives in the host scope, so the repository call is wrapped in CurrentTenant.Change(null) to bypass the tenant data filter (same pattern ABP's own TenantStore uses internally). In production, cache the per-tenant whitelist (e.g. via IDistributedCache or IMemoryCache) to avoid a DB hit on every login.
    4. If your UserType represents a fixed property of the user (e.g. Internal = employee, External = customer), also validate after local login (OnPostAsync) that the signed-in user's stored UserType matches the URL's userType. That code is short but very tied to your domain, so it's not shown here.

    Routing the URL — /{tenant}/{userType}/{provider} → /Account/Login

    The route-based tenant resolution side is covered in Resolving Tenant from Route in ABP Framework. After that middleware runs, /tenant1 is moved into PathBase, so the remaining path is /internal/entraid. Add one more small middleware to forward that into the standard /Account/Login page:

    // Place after the tenant PathBase middleware and before app.UseRouting().
    app.Use(async (httpContext, next) =>
    {
        var path = httpContext.Request.Path;
        string userType = null;
        PathString rest = default;
    
        if (path.StartsWithSegments("/internal", out rest))
        {
            userType = "internal";
        }
        else if (path.StartsWithSegments("/external", out rest))
        {
            userType = "external";
        }
    
        // Accept only one trailing segment (the provider name) — reject e.g. /internal/a/b.
        if (userType != null)
        {
            var provider = rest.HasValue ? rest.Value!.Trim('/') : null;
            if (string.IsNullOrEmpty(provider) || provider.Contains('/'))
            {
                await next(httpContext);
                return;
            }
    
            var query = QueryString.Create(new[]
            {
                new KeyValuePair<string, string>("userType", userType),
                new KeyValuePair<string, string>("provider", provider),
            });
            httpContext.Request.Path = "/Account/Login";
            httpContext.Request.QueryString = httpContext.Request.QueryString.Add(query);
        }
    
        await next(httpContext);
    });
    

    /tenant1/internal/entraid then resolves as: PathBase=/tenant1, Path=/Account/Login, Query=userType=internal&provider=entraid. Hitting that URL goes straight into the IdP redirect — the login page never renders.

    How to model the per-tenant configuration

    Don't use OpenIddict applications for this — those are for the client apps that authenticate against your AuthServer, not for external login providers.

    Two separate storage concerns:

    | Data | Where to store | | --- | --- | | ClientId / ClientSecret / Authority per tenant per provider | Account Pro's External Provider Settings (already multi-tenant; see my previous reply) | | The list of providers allowed for (tenant, userType) | Tenant ExtraProperties as JSON, or a small custom table if you prefer relational storage |

    Tenant ExtraProperties is convenient for the whitelist. Example payload:

    {
      "AllowedProviders": {
        "internal": ["AzureOpenId", "MyID"],
        "external": ["local", "Facebook"]
      }
    }
    

    Read it via ITenantRepository.FindAsync(tenantId) inside IsProviderAllowedForCurrentTenantAsync. Don't use ITenantStore — the cached TenantConfiguration it returns does not expose ExtraProperties. Add your own caching layer if read frequency is a concern.

    Per-tenant ClientId / Secret / Authority

    This is the WithDynamicOptions<TOptions, THandler> block from my previous reply. For MyID, register it the same way as Azure AD if it speaks OIDC. The OIDC handler resolves discovery on every Authority change, so you must also register an IPostConfigureAccountExternalProviderOptions<OpenIdConnectOptions> post-configurator alongside the dynamic options:

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.DependencyInjection.Extensions;
    using Microsoft.Extensions.Options;
    using Volo.Abp.Account.ExternalProviders;
    using Volo.Abp.Account.Public.Web.ExternalProviders;
    
    // inside ConfigureServices
    context.Services.AddAuthentication()
        .AddOpenIdConnect("MyID", "MyID", options => { /* static defaults */ })
        .WithDynamicOptions<OpenIdConnectOptions, OpenIdConnectHandler>("MyID",
            o =>
            {
                o.WithProperty(x => x.Authority);
                o.WithProperty(x => x.ClientId);
                o.WithProperty(x => x.ClientSecret, isSecret: true);
            });
    
    context.Services.TryAddEnumerable(
        ServiceDescriptor.Singleton<
            IPostConfigureAccountExternalProviderOptions<OpenIdConnectOptions>,
            OpenIdConnectPostConfigureAccountExternalProviderOptions>());
    
    public class OpenIdConnectPostConfigureAccountExternalProviderOptions
        : IPostConfigureAccountExternalProviderOptions<OpenIdConnectOptions>
    {
        private readonly IEnumerable<IPostConfigureOptions<OpenIdConnectOptions>> _postConfigureOptions;
    
        public OpenIdConnectPostConfigureAccountExternalProviderOptions(
            IEnumerable<IPostConfigureOptions<OpenIdConnectOptions>> postConfigureOptions)
        {
            _postConfigureOptions = postConfigureOptions;
        }
    
        public Task PostConfigureAsync(string name, OpenIdConnectOptions options)
        {
            foreach (var configureOption in _postConfigureOptions)
            {
                configureOption.PostConfigure(name, options);
            }
            return Task.CompletedTask;
        }
    }
    

    If MyID is not OIDC (SAML, custom protocol, etc.), let me know what it is and I'll send the right registration shape.

    Sample / reference

    There's no ready-made ABP sample combining all of this, but the closest official pieces are:

    • Account Pro External Providers docs: https://abp.io/docs/latest/modules/account-pro#sociallong-external-logins
    • Maliming's community article on route-based tenant resolution: https://abp.io/community/articles/Resolving-Tenant-from-Route-in-ABP-Framework/ah7oru97
    • The OpenIddictSupportedLoginModel already present in your AuthServer project — the override above follows the same pattern.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 24, 2026, 12:09
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.