Hi,
I also need to implement SAML in my client application. Based on my understanding, if SAML can work with normal ASP.NET Core 10, it can work with ABP as well, so I am investigating how to implement the SAML library from component space to authenticate users.
What I need is how and where I can wire up once the user is authenticated in the ABP application, which application service should I replace to customise the authentication flow, as the application will have:-
- SAML - Salesforce Integration works best
- ABP Openiddict - best for vendors to sign in
- Social Login - best for customers to sign in
In the Teams License, I will not have Commercial source code access, however I have noticed that some ABP open-source DLLs (Ex. : Volo.Abp.Identity.Domain.dll) are also being used in ABP commercial, if I customise Volo.Abp.Identity for example, how can I reference it in my Commercial ABP application so that it does not get in conflict, like a duplicate DLL in the application
13 Answer(s)
-
0
ABP doesn’t require you to “replace an application service” to integrate SAML. Treat SAML as another ASP.NET Core authentication handler (like any social/OIDC provider), then let ABP’s Account login page flow (external login) create/link the ABP user and sign them in.
The wiring point is in your host web module where authentication is configured (e.g.,
*.Webfor MVC/Razor UI or*.HttpApi.Host/ separate AuthServer host in tiered setups): you register the SAML scheme viaAddAuthentication().AddXxx(...)and ensure its SignInScheme isIdentityConstants.ExternalSchemeso ABP’s external-login pipeline can read it.Once that’s done, ABP will handle:
- External challenge/redirect
- Reading external identity on callback
- Creating/linking the ABP user (if enabled by your account module configuration)
- Issuing the application cookie / tokens (OpenIddict in your case)
To support multiple login types:
- SAML (Salesforce): register a SAML authentication scheme (ComponentSpace) as an external provider (must use
IdentityConstants.ExternalScheme). - ABP OpenIddict: keep it as your main auth server/token issuer (vendors can still use local username/password or any other configured provider).
- Social logins: register them the same way (external schemes) alongside SAML. ABP’s login UI will list them.
- SAML (Salesforce): register a SAML authentication scheme (ComponentSpace) as an external provider (must use
About “customizing Volo.Abp.Identity” with Team license / no commercial source:
- Do not fork/modify
Volo.Abp.Identityand reference it alongside the NuGet package; that will create assembly conflicts (duplicate types/assemblies). - The supported approach is to customize behavior via extension points in your own module/application (e.g., adding an external auth scheme, overriding options, adding claims, etc.), not by shipping a modified
Volo.Abp.*assembly. - If you need license/commercial source-code related guidance or entitlements, contact ABP via: https://abp.io/contact
- Do not fork/modify
(Links validated: https://abp.io/community/articles/4603b9cf , https://abp.io/docs/latest/solution-templates/layered-web-application/authentication , https://abp.io/contact)
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.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
not 100% agree with the AI answer, as in the Tiered Structure Auth model is separate, and SAML SSO requires configuration of attributes before sending back the ABP web application.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
You don't need to replace any application service to integrate SAML — that's not where ABP plugs external auth in. SAML is a standard ASP.NET Core remote authentication handler, and ABP's Account module (Razor Pages, runs on AuthServer in Tiered or on the Web/HttpApi host otherwise) already handles external sign-in, user creation, and link-by-email out of the box. Your job is to register the SAML scheme; ABP picks it up automatically.
Where to register the SAML scheme
- Tiered (separate AuthServer) → register inside the AuthServer module (
MyProjectName.AuthServer/MyProjectNameAuthServerModule.cs), next to where Google/Microsoft/Twitter providers are configured. The Account UI (login page + external login callback) and OpenIddict both live on AuthServer, so SAML must terminate there as well. - Non-tiered → register inside whichever host runs the Account module (
*.Webfor Blazor Server/WebApp/MVC, or*.HttpApi.Hostfor Angular without a separate AuthServer).
Minimal wiring with ComponentSpace.Saml2 (verified against package version 6.1.0):
public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); // 1) Load ComponentSpace SAML configuration (partners, certs, mapping rules). context.Services.AddSaml(configuration.GetSection("SAML")); // 2) Register SAML as an external auth scheme alongside Google / Microsoft / etc. context.Services.AddAuthentication() .AddSaml("saml", "SAML (Salesforce)", options => { options.SignInScheme = IdentityConstants.ExternalScheme; options.SignOutScheme = IdentityConstants.ApplicationScheme; options.AssertionConsumerServicePath = "/SAML/AssertionConsumerService"; options.PartnerName = ctx => "https://salesforce.com"; // your IdP entity ID }); }Three details worth being explicit about:
SignInScheme = IdentityConstants.ExternalScheme— this is the contract ABP'sLogin.cshtml.cs > OnGetExternalLoginCallbackAsyncreads from. Don't rely on defaults from a third-party handler.- ACS / callback URL must point to AuthServer (e.g.
https://auth.myapp.com/SAML/AssertionConsumerService). When you configure the SP entry inside Salesforce, set the assertion consumer URL to the AuthServer domain, not the business Web/Angular app. This is the Tiered detail you hinted at — AuthServer owns the entire external sign-in flow. - SAML attribute → ABP claim mapping — ComponentSpace doesn't use the ASP.NET Core
ClaimActionspipeline (its assertions are XML, not JSON). The cleanest way is to declareSamlMappingRuleentries in your SAML JSON configuration so each<saml:Attribute>becomes a standard ClaimType (e.g. map SAMLEmailAddress→http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress). After that, ABP'sOnGetExternalLoginCallbackAsyncreadsloginInfo.Principal.FindFirstValue(AbpClaimTypes.Email ?? ClaimTypes.Email)and the rest just works. You do not need to overrideIAbpClaimsPrincipalFactoryfor this — that hook fires for every login (local password, SAML, social) and is overkill for attribute mapping on a single provider. Getting SAML attributes into the OpenIddict access token
This is the part most SAML integrations get wrong, so worth calling out. After SAML callback, the user is signed into the
IdentityConstants.ApplicationSchemecookie on AuthServer. When the SPA / Blazor / Angular client then hits/connect/authorize, OpenIddict'sTokenControllerrebuilds the principal viaSignInManager.CreateUserPrincipalAsync(user)— so transient SAML claims do not automatically appear in the access token.If you need Salesforce attributes (role, department, employee ID, …) inside the token:
- Persist them onto the ABP user on first/each external sign-in (extra columns via
ObjectExtensionManager.ConfigureIdentity(...), or rows inAbpUserClaims) — they'll be issued naturally on every subsequent token. - Or implement
IAbpOpenIddictClaimsPrincipalHandlerto inject claims into the OpenIddict pipeline directly.
Login page button shows up automatically
Once the SAML scheme is registered, the Account login page enumerates it on its own — it filters auth schemes whose
HandlerTypeisRemoteAuthenticationHandler<>, which ComponentSpace satisfies. You only needAbpAccountOptions.ExternalProviderIconMapto set a custom icon. If you later want to enable/disable SAML from the database (without redeploy), plug it intoIAccountExternalProviderAppService, but that's optional.Coexistence of SAML + OpenIddict + Social
Fully supported. OpenIddict is your local token issuer; SAML and the social providers are just external schemes living alongside each other. One behavior to be aware of for UAT: ABP auto-links an external sign-in to an existing local user by email (
UserManager.AddLoginAsync). So if a vendor first registers with username/password and later signs in via Salesforce SAML using the same email, the SAML login attaches to the existing account. Depending on theVerifyPasswordDuringExternalLoginsetting on the Account admin page, the user may be asked to confirm their local password the first time. Worth flagging to your team before go-live.
On forking
Volo.Abp.IdentityI'd advise against it. The Pro Identity / Account modules (
Volo.Abp.Identity.Pro.*,Volo.Abp.Account.Pro.*) are built on top of the open-sourceVolo.Abp.Identitypackage and reference the matching version that ships in the same ABP release. If you fork the open-source code, repackage it, and try to reference it side-by-side with Pro NuGet packages, you're likely to hit assembly version mismatches, duplicate type registrations, and EF Core model conflicts at runtime. If you absolutely need to try, do it in a throwaway project first — but I wouldn't ship it.With Team license, you have all the extension points you need without forking:
- Replace a service: derive from any Pro or open-source service and register with
[Dependency(ReplaceServices = true, TryRegister = true)]. Works forSignInManager,UserManager,UserStore,*AppService, etc. - Extend entities:
ObjectExtensionManager.Instance.Modules().ConfigureIdentity(...)adds columns toAbpUserswithout touching the entity class. - Inject claims: implement
IAbpClaimsPrincipalContributorfor global claim injection, orIAbpOpenIddictClaimsPrincipalHandlerspecifically for OpenIddict tokens. - React to user lifecycle: subscribe to the framework's user created / updated events via the distributed event bus (good place to push Salesforce attributes onto
AbpUserClaimson first sign-in).
These cover the "customize authentication flow" surface area for Team license and stay upgrade-safe across ABP versions.
Two reference files worth opening when you start wiring:
Volo.Abp.Account.Pro.Public.Web/Pages/Account/Login.cshtml.cs(OnGetExternalLoginCallbackAsync) — the canonical external login flow that you do not need to override.- The Pro AuthServer template's
MyProjectNameAuthServerModule.cs— already has Google / Microsoft / Twitter registered exactly the way you'll add SAML.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Tiered (separate AuthServer) → register inside the AuthServer module (
-
0
Hi Maliming, sorry, the approach you have suggested is to implement SAML as SP, and managing users in Salesforce will be costly for the client. I have mapped out most of IDP side with ComponentSpace libraries for SSO and SLO.
What I need is to understand how I can wire post successful login from Account/login in ABP project as below
SP → /saml/sso ↓ Store SAML request context ↓ Redirect to ABP Login Page ↓ ABP Identity Login (/Pages/Account/Login.cshtml.cs) ↓ User authenticated ↓ Protocol Router ↙ ↘ OIDC SAML → Assign SAML Attributes → Issue SAMLResponseI have already created the SAML application class, same as the OpenIddict Application:
public class SamlApplication: FullAuditedAggregateRoot<Guid> { public SamlApplication() { } public SamlApplication(Guid id) : base(id) { } public string ApplicationName { get; set; } public string RequestId { get; set; } public string RelayState { get; set; } public string AssertionConsumerServiceUrl { get; set; } public string EntityId { get; set; } public string NameIdFormat { get; set; } }So, on the SAML side, I am fully covered; I just need help to wire post-login
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Thanks for the clarification — my previous answer was aimed at the SP direction, so it doesn't apply to your scenario. Before I give a concrete wiring suggestion for the IdP side, a few quick questions so I don't guess wrong on the ABP integration point:
Which ComponentSpace integration mode are you using?
AddSamlMiddleware(...)+app.UseSaml()— the middleware handles the SP-initiated SSO entry, the login redirect, and the SAMLResponse generation automatically, and you only implement theEvents.OnInitiateSsocallback.- Or are you injecting
ISamlIdentityProviderdirectly and callingSendSsoAsync/ReceiveSsoAsyncfrom your own controller / Razor page?
The two modes are mutually exclusive in ComponentSpace, and the "post-login wire-up" looks completely different in each — that's why I need to know which one you've gone with.
About your
SamlApplicationentity — what's the goal?- Multi-tenant dynamic SP registration (each tenant registers its own SPs at runtime)?
- A management UI for admins to add SP partners without editing
appsettings.json? - Something else?
ComponentSpace's default partner configuration lives in JSON (via
AddSaml(Configuration.GetSection("SAML"))). They also shipComponentSpace.Saml2.Configuration.Databasefor dynamic DB-backed partner configuration, which may already cover what you're building.Can you share the current shape of your SAML controller / endpoint (the one that receives the SP's
SAMLRequest) and the place you're trying to "wire post-login" from? Even pseudo-code is fine — I just need to see whether you're inside an MVC controller action, a Razor Page handler, or middleware so I can point at the right ABP extension point.Project setup (the Question Details section was left blank):
- ABP version
- UI type (MVC / Blazor / Blazor WebApp / Angular)
- Tiered, i.e. separate
AuthServerhost? Or single-host setup?
This matters because in Tiered the SAML IdP endpoints have to live on the AuthServer (where
/Account/Loginruns), not on the business Web/HttpApi host.
Once I have these four pieces, I can give you a single concrete wiring path instead of two parallel "if this then that" answers.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Thanks for your reply. Below is the answer to your question:-
Question 1: I will be using middleware with the app.UseSaml()
// Authentication builder.Services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => { o.Cookie.Name = "IdP.Session"; o.LoginPath = "/Account/Login"; o.LogoutPath = "/Account/Logout"; o.ExpireTimeSpan = TimeSpan.FromHours(8); }); builder.Services.AddScoped<ISamlUserResolver, SpResolvingSamlUserResolver>(); // Add the SAML middleware services. builder.Services.AddSamlMiddleware(idp => { idp.EntityId = "http://localhost:5100"; idp.BaseUrl = "http://localhost:5100"; idp.SigningCertificate = idpCert; idp.LoginUrl = "/Account/Login"; idp.AssertionLifetime = TimeSpan.FromMinutes(5); idp.SessionLifetime = TimeSpan.FromHours(8); idp.OnSignOut = ctx => ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); });Controller:-
public class AccountController : Controller { private static readonly Dictionary<string, (string Password, string GivenName, string Surname, string Role)> Users = new(StringComparer.OrdinalIgnoreCase) { ["admin"] = ("password", "Admin", "User", "Admin") }; [HttpGet] public IActionResult Login(string? returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(string? username, string? password, string? returnUrl) { if (string.IsNullOrWhiteSpace(username) || !Users.TryGetValue(username, out var info) || info.Password != password) { ModelState.AddModelError("", "Invalid username or password."); ViewBag.ReturnUrl = returnUrl; return View(); } var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Name, username)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, username)); identity.AddClaim(new Claim(ClaimTypes.Email, $"{username}@example.local")); identity.AddClaim(new Claim(ClaimTypes.GivenName, info.GivenName)); identity.AddClaim(new Claim(ClaimTypes.Surname, info.Surname)); identity.AddClaim(new Claim(ClaimTypes.Role, info.Role)); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent = true }); return Redirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl); } public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } }SpResolvingSamlUserResolver.cs
public sealed class SpResolvingSamlUserResolver(ILogger<SpResolvingSamlUserResolver> logger) : ISamlUserResolver { // In production, replace this with a real user store (EF Core, LDAP, etc.) private static readonly Dictionary<string, UserClaims> Users = new(StringComparer.OrdinalIgnoreCase) { ["admin"] = new("Admin", "User", "admin@example.local", ["Admin"]), ["alice"] = new("Alice", "Smith", "alice@example.local", ["User"]), ["bob"] = new("Bob", "Jones", "bob@example.local", ["User"]), }; public Task<bool> IsAuthenticatedAsync(HttpContext context) => Task.FromResult(context.User.Identity?.IsAuthenticated == true); public async Task<SamlUserInfo?> ResolveUserAsync(HttpContext context, SamlServiceProvider sp) { if (context.User.Identity?.IsAuthenticated != true) return null; var username = context.User.Identity.Name ?? "unknown"; if (!Users.TryGetValue(username, out var userClaims)) { logger.LogWarning("User not found in store: {Username}", username); return null; } var baseClaims = ToClaimList(userClaims); // Filter claims based on SP's configuration var allowedClaims = FilterClaims(baseClaims, sp); logger.LogInformation("Resolved {ClaimCount} claims for SP {EntityId}", allowedClaims.Count, sp.EntityId); return new SamlUserInfo { NameId = userClaims.Email, NameIdFormat = SamlConstants.NameIdFormats.EmailAddress, Attributes = allowedClaims, SessionIndex = "_" + Guid.NewGuid().ToString("N") }; } /// <summary> /// Filter claims based on SP's AllowedClaimTypes or AttributeMappings. /// If SP has AttributeMappings, only include mapped claim types. /// If SP has AllowedClaimTypes (but no AttributeMappings), use those. /// Otherwise, return all available claims. /// </summary> private static List<Claim> FilterClaims(List<Claim> availableClaims, SamlServiceProvider sp) { // Priority 1: AttributeMappings (most specific) if (sp.AttributeMappings.Count > 0) { var mappedTypes = sp.AttributeMappings .Select(m => m.ClaimType) .ToHashSet(StringComparer.OrdinalIgnoreCase); return availableClaims.Where(c => mappedTypes.Contains(c.Type)).ToList(); } // Priority 2: AllowedClaimTypes if (sp.AllowedClaimTypes.Count > 0) { return availableClaims.Where(c => sp.AllowedClaimTypes.Contains(c.Type)).ToList(); } // Priority 3: Allow all claims return [.. availableClaims]; } private static List<Claim> ToClaimList(UserClaims uc) { var claims = new List<Claim> { new(ClaimTypes.Name, uc.GivenName), new(ClaimTypes.NameIdentifier, uc.Email), new(ClaimTypes.Email, uc.Email), new(ClaimTypes.GivenName, uc.GivenName), new(ClaimTypes.Surname, uc.Surname), }; foreach (var role in uc.Roles) { claims.Add(new(ClaimTypes.Role, role)); } return claims; } }Question 2: No multi-tenancy, only for Host. I have replicated the OpenIddict Application Module to add SP information through the Web UI.
Question 3: Please see the above controller. Also note that I am using SpResolvingSamlUserResolver to resolve the user.
Question 4: Using ABP 10.3 with MVC and Tiered, separate the AuthServer host
Thanks, Pooja
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
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:
The
AddSamlMiddleware(idp => { idp.EntityId = ...; idp.BaseUrl = ...; idp.SigningCertificate = ...; idp.AssertionLifetime = ...; idp.SessionLifetime = ...; idp.OnSignOut = ...; })shape doesn't match ComponentSpace'sSamlMiddlewareOptions(which only exposesLoginUrl,PartnerName,Events,InitiateSingleSignOnPath,SingleSignOnServiceCompletionPath,PartnerNameParameter). Same with theISamlUserResolver/SamlUserInfo/SpResolvingSamlUserResolvertypes and theSamlServiceProvider.AllowedClaimTypes/AttributeMappingsproperties — 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.
The
AccountControlleryou pasted authenticates against an in-memoryUsersdictionary withCookieAuthenticationDefaults.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'sIdentityConstants.ApplicationSchemeand the in-memory user store with ABPIIdentityUserManager?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
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Regarding Question 1:- I am using almost 3-year-old libraries of Component Space, which were heavily customised to work with DevExpress. Now I know why there is confusion. I have downloaded new libraries, which are much easier to integrate. Can you please suggest based on the ComponentSpace ExampleIdentityProvider? Below is what I will use to implement again in ABP:-
Program
// Add SAML SSO services. builder.Services.AddSaml(builder.Configuration.GetSection("SAML"));Login
public class LoginModel : PageModel { private readonly SignInManager<IdentityUser> _signInManager; private readonly ILogger<LoginModel> _logger; public LoginModel(SignInManager<IdentityUser> signInManager, ILogger<LoginModel> logger) { _signInManager = signInManager; _logger = logger; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [BindProperty] public InputModel Input { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public IList<AuthenticationScheme> ExternalLogins { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public string ReturnUrl { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [TempData] public string ErrorMessage { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public class InputModel { /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [Required] [EmailAddress] public string Email { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [Required] [DataType(DataType.Password)] public string Password { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public async Task OnGetAsync(string returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { ModelState.AddModelError(string.Empty, ErrorMessage); } returnUrl ??= Url.Content("~/"); // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ReturnUrl = returnUrl; } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { _logger.LogInformation("User logged in."); return LocalRedirect(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning("User account locked out."); return RedirectToPage("./Lockout"); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); } } // If we got this far, something failed, redisplay form return Page(); } }SamlController.cs
[Route("[controller]/[action]")] public class SamlController : Controller { private readonly SignInManager<IdentityUser> _signInManager; private readonly ISamlIdentityProvider _samlIdentityProvider; private readonly IConfigurationToMetadata _configurationToMetadata; private readonly IConfiguration _configuration; public SamlController( SignInManager<IdentityUser> signInManager, ISamlIdentityProvider samlIdentityProvider, IConfigurationToMetadata configurationToMetadata, IConfiguration configuration) { _signInManager = signInManager; _samlIdentityProvider = samlIdentityProvider; _configurationToMetadata = configurationToMetadata; _configuration = configuration; } [Authorize] public async Task<IActionResult> InitiateSingleSignOn() { // Get the name of the logged in user. var userName = User?.Identity?.Name; // For demonstration purposes, include some claims. 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"]; // Initiate single sign-on to the service provider (IdP-initiated SSO) // by sending a SAML response containing a SAML assertion to the SP. // The optional relay state normally specifies the target URL once SSO completes. await _samlIdentityProvider.InitiateSsoAsync(partnerName, userName, attributes, relayState); return new EmptyResult(); } public async Task<IActionResult> InitiateSingleLogout(string? returnUrl = null) { // Request logout at the service provider(s). await _samlIdentityProvider.InitiateSloAsync(relayState: returnUrl); return new EmptyResult(); } public async Task<IActionResult> SingleSignOnService() { // Receive the authn request from the service provider (SP-initiated SSO). var idpSsoResult = await _samlIdentityProvider.ReceiveSsoAsync(); // If the user is logged in at the identity provider, complete SSO immediately. // Otherwise have the user login before completing SSO. if (User.Identity is not null && User.Identity.IsAuthenticated) { await CompleteSsoAsync(idpSsoResult.CorrelationID); return new EmptyResult(); } else { return RedirectToAction("SingleSignOnServiceCompletion", new { idpSsoResult.CorrelationID }); } } [Authorize] public async Task<IActionResult> SingleSignOnServiceCompletion(string correlationID) { await CompleteSsoAsync(correlationID); return new EmptyResult(); } public async Task<IActionResult> SingleLogoutService() { // Receive the single logout request or response. // If a request is received then single logout is being initiated by a partner service provider. // If a response is received then this is in response to single logout having been initiated by the identity provider. var sloResult = await _samlIdentityProvider.ReceiveSloAsync(); if (sloResult.IsResponse) { if (sloResult.HasCompleted) { // IdP-initiated SLO has completed. if (!string.IsNullOrEmpty(sloResult.RelayState)) { return LocalRedirect(sloResult.RelayState); } return RedirectToPage("/Index"); } } else { // Logout locally. await _signInManager.SignOutAsync(); // Respond to the SP-initiated SLO request indicating successful logout. await _samlIdentityProvider.SendSloAsync(correlationID: sloResult.CorrelationID); } return new EmptyResult(); } public async Task<IActionResult> ArtifactResolutionService() { // Resolve the HTTP artifact. // This is only required if supporting the HTTP-Artifact binding. 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.Append("Content-Disposition", "attachment; filename=\"metadata.xml\""); var xmlWriterSettings = new XmlWriterSettings() { Async = true, Encoding = Encoding.UTF8, Indent = true, OmitXmlDeclaration = true }; using (var xmlWriter = XmlWriter.Create(Response.Body, xmlWriterSettings)) { xmlElement.WriteTo(xmlWriter); await xmlWriter.FlushAsync(); } return new EmptyResult(); } private async Task CompleteSsoAsync(string correlationID) { // Get the name of the logged in user. var userName = User?.Identity?.Name; // For demonstration purposes, include some claims. 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), }; // The user is logged in at the identity provider. // Respond to the authn request by sending a SAML response containing a SAML assertion to the SP. await _samlIdentityProvider.SendSsoAsync(userName, attributes, correlationID: correlationID); } }- This is a standalone as simple ASP.NET Core 10, not ABP, as I am slightly confused about how to implement.
- I have created a separate DDD Module to implement the SAML Application and have created UI via ABP suite
Thanks, Pooja
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Reproduced the wiring locally on a fresh ABP 10.3 Tiered MVC AuthServer with
ComponentSpace.Saml26.1.0. The good news is the surface area is small — theSamlControlleryou 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.csAddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(...)and skipLoginModel.cs/AccountController.cs. ABP AuthServer already serves/Account/Loginand usesIdentityConstants.ApplicationSchemeas 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 inSamlMiddlewareApplicationBuilderExtensionsand is only required for ComponentSpace's middleware-based IdP pattern (AddSamlMiddleware+Events.OnInitiateSso). Your code uses the controller pattern (injectingISamlIdentityProviderand callingReceiveSsoAsync/SendSsoAsyncdirectly), soAddSaml(...)alone is enough. I verified this on the local repro — removingUseSaml()still builds and the controller routes resolve.3. The only edit to
SamlControlleris theSignInManagergeneric type. The sample injectsSignInManager<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.IdentityUseris a different type fromMicrosoft.AspNetCore.Identity.IdentityUser; ABP registersAbpSignInManager : 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'sAbpUserClaimsPrincipalFactorypopulates the standardClaimTypes.*values onto the cookie principal by default.4. About
_signInManager.SignOutAsync()inSingleLogoutService. This is the minimum needed to clear the application cookie. ABP's own/Account/Logout(Volo.Abp.Account.Public.Web.Pages.Account.LogoutModel.OnGetAsyncinVolo.Abp.Account.Pro.Public.Web) does more: it writes anIdentitySecurityLogaudit entry and clears theConfirmUser,ChangePassword, andLockedOutauxiliary schemes. If your SLO needs parity with/Account/Logout, mirror those steps inSingleLogoutServicebefore calling_samlIdentityProvider.SendSloAsync.5. SAML partner configuration — get the IdP vs SP roles right. Put the
SAMLsection in*.AuthServer/appsettings.jsonusing 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 theSAMLResponse. - In each partner SP's SAML config (the third-party app), the IdP
SingleSignOnServiceUrlmust point at your AuthServer's controller route — by defaulthttps://auth.example.com/Saml/SingleSignOnServicegiven your[Route("[controller]/[action]")]. The metadata XML returned by yourExportMetadataaction gives them the full set.
For PoC, the signing certificate can be a
.pfxfile referenced from the AuthServer project. For production, point ComponentSpace at a cert inX509Store/ 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'sISamlConfigurationResolver(or useComponentSpace.Saml2.Configuration.Database) to translate yourSamlApplicationrows intoPartnerServiceProviderConfigurationobjects. 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 ABPSignInManager<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
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - In your AuthServer's SAML config, each partner SP entry holds the SP-side
-
0
Hi,
Thanks, if you have the sample working, please share, as the SSO & SLO controller does not integrate with AuthServer; somehow, it needs to pass from the host to AuthServer.
I will try to figure it out. Pooja
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
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*.Webor*.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/ExportMetadatareturns the IdP XML signed with the local cert, and[Authorize]on the SAML actions redirects to/Account/Login?ReturnUrl=...preserving the fullSAMLRequestquery 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.csOne line inside
ConfigureServices. Noapp.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.csAlmost identical to the ComponentSpace
ExampleIdentityProvidercontroller. The only change is theSignInManagergeneric — must be ABP's user type, with full namespace to avoid confusion withMicrosoft.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()inSingleLogoutService: that only clears the application cookie. ABP's own/Account/Logout(seeVolo.Abp.Account.Public.Web.Pages.Account.LogoutModel.OnGetAsync) also writes anIdentitySecurityLogaudit entry and clears theConfirmUser,ChangePassword, andLockedOutauxiliary schemes. If your SLO needs parity with/Account/Logout, mirror those steps beforeSendSloAsync.4.
src/MyCompanyName.MyProjectName.AuthServer/appsettings.jsonThe exact field names that ComponentSpace's JSON binder expects (verified against
ComponentSpace.Saml2.Configuration.SamlConfigurations,SamlConfiguration,LocalIdentityProviderConfiguration,PartnerServiceProviderConfigurationin 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
NameinLocalIdentityProviderConfigurationis your IdP entity ID. Each entry inPartnerServiceProviderConfigurationsis one SP partner; theNamethere must match the SP's entity ID. For dev/PoC you can sign with theopeniddict.pfxyou already have; for production use a real.pfxfrom your PKI or aX509Storereference.The partner SP needs your IdP metadata to know where to send
SAMLRequest. Hithttps://your-authserver-host/Saml/ExportMetadatato download it — that's the file you give them.Once your SamlApplication entity goes live
Drop the static
PartnerServiceProviderConfigurationsarray and implementComponentSpace.Saml2.Configuration.Resolver.ISamlConfigurationResolverto translate yourSamlApplicationrows intoSamlConfiguration/PartnerServiceProviderConfigurationobjects, then register it viaservices.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:
- Spin up a fresh ABP 10.3 Tiered MVC project via ABP Studio or the
abp newCLI (or take a stripped-down copy of yours). - 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).
- 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
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Spin up a fresh ABP 10.3 Tiered MVC project via ABP Studio or the
-
0
I got it, you have the controller configured in AuthServer, that was the missing piece. Let me try on my side and see if it works.
Thanks, Pooja
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)