Using SAML to deliver Single Sign On in Umbraco is an effective way of delivering content to closed groups of corporate users, such as an intranet or extranet, without introducing another unnecessary username and password by using Umbraco Membership.
Single Sign On - where one set of credentials is shared across multiple applications - makes systems safer and easier to manage, and is often mandated in larger enterprise level IT departments. Delivering that by implementing SAML in Umbraco is both the focus of this article and a deeper dive into a concept I first skimmed over during a talk I gave at this year's UWestFest.
Carbon Six Digital has developed a number of Umbraco-based sites that implement Single Sign On using SAML, including a training portal for a large pharmaceutical firm, an intranet for a regional car dealership network and our own Intranet.
In each case, our goal has been to provide a seamless integration for a core directory of users into Umbraco, while aiming to maintain the existing Umbraco functionality such as password protected pages using public access and the Membership Provider.
Our experience on each project has been that the coding is relatively straightforward, but that configuring the systems to talk is a huge challenge.
What is SAML?
The Security Assertion Markup Language (SAML) is an XML-based protocol enabling loosely coupled systems to share security data. It is analogous to the OAuth protocol used by large SaaS platforms such as Google,and Facebook in that it enables users to share a single login credential across many systems.
However, while OAuth is ubiquitous on B2C and SME-focussed websites and apps, SAML is more commonplace in large and complex corporate environments where the cost and complexity of setting up the infrastructure can be balanced against the significant benefits of increased security that Single Sign On brings. These include reducing password fatigue and the associated costs of password resets in large corporate IT departments.
In the language of SAML, Service Providers delegate authentication and authorisation activities to trusted Identity Providers.
In each case, our goal has been to provide a seamless integration for a core directory of users into Umbraco, while aiming to maintain the existing Umbraco functionality such as password protected pages using public access and the Membership Provider.
A very typical Use case is the Service Provider Initiated sign in, where:
- A user makes a request to the Service Provider (SP); and is
- Directed to the Identity Provider (IdP) to prove their identity; and then
- Redirected back to the Service Provider with a signed assertion which proves their identity and can carry additional information such as their group memberships and permissions.
Exactly how the User proves their identity to be authenticated by the IdP is a matter for specific implementations of each IdP, it's not controlled by the SAML protocol which simply dictates the method of communication between client, IdP and SP. The most common method for authentication is basic username and password, but other systems include the ability to support two-factor authentication, and biometrics.
The entire conversation between the User, the Service Provider and the Identity Provider is mediated by the User’s browser, without any direct communication between IdP and SP. Requests and assertions may be signed by the SP and IdP to maintain trust between the two providers which don't actually have any direct contact.
Potential SAML Identity Provider solutions
Most SAML projects involve integrating with a third party Identity Provider, which is either an on-premise solution such as Microsoft Active Directory Federated Services (ADFS) and Ping Identity, or a SaaS solution such as Lastpass and Okta.
All typically integrate with a User Directory, such as Active Directory, and expose user authentication and authorisation via SAML.
The features and benefits of each make comparison a lengthy task, but in broad terms on-premise solutions will have a large capital expenditure but lower total cost of ownership.
Our preference is to make use of the SaaS platforms as they are simpler to implement and are up and running instantly.
Goals
Our initial SAML project, for a global Pharma company, had a very limited goal to provide member authentication only. For our later projects, including our intranet, we have started with Authentication, but have a longer term goal of also implementing:
- Just-in-time Member Provisioning - i.e. creating new Members in Umbraco as they first login via a trusted IdP
- Authorisation - i.e. determining a Member's permissions based on the information in a SAML assertion
- Personalisation - i.e. presenting personalised content on the basis of facts in the assertion
Our examples are also based on the use of Okta - which was far simpler to debug than Lastpass.
Our Solution
The solution presented below has common features drawn from all our SAML-based solutions. The actual code examples are drawn from our own Intranet, which is built on Umbraco Cloud on 7.5.3. The site previously used the standard the Umbraco Membership Provider for Authentication and Authorisation, and login / out partials that were created by me using the built in Umbraco templates (I discuss setting up simple authentication and authorisation in an earlier 24Days article).
In all of our SAML projects we've made use of the off the shelf SAML API wrapper ComponentSpace. It's a fairly low cost wrapper that insulates the developer from the need to parse and validate assertions. Of course, other SAML libraries exist including open source ones from OneLogin.
Our examples are also based on the use of Okta - which was far simpler to debug than Lastpass.
One important thing to note here is that this code has been intentionally simplified, for the purposes of a simple demonstration. Before letting any of this code out into the wild you would be well advised to ruggedise the code to ensure it is safe and secure.
Setup and configuration
The configuration of Okta for the purposes of this demo was incredibly simple. Anyone can create a free Developer instance of Okta, which supports up to three SAML SPs (called applications by Okta).
Once the account is set up, you create a new Application and supply critical information that defines the conversation between Okta.
These include:
- AssertionConsumerServiceUrl -- this is the URL of the controller method that you’ll create to receive Single Sign on Logins.
- SP entity ID - which equates to the service provider name in ComponentSpaces SAML.config file.
For the purposes of this test we have a very simple UCloud base Umbraco site with a separate Development environment. We've used the UaaS.cmd script to create a Visual Studio solution, and then put all the code for SAML into a separate Project.
With ComponentSpace installed, and the DLL added into your SAML project, you’ll want to configure the saml.config file to link your site with Okta.
By far the simplest way to edit the saml.config, is to use ComponentSpace’s ImportMetadata example app. To use the program, first download the metadata generated by Okta, compile the example app from ComponentSpace and run it passing in the name of the metadata file supplied by Okta. This will then generate an example saml.config which can be used with a few small amendments.
<?xml version="1.0"?>
<SAMLConfiguration xmlns="urn:componentspace:SAML:2.0:configuration">
<ServiceProvider Name="samldemo.s1.umbraco.io"
AssertionConsumerServiceUrl="~/SSO/AssertionConsumerService"
/>
<PartnerIdentityProvider Name="###PROVIDED BY OKTA###"
SignAuthnRequest="false"
WantSAMLResponseSigned="true"
WantAssertionSigned="false"
WantAssertionEncrypted="false"
UseEmbeddedCertificate="true"
SingleSignOnServiceUrl="###PROVIDED BY OKA###"
CertificateFile="okta.cert"
/>
</SAMLConfiguration>
Single Sign On
In the Identity Provider Initiated Single Sign On scenario, the Member has visited the Identity Provider, and followed a link which has redirected them back to the Service Provider (our site). The redirection includes in it a SAML Assertion.
To enable this, at application start-up you create a custom route which can be called by the Identity Provider, and then load up the saml.config file into Component Space using SAMLConfiguration.Load().
namespace c6d.SAML
{
class Application : ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
//Create a custom login route which is called by Okta
RouteTable.Routes.MapRoute(
"CustomLogin",
"Sso/{action}/{id}",
new
{
controller = "Sso",
action = "AssertionConsumerService",
id = UrlParameter.Optional
});
// Load the saml.config configuration.
SAMLConfiguration.Load();
base.ApplicationStarted(umbracoApplication, applicationContext);
}
}
}
Next we create the controller and method to manage the assertion. We first pass the Request object into ComponentSpace using SAMLServiceProvider.ReceiveSSO() which returns the information extracted from the SAML Assertion. Next we use the Umbraco MemberService to find a matching member to that which was passed in via the SAML assertion. In the example below we do a very crude check to ensure we find a matching member -- in the wild you might want to ensure that the Partner IdP is trusted, and that the Member isn’t locked in Umbraco.
namespace c6d.SAML.Controllers
{
public class SsoController : Controller
{
public ActionResult AssertionConsumerService()
{
try
{
bool isInResponseTo = false;
string partnerIdP = null;
string userName = null;
string targetUrl = null;
// Receive and process the SAML assertion contained in the SAML response.
SAMLServiceProvider.ReceiveSSO(Request, out isInResponseTo, out partnerIdP, out userName, out attributes, out targetUrl);
//Get the member from their user name
var memberService = ApplicationContext.Current.Services.MemberService;
var checkMember = memberService.GetByUsername(userName);
if (checkMember == null)
{
TempData["ErrorMessage"] = string.Format("The user {0} does not exist in this application.", userName);
return Redirect("~/error");
}
FormsAuthentication.SetAuthCookie(userName, false);
return RedirectToLocal("~/");
}
catch (Exception ex)
{
TempData["ErrorMessage"] = "There was a problem authenticating the user.";
return Redirect("~/error");
}
}
}
}
The alternative sign on workflow, which I find more intuitive, is Service Provider initiated SSO. In this scenario the Member visits the Umbraco site and is challenged to login. When they click a button, they are redirected back to the IdP to identify themselves, and are then returned back to the SP.
To achieve this we create a samllogin.cshtml partial:
@using c6d.SAML.Controllers.Surface
@if (!User.Identity.IsAuthenticated && !User.IsInRole("Member"))
{
using (Html.BeginUmbracoForm("Login"))
{
<input id="login" type="submit" title="Login" name="Login" value="Log in" class="button custombutton" />
}
}
And in the surface controller, we create a single Login() method, which initialises
using System;
using System.Web.Configuration;
using System.Web.Mvc;
using System.Web.Security;
using ComponentSpace.SAML2;
using Umbraco.Core.Logging;
using Umbraco.Web.Mvc;
namespace c6d.SAML.Controllers.Surface
{
public class AuthenticationSurfaceController : SurfaceController
{
[HttpPost]
public ActionResult Login()
{
SAMLServiceProvider.InitiateSSO(Response, null, "http://www.okta.com/exkalqc5w5a155ZqX0h7");
return new EmptyResult();
}
}
}
Just-in-me member provisioning
In our earlier projects we've taken a very simple approach to member provisioning; for ad-hoc member creation we instruct Editors to use the Umbraco Members Section; or for large batch provisioning we recommend using CMSImport.
However, it is possible to implement automated Member Provisioning. Some Directory Systems enable a batch or event-based synchronisation, so that Members are provisioned in Umbraco when they are created in the core Directory.
Another alternative, explored here, is just-in-time member provisioning. In this scenario we respond to SAML Assertions from new Members not recognised by Umbraco by automatically provisioning the Member in the backend.
In our example below, I've updated the AssertionConsumerService so that we trust all new Members that arrive from the Identity Provider. If we fail to find the member using memberService.GetByUsername() then we create the Member using memberService.CreateMember() and assign the basic group to the Member.
var memberService = ApplicationContext.Current.Services.MemberService;
var checkMember = memberService.GetByUsername(userName);
if (checkMember == null)
{
var newMember = memberService.CreateMember(userName, userName, userName, "Member");
memberService.Save(newMember);
memberService.AssignRole(newMember.Id, "MemberGroup");
}
FormsAuthentication.SetAuthCookie(userName, false);
Member authorisation
As with provisioning, in early projects we have taken a really simple approach to managing Member Authorisation through the use of standard Umbraco Member Groups. When a Member is logged in and matched against a Member in Umbraco, we pick up all the Member Group allocations in the normal way in Umbraco, which then enables us to piggyback off the core Public Access feature which protects pages.
The management of allocations of Members to Member Groups is either: Manually allocate on an ad-hoc basis in the Umbraco backend; or via a batch import using our own Member Group Import Tool.
However, SAML provides us with Attributes in the assertion that can be used to make Member Group allocation decisions. Using Okta we can pass Active Directory Group membership details via the Attributes. These AD groups could be used to match against Umbraco Groups.
Conclusion
In this article we've taken a quick look at how easy it can be to implement SAML-based single sign on in an Umbraco-based site. We've successfully implemented this on a number of sites for large corporates and mid-sized regional clients, as well as our own Intranet hosted on Umbraco Cloud. We feel that's it's a really effective way of integrating Umbraco into a larger estate of IT systems which can really improve the adoption of the sites we've delivered, as well as reducing their vulnerability to attack.
Acknowledgements
Much of the early development that contributed to this article was done at Carbon Six Digital by Ian Houghton. The UML diagram in the body of the article is by Tom Scavo.