Open Closed

Forcing external Login for certain actions #9918


User avatar
0
papusa created

Hi, This is a continuation of my previous ticket.

In my application, some actions should only be allowed when the user is authenticated via BankID (external login). I’m looking for a way to force a user to log in with a specific provider. So far, the only approach I found is overriding the LoginModel. While it works, it feels like a hack and may not be the recommended way.

navigateToBankIdLogin(){
    this.authService.navigateToLogin({
      prompt: 'login',
      bankid: true
    });
  }
[ExposeServices(typeof(Volo.Abp.Account.Public.Web.Pages.Account.LoginModel))]
public class LoginModel : Volo.Abp.Account.Public.Web.Pages.Account.LoginModel
{
    public LoginModel(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)
    {
    }

    public override Task<IActionResult> OnGetAsync()
    {
        return ShouldForceBankIdLogin()
            ? base.OnPostExternalLogin(MachineRegistryConsts.BankIdLoginProviderName)
            : base.OnGetAsync();
    }

    private bool ShouldForceBankIdLogin()
    {
        if (ReturnUrl.IsNullOrEmpty())
        {
            return false;
        }
        
        var decodedReturnUrl = HttpUtility.UrlDecode(ReturnUrl);
        return decodedReturnUrl.Split("&").Any(p => p.Equals("bankid=true", StringComparison.OrdinalIgnoreCase));
    }
}

Additionally, there’s a UI/flow issue: I have to click the login button twice before the user is navigated to the BankID login, even though the prompt is set to 'login'. Bellow is a gif showing the issue: Could you advise on the recommended ABP way to enforce external login for specific actions and/or address the double-click issue?


16 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent

    To enforce external login (e.g., BankID) for specific actions in ABP, the recommended approach is to customize the login flow by overriding the LoginModel and the login page, as you have done. This is the standard and supported way in ABP to control authentication logic, since the login UI and logic are encapsulated in the Account module and can be overridden as needed.

    Recommended Approach

    1. Override the LoginModel:
      By overriding the OnGetAsync method in your custom LoginModel, you can check for a specific query parameter (e.g., bankid=true) and trigger the external login flow for BankID. This is the correct extensibility point provided by ABP.
    [ExposeServices(typeof(Volo.Abp.Account.Public.Web.Pages.Account.LoginModel))]
       public class LoginModel : Volo.Abp.Account.Public.Web.Pages.Account.LoginModel
       {
           // ... constructor ...
    
           public override Task<IActionResult> OnGetAsync()
           {
               return ShouldForceBankIdLogin()
                   ? base.OnPostExternalLogin(MachineRegistryConsts.BankIdLoginProviderName)
                   : base.OnGetAsync();
           }
    
           private bool ShouldForceBankIdLogin()
           {
               if (ReturnUrl.IsNullOrEmpty())
               {
                   return false;
               }
               var decodedReturnUrl = HttpUtility.UrlDecode(ReturnUrl);
               return decodedReturnUrl.Split("&").Any(p => p.Equals("bankid=true", StringComparison.OrdinalIgnoreCase));
           }
       }
    
    1. Angular UI Trigger:
      On the Angular side, you can navigate to the login page with the required query parameter to force BankID login:
    navigateToBankIdLogin() {
         this.authService.navigateToLogin({
           prompt: 'login',
           bankid: true
         });
       }
    

    Double-Click Issue

    The double-click issue is likely due to the login page being loaded first, and then the external login being triggered on the next navigation. This happens because the first navigation loads the login page, and only after the OnGetAsync logic runs does it redirect to the external provider.

    How to Improve

    • You can optimize the user experience by directly navigating to the external login endpoint from Angular, bypassing the intermediate login page. This can be done by constructing the external login URL and redirecting the user to it when BankID is required.
    • Alternatively, you can keep your current approach, as it is the officially supported extensibility point in ABP for customizing login logic.

    Summary

    • Overriding the LoginModel is the recommended and supported way in ABP to enforce external login for specific actions.
    • The double navigation is a side effect of the login page lifecycle; to avoid it, consider redirecting directly to the external login endpoint from the client.
    • There is no built-in ABP feature to "force" a specific provider without customizing the login logic as you have done.

    Sources


    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.

  • User Avatar
    0
    papusa created

    Hah, the AI-bot just copy-pasted my own code.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    The ShouldForceBankIdLogin method has no problem. You can use that way.

    I have to click the login button twice before the user is navigated to the BankID login, even though the prompt is set to 'login'. Bellow is a gif showing the issue

    Can you share a GIF and the har file of all requests?

    https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems#answer-3a1a1c4b-691e-3403-98b9-5f23fa024759

    Thanks

  • User Avatar
    0
    papusa created

    Hi, Check your e-mail please, har file should be there.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    It seems the first login doesn't succeed.

    Can you share the test project again?

    Thanks.

  • User Avatar
    0
    papusa created

    Sure, check your e-mail.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    It has no problem on my side. Can you show the problem in the demo project?

    Video: https://www.transfernow.net/en/cld?utm_source=20250923ak7xJHn3

    Thanks

  • User Avatar
    0
    papusa created

    It worked for me once, just like in your video, but now it only works after the second click. I’ve added a video illustrating the issue with demo project. Could you please take a look at the folder I shared with you earlier?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    ok, I will check the video and try again.

  • User Avatar
    0
    papusa created

    It skips Account/Login after connect/authorize.... That's what I found in logs:

    18:29:05 INF] The authorization request was successfully validated. [18:29:05 DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was successfully processed by OpenIddict.Server.OpenIddictServerHandlers+Authentication+ValidateAuthorizationRequest. [18:29:05 DBG] The event OpenIddict.Server.OpenIddictServerEvents+HandleAuthorizationRequestContext was successfully processed by OpenIddict.Server.OpenIddictServerHandlers+Authentication+AttachPrincipal. [18:29:05 DBG] The event OpenIddict.Server.OpenIddictServerEvents+HandleAuthorizationRequestContext was successfully processed by OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers+EnablePassthroughMode2[[OpenIddict.Server.OpenIddictServerEvents+HandleAuthorizationRequestContext, OpenIddict.Server, Version=6.2.1.0, Culture=neutral, PublicKeyToken=35a561290d20de2f],[OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters+RequireAuthorizationEndpointPassthroughEnabled, OpenIddict.Server.AspNetCore, Version=6.2.1.0, Culture=neutral, PublicKeyToken=35a561290d20de2f]]. [18:29:05 DBG] **The event OpenIddict.Server.OpenIddictServerEvents+HandleAuthorizationRequestContext was marked as skipped by OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers+EnablePassthroughMode**2[[OpenIddict.Server.OpenIddictServerEvents+HandleAuthorizationRequestContext, OpenIddict.Server, Version=6.2.1.0, Culture=neutral, PublicKeyToken=35a561290d20de2f],[OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters+RequireAuthorizationEndpointPassthroughEnabled, OpenIddict.Server.AspNetCore, Version=6.2.1.0, Culture=neutral, PublicKeyToken=35a561290d20de2f]]. [18:29:05 DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was successfully processed by OpenIddict.Server.OpenIddictServerHandlers+Authentication+HandleAuthorizationRequest. [18:29:05 DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was marked as skipped by OpenIddict.Server.OpenIddictServerHandlers+Authentication+HandleAuthorizationRequest.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Try to override the AuthorizeController and check the (request.HasPromptValue(OpenIddictConstants.PromptValues.Login) && request.GetParameter("bankid").HasValue)

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using OpenIddict.Abstractions;
    using OpenIddict.Server.AspNetCore;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.OpenIddict.Controllers;
    
    namespace BankIdDemo.BankId;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(AuthorizeController))]
    public class MyAuthorizeController : AuthorizeController
    {
        public override async Task<IActionResult> HandleAsync()
        {
            var request = await GetOpenIddictServerRequestAsync(HttpContext);
    
            var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
            if (result is not { Succeeded: true } ||
                ((request.HasPromptValue(OpenIddictConstants.PromptValues.Login) || request.MaxAge is 0 ||
                  (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
                    TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
                    TempData["IgnoreAuthenticationChallenge"] is null or false)
                || (request.HasPromptValue(OpenIddictConstants.PromptValues.Login) && request.GetParameter("bankid").HasValue))
            {
                // If the client application requested promptless authentication,
                // return an error indicating that the user is not logged in.
                if (request.HasPromptValue(OpenIddictConstants.PromptValues.None))
                {
                    return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.LoginRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
                        }!));
                }
    
                TempData["IgnoreAuthenticationChallenge"] = true;
    
                return Challenge(new AuthenticationProperties
                {
                    RedirectUri = Request.PathBase + Request.Path + QueryString.Create(Request.HasFormContentType ? Request.Form : Request.Query)
                });
            }
    
            return await base.HandleAsync();
        }
    }
    
    
  • User Avatar
    0
    papusa created

    Hi, It solves double click issue, but brings a new one. Even after successful BankID login it redirects back to BankID login page causing an endless loop. Check video bankid-login-issue-2-2025-09-24 100243 in the shared folder.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I will check it again.

    Thanks.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using OpenIddict.Abstractions;
    using OpenIddict.Server.AspNetCore;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.OpenIddict.Controllers;
    
    namespace BankIdDemo.BankId;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(AuthorizeController))]
    public class MyAuthorizeController : AuthorizeController
    {
        public override async Task<IActionResult> HandleAsync()
        {
            var request = await GetOpenIddictServerRequestAsync(HttpContext);
    
            var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
            if (result is not { Succeeded: true } ||
                ((request.HasPromptValue(OpenIddictConstants.PromptValues.Login) || request.MaxAge is 0 ||
                  (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
                    TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
                    TempData["IgnoreAuthenticationChallenge"] is null or false)
                || (request.HasPromptValue(OpenIddictConstants.PromptValues.Login) && request.GetParameter("bankid").HasValue) && !request.GetParameter("skipBankId").HasValue)
            {
                // If the client application requested promptless authentication,
                // return an error indicating that the user is not logged in.
                if (request.HasPromptValue(OpenIddictConstants.PromptValues.None))
                {
                    return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.LoginRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
                        }!));
                }
    
                TempData["IgnoreAuthenticationChallenge"] = true;
    
                var parameters = Request.HasFormContentType ? Request.Form.ToDictionary() : Request.Query.ToDictionary();
                if (request.HasPromptValue(OpenIddictConstants.PromptValues.Login) &&
                    request.GetParameter("bankid").HasValue)
                {
                    parameters.Add("skipBankId", "true");
                }
    
                return Challenge(new AuthenticationProperties
                {
                    RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
                });
            }
    
            return await base.HandleAsync();
        }
    }
    
    
  • User Avatar
    0
    papusa created

    Thanks, that works! I’m just wondering - could this actually be a bug in ABP/OpenIddict? The current solution feels more like a workaround. I thought prompt=login was supposed to always force authentication. According to the OpenID Connect spec:

    prompt OPTIONAL. Space-delimited, case-sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are: none The Authorization Server MUST NOT display any authentication or consent user interface pages. An error is returned if an End-User is not already authenticated or the Client does not have pre-configured consent for the requested Claims or does not fulfill other conditions for processing the request. The error code will typically be login_required, interaction_required, or another code defined in Section 3.1.2.6. This can be used as a method to check for existing authentication and/or consent. login The Authorization Server SHOULD prompt the End-User for reauthentication. If it cannot reauthenticate the End-User, it MUST return an error, typically login_required. consent The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client. If it cannot obtain consent, it MUST return an error, typically consent_required. select_account The Authorization Server SHOULD prompt the End-User to select a user account. This enables an End-User who has multiple accounts at the Authorization Server to select amongst the multiple accounts that they might have current sessions for. If it cannot obtain an account selection choice made by the End-User, it MUST return an error, typically account_selection_required.

    So it seems like the behavior here doesn’t fully match the spec. Are there any plans to fix this in future ABP releases?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi

    You are right. I will check this behavior.

    Thanks.

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.1.0-preview. Updated on October 14, 2025, 08:24