Open Closed

Custom External Identity provider and integration in tiered MVC template #1587


User avatar
0
KevinLG created
  • ABP Framework version: v4.3.2
  • UI type: MVC
  • DB provider: EF Core
  • Tiered (MVC) or Identity Server Separated (Angular): Tiered MVC
  • Exception message and stack trace:
[02:25:19 ERR] An unhandled exception has occurred while executing the request. Autofac.Core.DependencyResolutionException: An exception was thrown while activating xxx.xxx.Web.Pages.Account.SsoLoginModel -> Volo.Abp.Identity.AspNetCore.AbpSignInManager -> Castle.Proxies.IdentityUserManagerProxy. ---> Autofac.Core.DependencyResolutionException: None of the constructors found with 'Volo.Abp.Autofac.AbpAutofacConstructorFinder' on type 'Castle.Proxies.IdentityUserManagerProxy' can be invoked with the available services and parameters: Cannot resolve parameter 'Volo.Abp.Identity.IIdentityRoleRepository roleRepository' of constructor 'Void .ctor(Castle.DynamicProxy.IInterceptor[], Volo.Abp.Identity.IdentityUserStore, Volo.Abp.Identity.IIdentityRoleRepository, Volo.Abp.Identity.IIdentityUserRepository, Microsoft.Extensions.Options.IOptions1[Microsoft.AspNetCore.Identity.IdentityOptions], Microsoft.AspNetCore.Identity.IPasswordHasher1[Volo.Abp.Identity.IdentityUser], System.Collections.Generic.IEnumerable1[Microsoft.AspNetCore.Identity.IUserValidator1[Volo.Abp.Identity.IdentityUser]], System.Collections.Generic.IEnumerable1[Microsoft.AspNetCore.Identity.IPasswordValidator1[Volo.Abp.Identity.IdentityUser]], Microsoft.AspNetCore.Identity.ILookupNormalizer, Microsoft.AspNetCore.Identity.IdentityErrorDescriber, System.IServiceProvider, Microsoft.Extensions.Logging.ILogger\`1[Volo.Abp.Identity.IdentityUserManager], Volo.Abp.Threading.ICancellationTokenProvider, Volo.Abp.Identity.IOrganizationUnitRepository, Volo.Abp.Settings.ISettingProvider)'.

Hello, we have our own existing authorization server created with a different oidc provider than idsrv4. We started a new project based on the tiered project template, where Identity server is separated in it's own tier project in the solution. We want to keep the identity module with local authentication in the web layer (We want the Asp.net Identity Roles management directly in the application), as well as integrating this latter with our existing auth server and use it as external provider (project requirement : we have multiple other applications based on this authent provider). In the web tier we changed the authentication service configuration to use our existing auth server, here is a peek into our WebModule class : ps: I replaced project names with XXXX for confidentiality purposes

using ...
namespace xxx.xxx.Web
{
    [DependsOn(
        typeof(XXXXXHttpApiModule),
        typeof(XXXXXHttpApiClientModule),
        typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule),
        typeof(AbpAspNetCoreMvcClientModule),
        typeof(AbpAutofacModule),
        typeof(AbpCachingStackExchangeRedisModule),
        typeof(AbpFeatureManagementWebModule),
        typeof(AbpAccountAdminWebModule),
        typeof(AbpHttpClientIdentityModelWebModule),
        typeof(AbpIdentityWebModule),
        typeof(AbpAuditLoggingWebModule),
        typeof(LeptonThemeManagementWebModule),
        typeof(AbpAspNetCoreMvcUiLeptonThemeModule),
        typeof(LanguageManagementWebModule),
        typeof(TextTemplateManagementWebModule),
        typeof(AbpSwashbuckleModule),
        typeof(AbpAspNetCoreSerilogModule),
        typeof(AbpAccountPublicWebModule)
        )]
        ///these are module developped by us
    [DependsOn(typeof(XXXXWebModule))]
    [DependsOn(typeof(XXXWebModule))]
    [DependsOn(typeof(XXXXWebModule))]
    [DependsOn(typeof(XXXWebModule))]
    [DependsOn(typeof(XXXXWebModule))]
    public class XXXXWebModule : AbpModule
    {
        .....
        private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
        {
            AbpClaimTypes.UserId = "sub"; 
            context.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(options =>
            {
                options.ExpireTimeSpan = TimeSpan.FromDays(365);
            })
            .AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority = configuration["AuthServer:Authority"];
                options.ClientId = configuration["AuthServer:ClientId"];
                options.ClientSecret = configuration["AuthServer:ClientSecret"];
                options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); ;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = true;
                options.ResponseType = OpenIdConnectResponseType.Code;
                options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                options.ResponseMode = OpenIdConnectResponseMode.FormPost;

                options.UsePkce = true;
                options.Scope.Clear();

                options.Scope.Add("openid"); 
                options.Scope.Add("profile"); 
                options.Scope.Add("email"); 
                options.Scope.Add("roles"); 

                options.Scope.Add("XXXX.Api");
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["AuthServer:ClientSecret"]))
                };
                options.SecurityTokenValidator = new JwtSecurityTokenHandler
                {
                    // Disable the built-in JWT claims mapping feature.
                    InboundClaimTypeMap = new Dictionary<string, string>()
                };
                options.TokenValidationParameters.NameClaimType = "name";
                options.TokenValidationParameters.RoleClaimType = "role";
                options.Events = new OpenIdConnectEvents
                {
                    OnRedirectToIdentityProviderForSignOut = context =>
                    {
                        context.ProtocolMessage.SetParameter("skipConsent", "true");
                        return Task.FromResult(0);
                    }
                };
            });
        }
        
    }
}

We removed the identity server project from our solution manually along with all its related abp depndendencies. then we intsalled the package AbpAccountPublicWebModule in the web tier. we overriden the Pages.Account/login.cshtml as follows:

@page
@model xxx.xxx.Web.Pages.Account.SsoLoginModel
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
<form asp-page="./SsoLogin" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
    <button type="submit"
            class="mt-2 mr-2 btn btn-outline-primary btn-sm"
            name="provider"
            value="@OpenIdConnectDefaults.AuthenticationScheme"
            data-busy-text="...">
        xxx Connect
    </button>
</form>

and made point to the following custom login PageModel:


    public class SsoLoginModel : LoginModel
    {
        public SsoLoginModel(
            IAuthenticationSchemeProvider schemeProvider,
            IOptions<AbpAccountOptions> accountOptions,
            IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
            IAccountExternalProviderAppService accountExternalProviderAppService,
            ICurrentPrincipalAccessor currentPrincipalAccessor,
            IOptions<IdentityOptions> identityOptions,
            IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions) : 
            base(schemeProvider, 
                accountOptions, 
                recaptchaValidatorFactory, 
                accountExternalProviderAppService, 
                currentPrincipalAccessor, 
                identityOptions, 
                reCaptchaOptions)
        {
        }
        [UnitOfWork]
        public override async Task<IActionResult> OnPostExternalLogin(string provider)
        {
            var redirectUrl = Url.Page("./SsoLogin", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash });
            var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            properties.Items["scheme"] = provider;
            return await Task.FromResult(Challenge(properties, provider));
        }
        [UnitOfWork]
        public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
        {
            if (remoteError != null)
            {
                Logger.LogWarning($"External login callback error: {remoteError}");
                return RedirectToPage("./SsoLogin");
            }
            await IdentityOptions.SetAsync();
            var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
            if (loginInfo == null)
            {
                Logger.LogWarning("External login info is not available");
                return RedirectToPage("./SsoLogin");
            }
            IsLinkLogin = await VerifyLinkTokenAsync();
            var result = await SignInManager.ExternalLoginSignInAsync(
                loginInfo.LoginProvider,
                loginInfo.ProviderKey,
                isPersistent: true,
                bypassTwoFactor: true
            );
            if (!result.Succeeded)
            {
                await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                {
                    Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
                    Action = "Login" + result
                });
            }
            if (result.Succeeded)
            {
                var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
                if (IsLinkLogin)
                {
                    using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
                    {
                        await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
                        {
                            UserId = LinkUserId.Value,
                            TenantId = LinkTenantId,
                            Token = LinkToken
                        });
                    }
                }
                await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                {
                    Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
                    Action = "result.ToIdentitySecurityLogAction() // FMA: we don't have sources of this as the module is closed source with current licence",
                    UserName = user.UserName
                });
                return RedirectSafely(returnUrl, returnUrlHash);
            }
            //TODO: Handle other cases for result!
            // Get the information about the user from the external login provider
            var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
            if (externalLoginInfo == null)
            {
                throw new ApplicationException("Error loading external login information during confirmation.");
            }
            var externalUser = await CreateExternalUserAsync(externalLoginInfo);
            if (await HasRequiredIdentitySettings())
            {
                Logger.LogWarning($"New external user is created but confirmation is required!");
                await StoreConfirmUser(externalUser);
                return RedirectToPage("./ConfirmUser", new
                {
                    returnUrl = ReturnUrl,
                    returnUrlHash = ReturnUrlHash
                });
            }
            await SignInManager.SignInAsync(externalUser, false);
            if (IsLinkLogin)
            {
                using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(externalUser)))
                {
                    await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
                    {
                        UserId = LinkUserId.Value,
                        TenantId = LinkTenantId,
                        Token = LinkToken
                    });
                }
            }
            await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
            {
                Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
                Action = "result.ToIdentitySecurityLogAction() // FMA: we don't have sources of this as the module is closed source with current licence",
                UserName = externalUser.Name
            });
            return RedirectSafely(returnUrl, returnUrlHash);
        }
  }

When we launch the web project it runs successfully, but the moment we call the domain/Account/SSoLogin we get the following ioc related exception (cf. above) we tried running the project without our custom login page, and we get the same error when we call the domain/Account/Login Tried everything that came to my mind to solve this and searched for a solution in the official docs and internet with no luck, can you please guide us to a solution or tell us if this is not possible under current project template. Regards,


6 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    This is actually a DI problem, you can try :

    [DependsOn(
            typeof(XXXXXHttpApiModule),
            typeof(XXXXXHttpApiClientModule),
            typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule),
            typeof(AbpAspNetCoreMvcClientModule),
            typeof(AbpAutofacModule),
            typeof(AbpCachingStackExchangeRedisModule),
            typeof(AbpFeatureManagementWebModule),
            typeof(AbpAccountAdminWebModule),
            typeof(AbpHttpClientIdentityModelWebModule),
            typeof(AbpIdentityWebModule),
            typeof(AbpAuditLoggingWebModule),
            typeof(LeptonThemeManagementWebModule),
            typeof(AbpAspNetCoreMvcUiLeptonThemeModule),
            typeof(LanguageManagementWebModule),
            typeof(TextTemplateManagementWebModule),
            typeof(AbpSwashbuckleModule),
            typeof(AbpAspNetCoreSerilogModule),
            typeof(AbpAccountPublicWebModule)
            )]
            ///these are module developped by us
        [DependsOn(typeof(XXXXWebModule))]
        [DependsOn(typeof(XXXWebModule))]
        [DependsOn(typeof(XXXXWebModule))]
        [DependsOn(typeof(XXXWebModule))]
        [DependsOn(typeof(XXXXWebModule))]
        [DependsOn(typeof(XXXXEntityFrameworkCoreModule))]  // this line.
        public class XXXXWebModule : AbpModule
    
  • User Avatar
    0
    KevinLG created

    Hello, thank for your answer

    Yes, we could, but we are in a tiered architecture and we surely don't want having a direct dependency from our Web tier, to the database.

    What we are looking for here is more architecture guideline. How to have custom external authentication in a tiered architecture, without relying on a the XXX.IdentityServer site (which access directly to the database).

    What we currently have :

    • XXX.HttpApi : Our APIs, having a direct access to database;
    • XXX.Web : Our back office website, accessing data only through HttpApi (via dynamic proxies)

    Both authenticated with a custom external prodiver, implementing oidc:

    • XXX.HttpApi : Use introspection to validate the accessToken passed.
    • XXX.Web : Use AbpOpenIdConnect integration

    What we would like, is populating own XXX user lists (ASP.NET Identity based), from external authentication, without breaking tiered-architecture

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You should not reference AbpAccountPublicWebModule to web project, because it requires you to use a database, you should be redirected to your authorization server

  • User Avatar
    0
    KevinLG created

    So if I understand correctly, to handle locally Roles and Permissions for external users (our SSO), we need a local authentication server, to handle our external provider (our SSO), and create users as "external user" in our system.

    Or could I add a reference to AbpAccountPublicWebModule in the HttpApi tier, and handle it there ?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    So if I understand correctly, to handle locally Roles and Permissions for external users (our SSO), we need a local authentication server, to handle our external provider (our SSO), and create users as "external user" in our system.

    Yes you need, like IdentityServer

    Or could I add a reference to AbpAccountPublicWebModule in the HttpApi tier, and handle it there ?

    No problem, HttpApi.Host can also be an authorization server

  • User Avatar
    0
    ServiceBot created
    Support Team Automatic process manager

    This question has been automatically marked as stale because it has not had recent activity.

Made with ❤️ on ABP v9.1.0-preview. Updated on November 11, 2024, 11:11