We have integrated an external login provider (BankID) into our ABP application v9 (Layered/Angular/No separate auth). Login flow works, but two issues remain regarding claims handling and distinguishing login type:
- Forwarding External Claims
- During the external login callback, BankID provides additional claims (e.g., national identity number, date of birth).
- I can see these claims in the external cookie (IdentityConstants.ExternalScheme).
- However, once ABP finalizes the login and issues tokens for the Angular app, these claims are gone.
- I tried implementing IAbpClaimsPrincipalContributor to forward them, but the external cookie is not available for angular app (different domain/port).
- Question: What is the proper ABP way to forward external provider claims (without persisting sensitive values like national identity number in the DB) so that they appear in the JWT used by the Angular client?
- Recognizing Authentication Type in API Calls
- For security logic, I need to know if the current user authenticated via BankID or the standard username/password login.
- In Swagger tests, I can inject and see custom claims, but once Angular is used, claims seem to be rewritten and the info is lost.
- Question: What is the recommended approach in ABP to tag the authentication method (e.g., extenal (BankID) vs. local) so that this info reliably propagates into JWT tokens consumed by the Angular frontend?
Notes: I don’t want to persist national identity number in the database, so I’m looking for a runtime claim-forwarding approach. I’ve already explored AbpUserClaimsPrincipalFactory and IAbpClaimsPrincipalContributor. Claims show up in Swagger tests but not from Angular.
15 Answer(s)
-
0
- Forwarding External Claims to JWT (Without Persisting in DB)
To forward external provider claims (like national identity number, date of birth) into the JWT issued to the Angular client, without persisting them in the database, you need to inject these claims at the token creation stage. The recommended ABP approach is to use a custom IAbpClaimsPrincipalContributor or override the claims principal creation logic, but you must ensure these claims are available at the time the JWT is generated.
However, since the external cookie is not available in the Angular app (due to different domain/port), you should transfer the required claims from the external identity to the local identity during the external login callback (OnGetExternalLoginCallbackAsync in LoginModel). You can do this by:
- Reading the external claims from the external login info.
- Adding the required claims to the ClaimsPrincipal that will be used to generate the JWT (but not persisting them in the user database).
- Use IAbpClaimsPrincipalContributor to add these claims at runtime, but only if you can access them (e.g., via TempData, session, or a custom mechanism during the login flow).
Example (in LoginModel.OnGetExternalLoginCallbackAsync):
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); var nationalId = externalLoginInfo.Principal.FindFirst("national_identity_number")?.Value; // Store in a temp location (e.g., ClaimsPrincipal, session, or custom claim) var claims = new List<Claim>(); if (!string.IsNullOrEmpty(nationalId)) { claims.Add(new Claim("national_identity_number", nationalId)); } // Pass these claims to the ClaimsPrincipal used for JWT generation
You must ensure these claims are added to the ClaimsPrincipal before the JWT is created. Do not persist them in the database if you want to avoid storage.
- Tagging Authentication Method in JWT
To distinguish between BankID and local login, add a custom claim (e.g., "auth_method": "bankid" or "local") to the ClaimsPrincipal during the login process. This can be done in the same place as above (external login callback or in a custom IAbpClaimsPrincipalContributor).
Example:
claims.Add(new Claim("auth_method", "bankid")); // or "local"
This claim will then be included in the JWT and available to your Angular app and API logic.
Key Points:
- Claims must be present in the ClaimsPrincipal at the time the JWT is generated.
- Use the external login callback to transfer claims from the external provider to the local identity.
- Do not persist sensitive claims in the database; just add them to the ClaimsPrincipal for the current session/token.
- For tagging authentication type, always add a custom claim during login.
If you need to customize the OpenIddict token creation pipeline for more advanced scenarios, you can implement a custom grant type or event handler to inject claims at token issuance.
Sources:
- ABP Authorization & Claims
- How to add a custom grant type in OpenIddict
- GitHub: Custom External Login Provider Example
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.
-
0
AI answer didn't solve the problem.
-
0
hi
Can you share a demo project and a test username with password?
You can create a private GitHub repository and invite https://github.com/maliming
liming.ma@volosoft.com
I will change the code and add custom claims.
Thanks.
-
0
Hi, Thanks for your response.
Could you clarify the purpose of the demo project? I’m unable to share external provider credentials, so I'm not sure how useful it would be in this case.
If you’re familiar with retrieving additional claims, perhaps you could share a demo that demonstrates the solution? :)
Here is how external provider is added in our app:
private void ConfigureExternalProviders(ServiceConfigurationContext context, IConfiguration configuration) { var criiptoAuthority = configuration["Authentication:Criipto:Authority"]; var criiptoClientId = configuration["Authentication:Criipto:ClientId"]; var criiptoClientSecret = configuration["Authentication:Criipto:ClientSecret"]; context.Services.AddAuthentication() .AddOpenIdConnect("Criipto", "BankID", options => { options.ClientId = criiptoClientId; options.ClientSecret = criiptoClientSecret; options.Authority = criiptoAuthority; options.ResponseType = OpenIdConnectResponseType.Code; options.CallbackPath = new PathString("/signin-bankid"); options.Scope.Add("ssn"); options.ClaimActions.MapJsonKey("socialno", "socialno"); options.ClaimActions.MapJsonKey("dateofbirth", "dateofbirth"); }); }
-
0
hi
You can check this case. It's basically the same with you.
https://abp.io/support/questions/9721/What-is-the-recommended-way-of-making-IdentityExternal-Claims-available-in-IdentityApplication#answer-3a1baf9e-4c09-c9fc-9d6c-c193a681c7bf
If you can share a template/demo project, I can add code to test it.
Thanks.
-
0
Previously, I was using
HttpContextAccessor.HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme)
to retrieve the external claims principal.After reviewing the link you provided, I switched to using
SignInManager
, but the behavior hasn't changed. When logging into the Angular app, the external login is null, and I'm not receiving the expected claims when calling the API from the Angular front end.Here's the code for my
IAbpClaimsPrincipalContributor
implementation. Could you please take a look and let me know if there's anything wrong or missing?public class BankIdClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency { private readonly SignInManager<IdentityUser> _signInManager;
public BankIdClaimsPrincipalContributor(SignInManager<IdentityUser> signInManager) { _signInManager = signInManager; } public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context) { var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); var externalLogin = await _signInManager.GetExternalLoginInfoAsync(); if (externalLogin == null) { identity?.AddClaim(new Claim("isbankidauthenticated", false.ToString().ToLower())); return; } var authenticationTypeClaim = externalLogin.Principal.FindFirst(Claims.BankId.AuthenticationType); var isBankIdAuthenticated = authenticationTypeClaim is { Value: Claims.BankId.NorwegianAuthenticationTypeValue }; identity?.AddClaim(new Claim("isbankidauthenticated", isBankIdAuthenticated.ToString().ToLower())); if (!isBankIdAuthenticated) { return; } ForwardClaim("socialno", "nationalidentitynumber"); ForwardClaim("dateofbirth", "dateofbirth"); return; void ForwardClaim(string bankIdClaimType, string claimType) { var claim = externalLogin.Principal.FindFirst(bankIdClaimType); if (claim != null) { identity?.AddClaim(new Claim(claimType, claim.Value)); } } }
}
I think it might be more convenient if you could create a demo project yourself with the setup you have in mind. :)
-
0
hi
I can create a demo project, but I don't have an external user account with extra claims.
That’s why I ask you to share a project and test BankID user info.
Thanks.
-
0
I can definitely create and share a demo project with you, but please note that I won’t be able to include the external login provider credentials (like the client secret), since those are sensitive. Would that still work for you?
I can provide a test user (test identity number), or you're welcome to generate one yourself using this link: https://ra-preprod.bankidnorge.no/#/generate We're using Criipto for Norwegian BankID. You can get test account for free.
That said, I assumed the issue wasn't specific to BankID, and that any external login provider would be sufficient for testing in a demo setup. Please let me know how you'd prefer to proceed!
-
0
hi
I will test your case with GitHub login.
Is that ok for you?
You can create an Angular template project and share it with liming.ma@volosoft.com
Thanks.
-
0
I will test your case with GitHub login.
Sounds good. Thanks
-
0
Please create an Angular template project and share it with liming.ma@volosoft.com
Thanks.
-
0
I've shared a link to the BankID demo project via OneDrive. You should receive e-mail with link.
While testing the demo, I noticed that the issue only occurs when an external login is added to an existing user. If the user is registered directly via BankID, the problem doesn't appear.
Here are the steps to reproduce the issue:
- Log in using any local user (e.g., admin).
- Navigate to External Logins.
- Add an external BankID account using the following test credentials:
- National identity number:
04080599469
- One-time code:
otp
- BankID password:
qwer1234
- National identity number:
- After linking the external login, log out.
- Log in using BankID.
- In the Angular app's Home page, click the "Test national identity claim" button.
- Expected: It should display the national identity number. Actual: It shows a message indicating the claim is not found.
-
0
hi
Add
BankIdOpenIddictServerHandler
to your AuthServer projectpublic override void PreConfigureServices(ServiceConfigurationContext context) { PreConfigure<OpenIddictServerBuilder>(builder => { builder.AddEventHandler(BankIdOpenIddictServerHandler.Descriptor); }); }
using System; using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using OpenIddict.Abstractions; using OpenIddict.Server; namespace BankIdDemo; public class BankIdOpenIddictServerHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ProcessSignInContext> { public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.ProcessSignInContext>() .UseSingletonHandler<BankIdOpenIddictServerHandler>() .SetOrder(100_000) .SetType(OpenIddictServerHandlerType.Custom) .Build(); public async ValueTask HandleAsync(OpenIddictServerEvents.ProcessSignInContext context) { if (context is { EndpointType: OpenIddictServerEndpointType.Authorization, AuthorizationCodePrincipal: not null}) { var httpContext = context.Transaction.GetHttpRequest()?.HttpContext; if (httpContext == null) { return; } var identity = await httpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); if (identity?.Principal == null) { return; } var nationalidentitynumber =identity?.Principal?.FindFirst("nationalidentitynumber")?.Value; if (!nationalidentitynumber.IsNullOrWhiteSpace()) { context.AuthorizationCodePrincipal?.AddClaim("nationalidentitynumber", nationalidentitynumber); } return; } if (context is { EndpointType: OpenIddictServerEndpointType.Token, AccessTokenPrincipal: not null}) { var nationalidentitynumber = context.AccessTokenPrincipal?.FindFirst("nationalidentitynumber")?.Value; if (!nationalidentitynumber.IsNullOrWhiteSpace()) { context.AccessTokenPrincipal?.AddClaim("nationalidentitynumber", nationalidentitynumber); } } } }
-
0
It works. Thank you!
-
0
Great!