Activities of "maliming"

Hi,

When you say "needs to pass from the host to AuthServer", I think the controller has ended up in the wrong project. In a Tiered setup the SAML IdP endpoints must live inside *.AuthServer, not in *.Web or *.HttpApi.Host. The Account login page, the application cookie (IdentityConstants.ApplicationScheme), and OpenIddict all run on AuthServer — if the controller is anywhere else, [Authorize] redirects to a login page that doesn't exist on that host, and the AuthServer cookie isn't visible there either. There's nothing to "pass between hosts"; the controller and the login page have to share one host.

I built a fresh ABP 10.3 Tiered MVC solution end-to-end and confirmed the wiring works at runtime (/Saml/ExportMetadata returns the IdP XML signed with the local cert, and [Authorize] on the SAML actions redirects to /Account/Login?ReturnUrl=... preserving the full SAMLRequest query string even at 3 KB+). Here are the exact four diffs.

1. src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj

<PackageReference Include="ComponentSpace.Saml2" Version="6.1.0" />

2. src/MyCompanyName.MyProjectName.AuthServer/*AuthServerModule.cs

One line inside ConfigureServices. No app.UseSaml() — that extension is only for ComponentSpace's middleware mode, your controller uses the API-injection mode and doesn't need it.

public override void ConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();
    // ... existing ABP configuration ...

    context.Services.AddSaml(configuration.GetSection("SAML"));
}

3. src/MyCompanyName.MyProjectName.AuthServer/Controllers/SamlController.cs

Almost identical to the ComponentSpace ExampleIdentityProvider controller. The only change is the SignInManager generic — must be ABP's user type, with full namespace to avoid confusion with Microsoft.AspNetCore.Identity.IdentityUser.

using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using ComponentSpace.Saml2;
using ComponentSpace.Saml2.Assertions;
using ComponentSpace.Saml2.Metadata.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace MyCompanyName.MyProjectName.Controllers;

[Route("[controller]/[action]")]
public class SamlController : Controller
{
    private readonly SignInManager<Volo.Abp.Identity.IdentityUser> _signInManager;
    private readonly ISamlIdentityProvider _samlIdentityProvider;
    private readonly IConfigurationToMetadata _configurationToMetadata;
    private readonly IConfiguration _configuration;

    public SamlController(
        SignInManager<Volo.Abp.Identity.IdentityUser> signInManager,
        ISamlIdentityProvider samlIdentityProvider,
        IConfigurationToMetadata configurationToMetadata,
        IConfiguration configuration)
    {
        _signInManager = signInManager;
        _samlIdentityProvider = samlIdentityProvider;
        _configurationToMetadata = configurationToMetadata;
        _configuration = configuration;
    }

    [Authorize]
    public async Task<IActionResult> InitiateSingleSignOn()
    {
        var userName = User?.Identity?.Name;
        var attributes = new List<SamlAttribute>
        {
            new SamlAttribute(ClaimTypes.Email, User?.FindFirst(ClaimTypes.Email)?.Value),
            new SamlAttribute(ClaimTypes.GivenName, User?.FindFirst(ClaimTypes.GivenName)?.Value),
            new SamlAttribute(ClaimTypes.Surname, User?.FindFirst(ClaimTypes.Surname)?.Value),
        };
        var partnerName = _configuration["PartnerName"];
        var relayState = _configuration["RelayState"];
        await _samlIdentityProvider.InitiateSsoAsync(partnerName, userName, attributes, relayState);
        return new EmptyResult();
    }

    public async Task<IActionResult> SingleSignOnService()
    {
        var idpSsoResult = await _samlIdentityProvider.ReceiveSsoAsync();
        if (User.Identity is not null && User.Identity.IsAuthenticated)
        {
            await CompleteSsoAsync(idpSsoResult.CorrelationID);
            return new EmptyResult();
        }
        return RedirectToAction(nameof(SingleSignOnServiceCompletion), new { idpSsoResult.CorrelationID });
    }

    [Authorize]
    public async Task<IActionResult> SingleSignOnServiceCompletion(string correlationID)
    {
        await CompleteSsoAsync(correlationID);
        return new EmptyResult();
    }

    public async Task<IActionResult> SingleLogoutService()
    {
        var sloResult = await _samlIdentityProvider.ReceiveSloAsync();
        if (sloResult.IsResponse)
        {
            if (sloResult.HasCompleted && !string.IsNullOrEmpty(sloResult.RelayState))
                return LocalRedirect(sloResult.RelayState);
            return Redirect("~/");
        }
        await _signInManager.SignOutAsync();
        await _samlIdentityProvider.SendSloAsync(correlationID: sloResult.CorrelationID);
        return new EmptyResult();
    }

    public async Task<IActionResult> ArtifactResolutionService()
    {
        await _samlIdentityProvider.ResolveArtifactAsync();
        return new EmptyResult();
    }

    public async Task<IActionResult> ExportMetadata()
    {
        var entityDescriptor = await _configurationToMetadata.ExportAsync();
        var xmlElement = entityDescriptor.ToXml();
        Response.ContentType = "text/xml";
        Response.Headers["Content-Disposition"] = "attachment; filename=\"metadata.xml\"";
        var xmlWriterSettings = new XmlWriterSettings { Async = true, Encoding = Encoding.UTF8, Indent = true, OmitXmlDeclaration = true };
        await using var xmlWriter = XmlWriter.Create(Response.Body, xmlWriterSettings);
        xmlElement.WriteTo(xmlWriter);
        await xmlWriter.FlushAsync();
        return new EmptyResult();
    }

    private async Task CompleteSsoAsync(string correlationID)
    {
        var userName = User?.Identity?.Name;
        var attributes = new List<SamlAttribute>
        {
            new SamlAttribute(ClaimTypes.Email, User?.FindFirst(ClaimTypes.Email)?.Value),
            new SamlAttribute(ClaimTypes.GivenName, User?.FindFirst(ClaimTypes.GivenName)?.Value),
            new SamlAttribute(ClaimTypes.Surname, User?.FindFirst(ClaimTypes.Surname)?.Value),
        };
        await _samlIdentityProvider.SendSsoAsync(userName, attributes, correlationID: correlationID);
    }
}

About _signInManager.SignOutAsync() in SingleLogoutService: that only clears the application cookie. ABP's own /Account/Logout (see Volo.Abp.Account.Public.Web.Pages.Account.LogoutModel.OnGetAsync) also writes an IdentitySecurityLog audit entry and clears the ConfirmUser, ChangePassword, and LockedOut auxiliary schemes. If your SLO needs parity with /Account/Logout, mirror those steps before SendSloAsync.

4. src/MyCompanyName.MyProjectName.AuthServer/appsettings.json

The exact field names that ComponentSpace's JSON binder expects (verified against ComponentSpace.Saml2.Configuration.SamlConfigurations, SamlConfiguration, LocalIdentityProviderConfiguration, PartnerServiceProviderConfiguration in v6.1.0):

"SAML": {
  "Configurations": [
    {
      "LocalIdentityProviderConfiguration": {
        "Name": "https://your-authserver-host/saml",
        "Description": "Local IdP",
        "SingleSignOnServiceUrl": "https://your-authserver-host/Saml/SingleSignOnService",
        "SingleLogoutServiceUrl": "https://your-authserver-host/Saml/SingleLogoutService",
        "LocalCertificates": [
          {
            "FileName": "saml.pfx",
            "Password": "<cert-password>"
          }
        ]
      },
      "PartnerServiceProviderConfigurations": [
        {
          "Name": "<sp-entity-id>",
          "Description": "Partner SP",
          "AssertionConsumerServiceUrl": "https://partner-sp-host/saml/acs",
          "SignSamlResponse": true,
          "SignAssertion": false,
          "WantAuthnRequestSigned": false
        }
      ]
    }
  ]
}

The Name in LocalIdentityProviderConfiguration is your IdP entity ID. Each entry in PartnerServiceProviderConfigurations is one SP partner; the Name there must match the SP's entity ID. For dev/PoC you can sign with the openiddict.pfx you already have; for production use a real .pfx from your PKI or a X509Store reference.

The partner SP needs your IdP metadata to know where to send SAMLRequest. Hit https://your-authserver-host/Saml/ExportMetadata to download it — that's the file you give them.

Once your SamlApplication entity goes live

Drop the static PartnerServiceProviderConfigurations array and implement ComponentSpace.Saml2.Configuration.Resolver.ISamlConfigurationResolver to translate your SamlApplication rows into SamlConfiguration / PartnerServiceProviderConfiguration objects, then register it via services.AddSamlConfigurationResolver<YourResolver>(). Do this after the static-config flow works end-to-end so you have a known-good baseline.

Want me to apply this to your code?

I can't share my throwaway test solution directly, but if you can do this it'll be much faster than us going back and forth:

  1. Spin up a fresh ABP 10.3 Tiered MVC project via ABP Studio or the abp new CLI (or take a stripped-down copy of yours).
  2. Push it to a private GitHub repository (please make sure it's marked private — don't use a public repo for code with your SAML config or any cert references).
  3. Invite maliming as a collaborator.

I'll push the four diffs above as a branch / PR against your repo so you can see them applied in context and run it locally to verify the redirect flow.

Thanks

Answer

Your current ticket has been refunded.

Answer

hi

We are testing the new MudBlazor template in Studio. It is expected that the next patch version will be available.

Thank you for your attention

Hi,

Reproduced the wiring locally on a fresh ABP 10.3 Tiered MVC AuthServer with ComponentSpace.Saml2 6.1.0. The good news is the surface area is small — the SamlController you have from the ComponentSpace sample needs almost no changes; the work is mostly in dropping the parts of the sample that fight ABP's Account module. Here are the concrete points:

1. Don't carry the sample's authentication wiring across. Skip the sample's Program.cs AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(...) and skip LoginModel.cs / AccountController.cs. ABP AuthServer already serves /Account/Login and uses IdentityConstants.ApplicationScheme as its default cookie, so [Authorize] on your SAML controller automatically falls through to the ABP login page and back. Adding the sample's cookie scheme on top will clash with ABP's.

2. AuthServer csproj + module registration. Add the package and one line of DI registration:

<PackageReference Include="ComponentSpace.Saml2" Version="6.1.0" />

In *.AuthServerModule.ConfigureServices:

context.Services.AddSaml(configuration.GetSection("SAML"));

Do not call app.UseSaml() — that extension lives in SamlMiddlewareApplicationBuilderExtensions and is only required for ComponentSpace's middleware-based IdP pattern (AddSamlMiddleware + Events.OnInitiateSso). Your code uses the controller pattern (injecting ISamlIdentityProvider and calling ReceiveSsoAsync / SendSsoAsync directly), so AddSaml(...) alone is enough. I verified this on the local repro — removing UseSaml() still builds and the controller routes resolve.

3. The only edit to SamlController is the SignInManager generic type. The sample injects SignInManager<IdentityUser> (the default ASP.NET Core Identity user type). In ABP, replace it with the ABP user type — full namespace to avoid mistakes:

private readonly SignInManager<Volo.Abp.Identity.IdentityUser> _signInManager;

Volo.Abp.Identity.IdentityUser is a different type from Microsoft.AspNetCore.Identity.IdentityUser; ABP registers AbpSignInManager : SignInManager<Volo.Abp.Identity.IdentityUser>, so injection resolves correctly. Everything else in the controller (User.Identity.Name, User.FindFirst(ClaimTypes.Email), ClaimTypes.GivenName, ClaimTypes.Surname) works as-is because ABP's AbpUserClaimsPrincipalFactory populates the standard ClaimTypes.* values onto the cookie principal by default.

4. About _signInManager.SignOutAsync() in SingleLogoutService. This is the minimum needed to clear the application cookie. ABP's own /Account/Logout (Volo.Abp.Account.Public.Web.Pages.Account.LogoutModel.OnGetAsync in Volo.Abp.Account.Pro.Public.Web) does more: it writes an IdentitySecurityLog audit entry and clears the ConfirmUser, ChangePassword, and LockedOut auxiliary schemes. If your SLO needs parity with /Account/Logout, mirror those steps in SingleLogoutService before calling _samlIdentityProvider.SendSloAsync.

5. SAML partner configuration — get the IdP vs SP roles right. Put the SAML section in *.AuthServer/appsettings.json using the same JSON layout the ComponentSpace sample uses (LocalIdentityProviderConfiguration + PartnerServiceProviderConfigurations). Two URLs that are easy to mix up:

  • In your AuthServer's SAML config, each partner SP entry holds the SP-side AssertionConsumerServiceUrl — that's the SP's own endpoint where your IdP POSTs the SAMLResponse.
  • In each partner SP's SAML config (the third-party app), the IdP SingleSignOnServiceUrl must point at your AuthServer's controller route — by default https://auth.example.com/Saml/SingleSignOnService given your [Route("[controller]/[action]")]. The metadata XML returned by your ExportMetadata action gives them the full set.

For PoC, the signing certificate can be a .pfx file referenced from the AuthServer project. For production, point ComponentSpace at a cert in X509Store / Key Vault rather than shipping the private key inside the published artifact.

6. SamlApplication DDD module. When you're ready to drive partner configuration from your ABP entity instead of appsettings.json, implement ComponentSpace's ISamlConfigurationResolver (or use ComponentSpace.Saml2.Configuration.Database) to translate your SamlApplication rows into PartnerServiceProviderConfiguration objects. Recommend doing this after the static-config path is working end-to-end, so the SAML cert / endpoint / cookie issues are isolated from the resolver wiring.

Scope of what I verified: I reproduced the AuthServer module changes and the SamlController (with the ABP SignInManager<IdentityUser> swap) on a fresh ABP 10.3 Tiered MVC solution and confirmed it builds and DI resolves. Full SSO/SLO at runtime still needs real partner metadata, signing certs, and a browser round-trip through the SP to verify end-to-end.

Thanks

: )

Hi,

azure_ad_token is not a standard OAuth/OIDC grant type, and ABP/OpenIddict does not register it out of the box. The unsupported_grant_type error you see is returned by the OpenIddict server middleware itself — the request never even reaches ABP's TokenController, because OpenIddict rejects any grant that has not been registered on the server and granted to the client.

The reason web works but mobile does not is unrelated to the grant type: AddMicrosoftAccount(...) is a cookie-based external login handler used by the browser interactive flow (/Account/Login → redirect to Microsoft → /signin-microsoft callback → /connect/authorize → /connect/token with grant_type=authorization_code). It does not register anything on the /connect/token pipeline. So your web client succeeds with the standard authorization_code grant, while your mobile client tries a custom grant that no one ever wired up.

Flutter should use Authorization Code + PKCE against the ABP AuthServer

Do not let the Flutter app talk to Azure AD directly. Instead, open a system browser from Flutter pointing at the ABP AuthServer login page, let the user click "Login with Microsoft" (which goes through your existing AddMicrosoftAccount setup), and exchange the resulting authorization code for an ABP access token + refresh token.

Server-side: no changes needed. The Pro mobile client already has authorization_code + refresh_token granted by default in OpenIddictDataSeedContributor. Just make sure the redirect URI of XXX_MobileApp matches your Flutter app's custom scheme.

Flutter side, using the flutter_appauth package:

import 'package:flutter_appauth/flutter_appauth.dart';

const appAuth = FlutterAppAuth();

final result = await appAuth.authorizeAndExchangeCode(
  AuthorizationTokenRequest(
    'XXX_MobileApp',
    'com.yourcompany.yourapp:/oauthredirect', // must match the redirect URI seeded for XXX_MobileApp
    issuer: 'https://authserver.xxx.com',
    scopes: ['openid', 'profile', 'email', 'offline_access'],
  ),
);

final abpAccessToken = result.accessToken;
final abpRefreshToken = result.refreshToken;

This reuses the exact same Microsoft external login flow that already works on web, and refresh token rotation comes for free.

Thanks

Hi Raff,

Yes — when you create a layered (MVC) tiered solution with a recent ABP Studio, the official rules ship inside the solution itself. I just generated a fresh MVC + Tiered + EF/PostgreSQL solution on ABP 10.4.0 to confirm. Two folders to look at in your solution root:

.abpstudio/ai-rules/ — loaded by the Studio AI Agent automatically.

  • app.mdc — describes the DDD layering, layer dependencies, the Web (MVC/Razor Pages) project, Mapperly conventions, etc. (alwaysApply: true, so it's injected into every agent conversation.)

.cursor/rules/ — these are written for Cursor IDE (.mdc files). For your MVC + Tiered + EF setup the template only emits the relevant ones:

  • template/app.mdc
  • mcp-studio.mdc
  • framework/common/ — abp-core, application-layer, authorization, cli-commands, ddd-patterns, dependency-rules, development-flow, infrastructure, multi-tenancy
  • framework/data/ef-core.mdc
  • framework/ui/mvc.mdc
  • framework/testing/patterns.mdc

The UI/data ones are auto-pruned for your stack (no Angular/Blazor/Mongo rules in your solution since you didn't pick them). If you want any of these to also be used by the Studio AI Agent, copy the content into a new .mdc file under .abpstudio/ai-rules/ (solution-level, shared with team) or ~/.abp/studio/rules/ (user-level, all your solutions). Studio also has an Import Skills button under AI Agent Settings, but note that one currently only accepts .md files, not .mdc.

If your existing project was created with an older Studio and these folders aren't there, the easiest path is: create a one-off MVC Tiered solution with current Studio, copy .abpstudio/ai-rules/ and .cursor/rules/ into your real project, then commit. They live with your solution and travel with the team.

For workflows, there's nothing pre-shipped — you author those yourself per solution in AI Agent Settings → Workflow Settings. A typical first one: add a Build task as an After step so the agent verifies its own changes each iteration.

Thanks

Hi,

Thanks for the details — I want to make sure I point you at the right ABP extension point before suggesting anything, so one more round of clarification on the SAML library, because a few things in the snippets don't match the ComponentSpace public API I have access to:

  1. The AddSamlMiddleware(idp => { idp.EntityId = ...; idp.BaseUrl = ...; idp.SigningCertificate = ...; idp.AssertionLifetime = ...; idp.SessionLifetime = ...; idp.OnSignOut = ...; }) shape doesn't match ComponentSpace's SamlMiddlewareOptions (which only exposes LoginUrl, PartnerName, Events, InitiateSingleSignOnPath, SingleSignOnServiceCompletionPath, PartnerNameParameter). Same with the ISamlUserResolver / SamlUserInfo / SpResolvingSamlUserResolver types and the SamlServiceProvider.AllowedClaimTypes / AttributeMappings properties — they aren't in ComponentSpace's documented surface either.

    Is this a wrapper / abstraction you wrote yourself on top of ComponentSpace, or are you using a different SAML library / a community sample / fork? If you can share the GitHub repo, the NuGet package name, or the documentation URL of the library/sample you're using, I can read its source and give you a wiring suggestion that lines up with the actual API you have in hand. Without that I'd just be guessing at method signatures that may not exist in your build.

  2. The AccountController you pasted authenticates against an in-memory Users dictionary with CookieAuthenticationDefaults.AuthenticationScheme. Is that controller part of a standalone PoC project (separate from the ABP solution), or is it living inside your ABP 10.3 AuthServer? In other words: is the goal to migrate this PoC's SAML middleware into the ABP AuthServer, replacing the cookie scheme with ABP's IdentityConstants.ApplicationScheme and the in-memory user store with ABP IIdentityUserManager?

  3. About the SP management UI you replicated from OpenIddict Application Module — is it already in the ABP AuthServer host (alongside the OpenIddict Application admin pages), or also in a separate project at the moment?

Once I know the library source and where this code currently lives vs. where it needs to land, I can give a concrete wiring path (which scheme to authenticate against, where to register the middleware in the AuthServer module, how the user resolver should fetch from ABP's user store).

Thanks

Hi Raff,

Yes to all three — ABP Studio's AI Agent already has memory, custom skills/rules, and custom workflows (playbooks).

Memory (Learned Lessons). The agent can call a save_lesson action to record a verified correction into a persistent ai-learned-lessons file in your solution. Those lessons are automatically injected into every future agent conversation in the same solution, so it stops repeating the same mistake. You can also clear them from the UI.

Custom AI Rules / Skills. You can author your own rules as .mdc files (Markdown body + a small YAML header with name / description / alwaysApply). Two scopes:

  • Solution-level: .abpstudio/ai-rules/ — checked into source control, shared with your team.
  • Global (user-level): ~/.abp/studio/rules/ — applies across all your solutions.

alwaysApply: true keeps the rule in the agent's system prompt at all times; alwaysApply: false exposes only the name/description and lets the agent pull the full content on demand via fetch_ai_skills (cheaper on tokens). External .md skill files can also be imported.

Custom Workflows (playbooks/commands). You can define tasks to run automatically before and/or after each agent run — Build, Start/Stop/Restart application, Start/Stop containers, Add migration, Install libs, Generate C# / Angular proxies, or your own Run Task. Workflows can be personal (stored under .abpstudio/workflows/) or shared via the run profile (.abprun.json). This is the most effective way to make the agent actually verify its changes (e.g. force a Build after each edit) instead of looping on hallucinated code.

Thanks

Hi Raff,

Sorry about this experience, and thanks for the detailed screenshots.

Cases like yours are exactly what we need to see. Our team will review the issues the AI Agent ran into and work on improving it in the upcoming versions.

Thanks for your patience.

Showing 461 to 470 of 12915 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.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.