Activities of "Pooja_Ojha"

I can understand that it's not available publicly. You can send me the demo code link to my email address in the file.

For Low-Code, I will wait for an agent to reply; maybe it is in preview, that's why you cannot find it in the documentation

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

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

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);
        }
    }
  1. This is a standalone as simple ASP.NET Core 10, not ABP, as I am slightly confused about how to implement.
  2. I have created a separate DDD Module to implement the SAML Application and have created UI via ABP suite

Thanks, Pooja

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

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 SAMLResponse

I 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

Thanks for your reply. I will be going by the design time approach, as there are fewer chances of error

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.

Answer

AI suggestion to remove Saas is perfect and suitable for our project

Showing 1 to 9 of 9 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 September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.