Part2 : Enabling SAML Single Sign-On for Umbraco Backoffice with PingID Integration
Configuring Umbraco for PingID SAML Authentication
This implementation provides a foundation for PingID SAML authentication in Umbraco. For production environments, consider these enhancements based on your business requirements:
1. Configuration Setup
Let’s keep the SAML settings in the appsettings.json file.
appsettings.json
Where to Find These Values
- Certificate
- Copy the entire content from your
.crt
file - Preserve the
-----BEGIN/END CERTIFICATE-----
headers - Keep all line breaks (use
\n
for JSON formatting)
- SSO Login URL
- Found in PingID Admin Console under:
Connections → Applications → Your App → SAML Settings
- Typically follows pattern:
https://auth.pingone.com/[TENANT_ID]/saml20/idp/sso
- ACS URL
- Must exactly match the value configured in PingID
- Use HTTPS in production (except for local development)
- Example format:
https://[YOUR_DOMAIN]/auth
- Example format:
https://[YOUR_DOMAIN]/auth
{
"SAML": {
"EntityId": "umb-sso",
"Certificate": "-----BEGIN CERTIFICATE-----\nYOUR_CERT\n-----END CERTIFICATE-----",
"SsoLoginUrl": "https://auth.pingone.com/YOUR_ENDPOINT/saml20/idp/sso",
"AcsUrl": "https://yourdomain.com/auth"
}
}
The certificate value should be copied from the X509 PEM (.crt) file, which you have downloaded.
The SsoLoginUrl should be the Single Signon Service you have copied in the previous step.
2. Configuration Class
This class represents a strongly-typed configuration model that maps to the SAML settings in the appsettings.json
public class SamlSettings
{
public const string SectionName = "SAML";
[Required(ErrorMessage = "EntityId is required")]
public string EntityId { get; set; }
[Required(ErrorMessage = "Certificate is required")]
public string Certificate { get; set; }
[Required, Url(ErrorMessage = "Invalid SSO URL format")]
public string SsoLoginUrl { get; set; }
[Required, Url(ErrorMessage = "Invalid ACS URL format")]
public string AcsUrl { get; set; }
}
3. Service Registration
This registration allows you to inject IOptions<SamlSettings>
into other services or controllers.
// Configuration
builder.Services.Configure<SamlSettings>(
builder.Configuration.GetSection(SamlSettings.SectionName));
4. Add LoginController and Login Form
This controller initiates the SAML authentication flow by redirecting users to PingID’s login page. It:
- Generates a SAML authentication request
- Redirects users to PingID’s SSO endpoint
- Uses strongly-typed configuration from
appsettings.json
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Website.Controllers;
using Umbraco.Cms.Core;
using Saml;
using Microsoft.Extensions.Logging;
namespace SampleUmbracoProject.Core.Controllers
{
public class LoginController : SurfaceController
{
private readonly IOptions<SamlSettings> _samlSettings;
private readonly IPublishedContentQuery _publishedContentQuery;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ILogger<LoginController> _logger;
public LoginController(
IOptions<SamlSettings> samlSettings,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentQuery publishedContentQuery,
IVariationContextAccessor variationContextAccessor,
ILogger<LoginController> logger)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_publishedContentQuery = publishedContentQuery;
_variationContextAccessor = variationContextAccessor;
_logger = logger;
}
public IActionResult Login()
{
var settings = _samlSettings.Value;
var request = new AuthRequest(
settings.EntityId,
settings.AcsUrl
);
return Redirect(request.GetRedirectUrl(settings.SsoLoginUrl));
}
}
}
LoginForm.cshtml
You can create a new page and add the form below.
<div class="saml-login">
<form asp-controller="Login" asp-action="Login" method="post">
<button type="submit" class="btn btn-saml">
<img src="/assets/pingid-logo.svg" alt="PingID"/>
Sign in with PingID
</button>
</form>
</div>
5. Saml Response Handler
This controller handles the SAML response from PingID after a user attempts to log in. It:
- Validates the SAML response
- Extracts user email from the SAML payload
- Signs the user into Umbraco’s backoffice
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Saml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.BackOffice.Security;
using Umbraco.Cms.Web.Common.Controllers;
namespace SampleUmbracoProject.Core.Controllers
{
[Route("auth")]
public class AuthController : UmbracoController
{
private readonly IOptions<SamlSettings> _samlSettings;
private readonly IBackOfficeSignInManager _signInManager;
private readonly IBackOfficeUserManager _userManager;
private readonly ILogger<AuthController> _logger;
public AuthController(
IOptions<SamlSettings> samlSettings,
IBackOfficeSignInManager signInManager,
IBackOfficeUserManager userManager,
ILogger<AuthController> logger)
{
_samlSettings=samlSettings;
_signInManager = signInManager;
_userManager = userManager;
_logger = logger;
}
//ASP.NET Core MVC action method... But you can easily modify the code for old .NET Framework, Web-forms etc.
public async Task<IActionResult> Index()
{
try
{
var settings = _samlSettings.Value;
var samlResponse = new Response(settings.Certificate, Request.Form["SAMLResponse"]);
if (samlResponse.IsValid())
{
var email = samlResponse.GetEmail();
var user = await _userManager.FindByEmailAsync(email);
if(user!=null)
{
await _signInManager.SignInAsync(user, true);
return Redirect("~/umbraco");
}
}
}
catch (Exception ex)
{
_logger.LogError($"Error, {ex.Message}");
throw;
}
return Content("Unauthorized");
}
}
}
That’s all for now! 😊 Give it a try and let me know your thoughts — I’d love to hear your feedback! 😄