Activities of "mharnos"

Hi,

I didn't really get an answer for this topic before it was auto-closed. Could you have another look into this, or give me some feedback on what the right approach would be on providing global styling, that can override LeptonX Themes?

Kind Regards, Marc

Hello Mrs. Kurtuluş,

I was able to download the example project successfully and will look into it soon. Thank you for the example and explanation; I was somewhat aware that the data on the angular-routes was used for ABP.Routes (+Navbar generation +Breadcrum generation) but I'll look into your example in detail.

We have some other problems related to Navbar (in particular the groups are not selected correctly, so sometimes the Admin-Group gets selected, even though I'm in my application route) -> maybe if we change our code to align with your example this will be resolved too, otherwise I'll open another ticket for it 👍

I'll give feedback about this example shortly, but I'm not sure when exactly I'll be able to. (Maybe beginning of next week)

Thanks, Marc

Hi, while upgrading from 9.1.1 and 9.2.3 to 9.3.1 we're experiencing a regression in the angular frontend regarding the order of style injections. It seems that previously the injection order was preserved / similar to the one declared in angular.json which is good because we want to override some variables and styles from the lepton-x theme. This is no longer possible because the style injector is always injecting the lepton styles after the ones provided by angular.

Production (9.1.1):

Local (9.3.1):

Question 1) Do you know how I can load my style at the very bottom? Do I need to provide my own Style Load Factory / LPX_PRO_STYLE_TOKEN or LPX_LAYOUT_STYLE_FINAL or LPX_STYLE_FINAL?

Question 2) Is this something you're planning to fix? Because in the code I've seen this comment

      const linkElem = this.createLinkElem(style, direction, resolve);
      //TODO: find a better way for understand style laaded by angular json
      const appStyles = document.querySelector(
        'link[rel="stylesheet"][href*="styles"]'
      );

Which leads me to think that this could be improved.

Kind Regards, Marc

Hi, the ABP.Routes are already configured with the placeholder inside the path variable (like the AI suggested): /${path}/:exampleId/${EXAMPLE_ROUTE_NAMES.b}

Hi,

when I add an angular route for the path /examples/:exampleId how would I add the corresponding ABP.Route for this route entry? If I specify the same path (or undefined) and then navigate to that route, the Breadcrumbs + Icons are not correct (they return the fallback value for our main route at /)

Our route configuration currently looks like this:

export const EXAMPLE_ROUTE_NAMES = {
  a: 'subroute-a',
  b: 'subroute-b',
}

export const createExampleRoutes = (path: string, order?: number, group?: string): AbpRoute[] => {
  return [
    {
      path,
      canActivate: [ authGuard ],
      data: {
        routes: [
          {
            path: undefined,
            name: '::Menu:Examples',
            iconClass: 'fas fa-dolly-flatbed',
            layout: eLayoutType.application,
            order,
            group,
          },
          {
            path: `/${path}`,
            name: '::Menu:Examples:Overview',
            parentName: '::Menu:Examples',
            order: 1,
          },
          {
            path: `/${path}/:exampleId/${EXAMPLE_ROUTE_NAMES.a}`, // <<<<<< the issue is here ! >>>>>>>>
            name: '::Menu:Examples:A',
            parentName: '::Menu:Examples',
            order: 2,
          },
          {
            path: `/${path}/:exampleId/${EXAMPLE_ROUTE_NAMES.b}`, // <<<<<< the issue is here ! >>>>>>>>
            name: '::Menu:Examples:B',
            parentName: '::Menu:Examples',
            order: 3,
          },
        ],
      },
      children: [
        {
          path: '',
          component: ExampleComponent,
          title: '::Menu:Examples:Overview',
        },
        {
          path: ':exampleId', // <<<<< all components are injected with this `exampleId` >>>>>>>
          children: [
            {
              path: `${EXAMPLE_ROUTE_NAMES.a}`,
              component: ExampleComponent,
              title: '::Menu:Examples:A',
            },

            {
              path: `${EXAMPLE_ROUTE_NAMES.b}`,
              component: ExampleComponent,
              title: '::Menu:Examples:B',
            },
            // ... more routes ...
          ],
        },
      ],
    },
  ]
}

Do you have any idea how to provide the "Menu Configuration" / ABP.Route Data for those routes with path placeholders?

Kind Regards, Marc

Hello @maliming,

thank you for the support over e-mail and here in the forum; I can confirm, that outside of the IAbpClaimsPrincipalContributor itself, the claims are persisted in the correct / current Principal/Identity, so this is just an issue, where I was expecting the Contributors to be called with already updated contexts, but they all get the same (initial) context 👍 So it doesn't matter if the external login is not available for the third call, it was already written in the first one.

Nevertheless, we will probably stick with the "persist in database" solution for now, so we can a) easier see who has what role and claim and b) ExternalLogin context is not always available, e.g. after logging in, the External identity gets logged out, meaning, if I then (after some time) the user gets a new session, or if the application restarts, there is no external login context, but the user is still considered to be logged in (through cookie for example) - I think if we would rely on this, it would lead to some rather confusing bugs on our side, where a user "sometimes" has a claim, depending on whether he logged in through an external provider in the current session or not, and sometimes it wouldn't have those claims; worst, this would probably happen on any "deployment day" which is absolutely the worst time for a bug to happen 😅

I think overall it is easier to reason about this issue, with persisting it into AbpUserClaims and then it doesn't matter if the user is external or not.

Thanks again! Marc

Hi @maliming,

I've created a dummy project and sent it to your mail just now. The first e-mail was bounced, I hope you have received and can download the files from the second e-mail.

Kind Regards, Marc

Hi @maliming thanks for the suggestion.

I had a similar idea of using the IAbpClaimsPrincipalContributor but the Claims are not persisted if I use your code. I can see, that the identity from var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); has the new claim added when I call AddClaim, but the ContributeAsync is being called 3x during (external) login, and each time the Identity.Application is missing this newly added claim, so after adding it the first time, it is not available in the Claims the second, or third time that the method ContributeAsync is being called - it has no lasting effect on the Principal Identity.

In addition only the first two times GetExternalLoginInfoAsync returns a non-null response, on the third and final call, I have no ExternalLogin context left anymore. (I suppose this is when OpenIddict issues its own Identity?)

Currently my solution is, that I add the Claims (+Roles) into the database so that they are available as AbpUserClaims and AbpRoles:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Security.Claims;

public class ExternalRoleClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
{
    public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
    {
        var roleRepository = context.ServiceProvider.GetRequiredService<IIdentityRoleRepository>();
        var signInManager = context.ServiceProvider.GetRequiredService<AbpSignInManager>();
        var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();

        var availableRoles = (await roleRepository.GetListAsync())?
            .Select(i => i.NormalizedName)
            .ToHashSet();

        if (availableRoles == null)
        {
            return;
        }

        var externalLoginInfo = await signInManager.GetExternalLoginInfoAsync();
        if (externalLoginInfo == null)
        {
            return;
        }

        var user = await signInManager.UserManager.GetUserAsync(context.ClaimsPrincipal).ConfigureAwait(false);
        if (user == null)
        {
            return;
        }

        var externallyManagedRoles = configuration["ExternalIdentityMapping:ExternallyManagedRoles"]?
            .Split(',', StringSplitOptions.RemoveEmptyEntries)
            .Select(r => r.ToUppervariant())
            .ToHashSet();

        if (externallyManagedRoles.IsNullOrEmpty())
        {
            return;
        }

        await signInManager.UserManager.RemoveFromRolesAsync(user, externallyManagedRoles!).ConfigureAwait(false);

        var roleClaims = externalLoginInfo.Principal?
            .GetClaims(ClaimTypes.Role)
            .Where(roleClaim => availableRoles.Contains(roleClaim.ToUppervariant()))
            .Where(roleClaim => externallyManagedRoles!.Contains(roleClaim.ToUppervariant()))
            .ToList();

        if (roleClaims.IsNullOrEmpty())
        {
            return;
        }

        await signInManager.UserManager.AddToRolesAsync(user, roleClaims!).ConfigureAwait(false);
    }
}

And I had to modify the code for the authentication, because the dynamic options are not working for OpenIdConnect, I used your answer from https://abp.io/qa/questions/2468/3c6595db-97b4-07b0-2a33-3a0219d08a3c (but it would be cool if this was added to upstream)

    private void ConfigureExternalProviders(ServiceConfigurationContext context)
    {
        context.Services.AddAuthentication()
            // provide an empty DisplayName so the 'Microsoft/OpenIdConnect'-Button is not rendered on the login page
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "", options =>
            {
                options.CallbackPath = new PathString("/signin-microsoft");
                options.ResponseType = OpenIdConnectResponseType.Code;

                options.UsePkce = true;
                options.SaveTokens = true;
                options.SignInScheme = IdentityConstants.ExternalScheme;

                options.Scope.Add("email");
                options.Scope.Add("https://graph.microsoft.com/user.read");
                options.Scope.Add("https://graph.microsoft.com/user.readbasic.all");

                // this is required for ClaimActions, otherwise they're not evaluated (missing User json)
                // but we are calling a custom endpoint, so we don't have to set this to 'true'
                // options.GetClaimsFromUserInfoEndpoint = true;

                // copy the default microsoft account claim actions into this openidconnect handler
                var microsoftOptions = new MicrosoftAccountOptions();
                foreach (var claimAction in microsoftOptions.ClaimActions)
                {
                    options.ClaimActions.Add(claimAction);
                }

                options.ClaimActions.MapCustomJson(AbpClaimTypes.Picture,
                    user => user.GetString("id") == null ? null : "https://graph.microsoft.com/v1.0/me/photo/$value");

                options.ClaimActions.MapCustomJson(AbpClaimTypes.PhoneNumber,
                    user => user.GetString("mobilePhone") ?? user.TryGetStringArray("businessPhones").FirstOrDefault());

                options.ClaimActions.MapJsonKey("job_title", "jobTitle");

                options.Events = new OpenIdConnectEvents
                {
                    OnTokenValidated = CallCustomUserEndpointOnTokenValidated
                };
            })
            .WithDynamicOptions<OpenIdConnectOptions, OpenIdConnectHandler>(
                OpenIdConnectDefaults.AuthenticationScheme,
                options =>
                {
                    options.WithProperty<string?>(o => o.Authority);
                    options.WithProperty<string?>(o => o.ClientId);
                    options.WithProperty<string?>(o => o.ClientSecret, isSecret: true);
                }
            );

        // add bugfix from https://abp.io/qa/questions/2468/3c6595db-97b4-07b0-2a33-3a0219d08a3c
        context.Services.Replace(ServiceDescriptor
            .Scoped<AccountExternalProviderOptionsManager<OpenIdConnectOptions>,
                OpenIdAccountExternalProviderOptionsManager>());
    }

    private async Task CallCustomUserEndpointOnTokenValidated(TokenValidatedContext eventContext)
    {
        var identity = (ClaimsIdentity?)eventContext.Principal?.Identity;
        if (identity == null)
        {
            return;
        }

        var accessToken = eventContext.TokenEndpointResponse?.AccessToken;
        if (accessToken == null)
        {
            return;
        }

        // implementation copied from [Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountHandler.CreateTicketAsync]
        var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me");
        request.Version = eventContext.Options.Backchannel.DefaultRequestVersion;
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        var response =
            await eventContext.Options.Backchannel.SendAsync(request, eventContext.HttpContext.RequestAborted);
        response.EnsureSuccessStatusCode();

        using var payload =
            JsonDocument.Parse(await response.Content.ReadAsStringAsync(eventContext.HttpContext.RequestAborted));
        foreach (var action in eventContext.Options.ClaimActions)
        {
            action.Run(payload.RootElement, identity, eventContext.Options.ClaimsIssuer ?? eventContext.Scheme.Name);
        }
    }
public class OpenIdAccountExternalProviderOptionsManager : AccountExternalProviderOptionsManager<OpenIdConnectOptions>
{
    private readonly OpenIdConnectPostConfigureOptions _openIdConnectPostConfigureOptions;

    public OpenIdAccountExternalProviderOptionsManager(
        IOptionsFactory<OpenIdConnectOptions> factory,
        IAccountExternalProviderAppService accountExternalProviderAppService,
        IStringEncryptionService stringEncryptionService,
        ITenantConfigurationProvider tenantConfigurationProvider,
        IEnumerable<IPostConfigureAccountExternalProviderOptions<OpenIdConnectOptions>> postConfigures,
        ICurrentTenant currentTenant,
        IDataProtectionProvider dataProtection) :
        base(factory, accountExternalProviderAppService, stringEncryptionService, tenantConfigurationProvider, postConfigures, currentTenant)
    {
        _openIdConnectPostConfigureOptions = new OpenIdConnectPostConfigureOptions(dataProtection);
    }

    protected async override Task OverrideOptionsAsync(string name, OpenIdConnectOptions options)
    {
        await base.OverrideOptionsAsync(name, options);
        _openIdConnectPostConfigureOptions.PostConfigure(name, options);
    }
}

Not sure if this a good idea though, calling the DB during those contributor calls; what do you think? Kind Regards, Marc

Hi,

the application we're currently developing has a couple of tenants, that already have established access management solutions they want to keep using. We have already agreed on using specific role names, that they can manage through Entra App. Registrations with specific App Roles, so they can keep their internal Security Group naming, but the specific role names can be recognized on our end. This works fine on a technical level, and is also an established way of handling such requests, e.g. Grafana describes the same setup for access to Entra / AzureAD with app roles like Viewer, Editor or Admin.

I have verified that I can access the roles when performing an external login, through either AddMicrosoftAccount or AddOpenIdConnect, in both cases I have an authentication event I can use to access the roles claims from the returned id_token:

This microsoft azure example application also works as expected and I can see the assigned roles for my external identity: https://github.com/azure-samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/5-WebApp-AuthZ/5-1-Roles/README.md

I have seen the articles and other questions regarding the general question of how to use the external identity / login with SSO:

But at the moment I have not found a definitive answer on how to make those claims available in ABP itself. My current best guess is, to use the dynamic claims concept and re-read the id token with SignInManager when the user first registers, logs in, and maybe even periodically, and remove + add all roles "managed" by the external identity?

I don't think that I have to use the Claims Principal Factory itself, because the role claim is already exposed to the OpenIddict access_token and id_token by default: https://abp.io/docs/latest/modules/openiddict#updating-claims-in-access_token-and-id_token or is that the correct mechanisms to "create" this claim through SignInManager when OpenIddic accesses the claims? https://abp.io/docs/latest/framework/fundamentals/authorization#claims-principal-factory

How would you suggest that I implement this kind of "forwarding" of claims? What the most idiomatic way of doing this with ABP? Because hooking into the LoginModel seems kinda hacky.

Kind Regards, Marc

Hi,

we had a similar issue, where if the returnUrl points to a guarded route (the default points to / / rootUrl and is not configurable) then the authGuard code would run in parallel to the angular-oauth2-oidc authentication code resulting in a loop if the authGuard was faster than the authentication code: https://github.com/manfredsteyer/angular-oauth2-oidc/issues/861

Our solution was to create an additional route in angular oauth/code that has no authGuard and just displays a placeholder with the empty layout (no menus) eLayoutType.empty, and the angular-oauth2-oidc code handles the redirect to the redirectUrl (not returnUrl) when it is finished with authentication. The redirectUrl can be authGuarded as you would expect.

We had to update the oAuthConfig in our environment.ts files (redirectUri: baseUrl + '/oauth/code') and extend the OpenIddictDataSeedContributor where the redirectUri in CreateApplicationAsync is always set to be the rootUrl, i.e. redirectUris: new List<string> { consoleAndAngularClientRootUrl } needs to be changed to include /oauth/code as well.

That resolved our issue, but it would've made it significantly easier to configure, if the "OpenIddict:Applications" config would include the RedirectUris and PostLogoutRedirectUris options instead of assuming they're RootUrl or point to a ABP configured, non-AuthGuard-ed route per default.

Hope this helps, Marc

Showing 1 to 10 of 11 entries
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.0.0-preview. Updated on September 18, 2025, 07:10