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)
-
0
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
- Override the LoginModel:
By overriding theOnGetAsync
method in your customLoginModel
, 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)); } }
- 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
- ABP Account Module Documentation
- How to Customize the Login Page for MVC / Razor Page Applications
- Related Support Ticket
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.
- Override the LoginModel:
-
0
Hah, the AI-bot just copy-pasted my own code.
-
0
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
-
0
Hi, Check your e-mail please, har file should be there.
-
0
hi
It seems the first login doesn't succeed.
Can you share the test project again?
Thanks.
-
0
Sure, check your e-mail.
-
0
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
-
0
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?
-
0
ok, I will check the video and try again.
-
0
It skips
Account/Login
afterconnect/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+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+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. -
0
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(); } }
-
0
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. -
0
hi
I will check it again.
Thanks.
-
0
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(); } }
-
0
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?
-
0
Hi
You are right. I will check this behavior.
Thanks.