Skip to content

Instantly share code, notes, and snippets.

@dsolovay
Last active December 10, 2021 22:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dsolovay/31ba7e84c2bb57ec67981f8c0b5af8d0 to your computer and use it in GitHub Desktop.
Save dsolovay/31ba7e84c2bb57ec67981f8c0b5af8d0 to your computer and use it in GitHub Desktop.
Code samples for Part 2 of Sustainsys.SAML2 Sitecore Identity plugin
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using IdentityServer4;
using IdentityServer4.Configuration;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Sitecore.Framework.Runtime.Configuration;
using Sustainsys.Saml2;
using Sustainsys.Saml2.Configuration;
using Sustainsys.Saml2.Metadata;
using Sustainsys.Saml2.WebSso;
namespace SitecoreIdentitySamlDemo
{
public class ConfigureSitecore
{
private Saml2Settings _settings;
private readonly ISitecoreConfiguration _sitecoreConfiguration;
private readonly ILogger _logger;
public ConfigureSitecore(ISitecoreConfiguration sitecoreConfiguration, ILogger<ConfigureSitecore> logger)
{
_sitecoreConfiguration = sitecoreConfiguration;
_logger = logger;
_settings = new Saml2Settings();
_sitecoreConfiguration.Bind("Sitecore:ExternalIdentityProviders:IdentityProviders:Saml2Configuration", _settings);
}
public void ConfigureServices(IServiceCollection services)
{
if (!_settings.Enabled)
{
return;
}
var builder = new AuthenticationBuilder(services);
builder.AddSaml2(_settings.AuthenticationScheme, _settings.DisplayName, options =>
{
// Required by Sitecore Identity. See https://doc.sitecore.com/xp/en/developers/102/sitecore-experience-manager/use-the-sitecore-identity-server-as-a-federation-gateway.html
options.SignInScheme = "idsrv.external";
options.SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme;
options.SPOptions.EntityId = new EntityId(_settings.EntityId);
IdentityProvider provider = GetIdentityProvider(options.SPOptions);
options.IdentityProviders.Add(provider);
if (!string.IsNullOrEmpty(_settings.PfxPath) && !string.IsNullOrEmpty(_settings.PfxPassword))
{
options.SPOptions.ServiceCertificates.Add(GetCertificate());
options.SPOptions.AuthenticateRequestSigningBehavior =
Enum.TryParse(_settings.AuthenticationRequestSigningBehavior, ignoreCase: true, out SigningBehavior result)
? result
: SigningBehavior.IfIdpWantAuthnRequestsSigned;
options.SPOptions.OutboundSigningAlgorithm = _settings.OutboundSigningAlgorithm.ConvertNullOrEmptyTo("SHA256");
}
options.Notifications.AcsCommandResultCreated += (result, response) =>
{
ClaimsIdentity theClaimsHolder = result.Principal.Identity as System.Security.Claims.ClaimsIdentity;
IEnumerable<Claim> incomingClaims = theClaimsHolder.Claims;
// Any claims removed from "incomingClaims" will not be added to the cookie, avoiding the error: "The size of the request headers is too long."
// See https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB0522544 for the issue, though this fix is specific to Sustainsys.SAML2.
};
});
}
private X509Certificate2 GetCertificate()
{
byte[] cert = File.ReadAllBytes(_settings.PfxPath);
SecureString ss = new SecureString();
_settings.PfxPassword.ToCharArray().ToList().ForEach(c => ss.AppendChar(c));
return new X509Certificate2(cert, ss);
}
private IdentityProvider GetIdentityProvider(SPOptions options)
{
var idp = new IdentityProvider(new EntityId(_settings.IdentityProviderId), options);
idp.Binding = Saml2BindingType.HttpPost;
// "Load metadata from the idp and use that information instead of the configuration. It is possible to use a specific certificate
// even though the metadata is loaded, in that case the configured certificate will take precedence over any contents in the metadata."
// See https://saml2.sustainsys.com/en/stable/config-elements/identity-providers.html?highlight=loadmetadata#identityproviders-element
idp.LoadMetadata = true;
return idp;
}
}
// https://stackoverflow.com/a/2420155/402949
public static class StringExtensions
{
public static string ConvertNullOrEmptyTo(this string input, string defaultValue)
{
return !string.IsNullOrEmpty(input) ? input : defaultValue;
}
}
}
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Sitecore.Owin.Authentication.Identity;
using Sitecore.Owin.Authentication.Services;
using Sitecore.SecurityModel.Cryptography;
namespace CustomUserBuilder
{
public class CustomExternalUserBuilder: DefaultExternalUserBuilder
{
public CustomExternalUserBuilder(ApplicationUserFactory applicationUserFactory, IHashEncryption hashEncryption) : base(applicationUserFactory, hashEncryption)
{
}
protected override string CreateUniqueUserName(UserManager<ApplicationUser> userManager, ExternalLoginInfo externalLoginInfo)
{
string hashDerivedName = base.CreateUniqueUserName(userManager, externalLoginInfo);
string domain = hashDerivedName.Substring(0, hashDerivedName.IndexOf('\\'));
string providerKeyName = domain + @"\" + externalLoginInfo.Login.ProviderKey;
// Check name not already assigned.
return userManager.FindByName(providerKeyName) == null ? providerKeyName : hashDerivedName;
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
xmlns:set="http://www.sitecore.net/xmlconfig/set">
<sitecore>
<federatedAuthentication>
<propertyInitializer>
<maps>
<map name="set FullName" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="name" />
<target name="FullName" />
</data>
</map>
<map name="set Email" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="email" />
<target name="Email" />
</data>
</map>
<map name="set Portait" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="portrait" />
<target name="Portrait" />
</data>
</map>
</maps>
</propertyInitializer>
<identityProvidersPerSites>
<mapEntry name ="all sites">
<externalUserBuilder set:type="CustomUserBuilder.CustomExternalUserBuilder, CustomUserBuilder" resolve="true">
<IsPersistentUser>true</IsPersistentUser>
</externalUserBuilder>
</mapEntry>
</identityProvidersPerSites>
</federatedAuthentication>
</sitecore>
</configuration>
namespace SitecoreIdentitySamlDemo
{
internal class Saml2Settings : Sitecore.Plugin.IdentityProviders.IdentityProvider
{
public string EntityId { get; set; }
public string PfxPath { get; set; }
public string PfxPassword { get; set; }
public string AuthenticationRequestSigningBehavior { get; set; }
public string OutboundSigningAlgorithm { get; set; }
public string IdentityProviderId { get; set; }
}
}
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<Sitecore>
<!-- Add the "portrait" claim -->
<IdentityServer>
<IdentityResources>
<SitecoreIdentityResource>
<UserClaims>
<Portait>portrait</Portait>
</UserClaims>
</SitecoreIdentityResource>
</IdentityResources>
</IdentityServer>
<!-- Plugin configuration -->
<ExternalIdentityProviders>
<IdentityProviders>
<Saml2Configuration type="Sitecore.Plugin.IdentityProviders.IdentityProvider, Sitecore.Plugin.IdentityProviders">
<AuthenticationScheme>Saml2</AuthenticationScheme>
<DisplayName>Sustainssys SAML2 Stub Login</DisplayName>
<Enabled>true</Enabled>
<EntityId>https://testidserver2/Saml2</EntityId>
<PfxPath>.\mycert.pfx</PfxPath>
<PfxPassword>secret</PfxPassword>
<AuthenticationRequestSigningBehavior>Always</AuthenticationRequestSigningBehavior>
<IdentityProviderId>https://stubidp.sustainsys.com/Metadata</IdentityProviderId>
<ClaimsTransformations>
<ClaimsTransformation1 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" />
</SourceClaims>
<NewClaims>
<Claim1 type="email" />
</NewClaims>
</ClaimsTransformation1 >
<ClaimsTransformation2 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
</SourceClaims>
<NewClaims>
<Claim1 type="email" />
</NewClaims>
</ClaimsTransformation2>
<AuthorRule type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="Author" />
</SourceClaims>
<NewClaims>
<Claim1 type="role" value="sitecore\Author" />
<Claim2 type="portrait" value="office/16x16/pencil.png" />
</NewClaims>
</AuthorRule>
<AdminRule type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="Administrator" />
</SourceClaims>
<NewClaims>
<Claim1 type="http://www.sitecore.net/identity/claims/isAdmin" value="true"/>
<Claim2 type="portrait" value="office/16x16/magician.png" />
</NewClaims>
</AdminRule>
<DisplayNameRule type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="DisplayName" />
</SourceClaims>
<NewClaims>
<Claim1 type="name" />
</NewClaims>
</DisplayNameRule>
</ClaimsTransformations>
</Saml2Configuration>
</IdentityProviders>
</ExternalIdentityProviders>
</Sitecore>
</Settings>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment