Skip to content

Instantly share code, notes, and snippets.

@Sam7
Last active November 4, 2021 07:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Sam7/0b99d6c180bdaa445501d1b19bc3e451 to your computer and use it in GitHub Desktop.
Save Sam7/0b99d6c180bdaa445501d1b19bc3e451 to your computer and use it in GitHub Desktop.
Umbraco AD FS
using Microsoft.Owin;
using Owin;
using Umbraco.Core;
using Umbraco.Core.Security;
using Umbraco.Web.Security.Identity;
//To use this startup class, change the appSetting value in the web.config called
// "owin:appStartup" to be "UmbracoCustomOwinStartup"
[assembly: OwinStartup("UmbracoCustomOwinStartup", typeof(UmbracoCustomOwinStartup))]
namespace UmbracoProject.Web
{
using System;
using System.Configuration;
using System.Linq;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.WsFederation;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Strings;
using Umbraco.Web;
/// <summary>
/// A custom way to configure OWIN for Umbraco
/// </summary>
/// <remarks>
/// The startup type is specified in appSettings under owin:appStartup - change it to "UmbracoCustomOwinStartup" to use this class
///
/// This startup class would allow you to customize the Identity IUserStore and/or IUserManager for the Umbraco Backoffice
/// </remarks>
public class UmbracoCustomOwinStartup
{
}
}
public void Configuration(IAppBuilder app)
{
//Configure the Identity user manager for use with Umbraco Back office
// *** EXPERT: There are several overloads of this method that allow you to specify a custom UserStore or even a custom UserManager!
app.ConfigureUserManagerForUmbracoBackOffice(
ApplicationContext.Current,
//The Umbraco membership provider needs to be specified in order to maintain backwards compatibility with the
// user password formats. The membership provider is not used for authentication, if you require custom logic
// to validate the username/password against an external data source you can create create a custom UserManager
// and override CheckPasswordAsync
MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider());
// Due to a problem with the cookie management caused by OWIN and ASP.NET we need to use this KentorOwinCookieSaver as described here: https://stackoverflow.com/a/26978166/465509
// If this isn't used, the external login will seemingly randomly stop working after a while.
app.UseKentorOwinCookieSaver();
//Ensure owin is configured for Umbraco back office authentication
base.Configuration(app);
// Configure additional back office authentication options
ConfigureBackOfficeAdfsAuthentication(app);
// room to configure other things like hangfire
// HangfireConfiguration.Configuration(app);
// HangfireConfiguration.Start();
}
private static void ConfigureBackOfficeAdfsAuthentication(
IAppBuilder app,
string caption = "AD FS",
string style = "btn-microsoft",
string icon = "fa-windows")
{
// Load configuration from web.config
var adfsMetadataEndpoint = ConfigurationManager.AppSettings["AdfsMetadataEndpoint"];
var adfsRelyingParty = ConfigurationManager.AppSettings["AdfsRelyingParty"];
var adfsFederationServerIdentifier = ConfigurationManager.AppSettings["AdfsFederationServerIdentifier"];
var adfsReplyUrl = ConfigurationManager.AppSettings["AdfsReplyUrl"];
app.SetDefaultSignInAsAuthenticationType(Constants.Security.BackOfficeExternalAuthenticationType);
var wsFedOptions = new WsFederationAuthenticationOptions
{
Wtrealm = adfsRelyingParty,
MetadataAddress = adfsMetadataEndpoint,
SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType,
Caption = caption,
Wreply = adfsReplyUrl, // Redirect to the Umbraco back office after succesful authentication
};
// The crucial bit, where we hook into the events when users login or when they are created
wsFedOptions.SetExternalSignInAutoLinkOptions(new ExternalSignInAutoLinkOptions(true, new string[0])
{
OnAutoLinking = OnAutoLinking,
OnExternalLogin = OnExternalLogin
});
// Apply options
wsFedOptions.ForUmbracoBackOffice(style, icon);
wsFedOptions.AuthenticationType = adfsFederationServerIdentifier;
app.UseWsFederationAuthentication(wsFedOptions);
}
using Microsoft.Owin;
using Owin;
using Umbraco.Core;
using Umbraco.Core.Security;
using Umbraco.Web.Security.Identity;
//To use this startup class, change the appSetting value in the web.config called
// "owin:appStartup" to be "UmbracoCustomOwinStartup"
[assembly: OwinStartup("UmbracoCustomOwinStartup", typeof(UmbracoCustomOwinStartup))]
namespace UmbracoProject.Web
{
using System;
using System.Configuration;
using System.Linq;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.WsFederation;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Strings;
using Umbraco.Web;
/// <summary>
/// A custom way to configure OWIN for Umbraco
/// </summary>
/// <remarks>
/// The startup type is specified in appSettings under owin:appStartup - change it to "UmbracoCustomOwinStartup" to use this class
///
/// This startup class would allow you to customize the Identity IUserStore and/or IUserManager for the Umbraco Backoffice
/// </remarks>
public class UmbracoCustomOwinStartup
{
private const string ClaimsTypeRole = "http://schemas.xmlsoap.org/claims/Group";
private const string ActiveDirectoryRolePrefix = "SG-STA-Umbraco";
private const string GroupAliasPrefix = "AD";
private const string GroupLabelPrefix = "AD Group: ";
public void Configuration(IAppBuilder app)
{
//Configure the Identity user manager for use with Umbraco Back office
// *** EXPERT: There are several overloads of this method that allow you to specify a custom UserStore or even a custom UserManager!
app.ConfigureUserManagerForUmbracoBackOffice(
ApplicationContext.Current,
//The Umbraco membership provider needs to be specified in order to maintain backwards compatibility with the
// user password formats. The membership provider is not used for authentication, if you require custom logic
// to validate the username/password against an external data source you can create create a custom UserManager
// and override CheckPasswordAsync
MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider());
// Due to a problem with the cookie management caused by OWIN and ASP.NET we need to use this KentorOwinCookieSaver as described here: https://stackoverflow.com/a/26978166/465509
// If this isn't used, the external login will seemingly randomly stop working after a while.
app.UseKentorOwinCookieSaver();
//Ensure owin is configured for Umbraco back office authentication
base.Configuration(app);
// Configure additional back office authentication options
ConfigureBackOfficeAdfsAuthentication(app);
// room to configure other things like hangfire
// HangfireConfiguration.Configuration(app);
// HangfireConfiguration.Start();
}
private static void ConfigureBackOfficeAdfsAuthentication(
IAppBuilder app,
string caption = "AD FS",
string style = "btn-microsoft",
string icon = "fa-windows")
{
// Load configuration from web.config
var adfsMetadataEndpoint = ConfigurationManager.AppSettings["AdfsMetadataEndpoint"];
var adfsRelyingParty = ConfigurationManager.AppSettings["AdfsRelyingParty"];
var adfsFederationServerIdentifier = ConfigurationManager.AppSettings["AdfsFederationServerIdentifier"];
var adfsReplyUrl = ConfigurationManager.AppSettings["AdfsReplyUrl"];
app.SetDefaultSignInAsAuthenticationType(Constants.Security.BackOfficeExternalAuthenticationType);
var wsFedOptions = new WsFederationAuthenticationOptions
{
Wtrealm = adfsRelyingParty,
MetadataAddress = adfsMetadataEndpoint,
SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType,
Caption = caption,
Wreply = adfsReplyUrl, // Redirect to the Umbraco back office after succesful authentication
};
// The crucial bit, where we hook into the events when users login or when they are created
wsFedOptions.SetExternalSignInAutoLinkOptions(new ExternalSignInAutoLinkOptions(true, new string[0])
{
OnAutoLinking = OnAutoLinking,
OnExternalLogin = OnExternalLogin
});
// Apply options
wsFedOptions.ForUmbracoBackOffice(style, icon);
wsFedOptions.AuthenticationType = adfsFederationServerIdentifier;
app.UseWsFederationAuthentication(wsFedOptions);
}
private static void OnAutoLinking(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
{
OnExternalLogin(autoLinkUser, loginInfo);
}
private static bool OnExternalLogin(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
{
// customise the user permissions based on the role
var adGroupNames = loginInfo.ExternalIdentity.Claims
.Where(x => x.Type.Equals(ClaimsTypeRole, StringComparison.CurrentCultureIgnoreCase) && x.Value.StartsWith(ActiveDirectoryRolePrefix, StringComparison.CurrentCultureIgnoreCase))
// remove the prefix, add new one and clean string to Umbraco Alias standard
.ToDictionary(x => (GroupAliasPrefix + x.Value.Substring(ActiveDirectoryRolePrefix.Length)).ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase));
// figure out what groups to add or remove.
var groupsToRemove = autoLinkUser.Groups.Where(x => x.Alias.StartsWith(GroupAliasPrefix) && !adGroupNames.ContainsKey(x.Alias)).ToArray();
var groupsToAdd = adGroupNames.Keys.Where(newGroupAlias => !autoLinkUser.Groups.Any(x => x.Alias.Equals(newGroupAlias)));
// remove old groups
// for some reason it only works if we adjust the groups first and then the roles.
// only works when both are changed and only in that order :S
var groups = autoLinkUser.Groups.ToList();
foreach (var adGroup in groupsToRemove)
groups.RemoveAll(x => x.Alias.Equals(adGroup.Alias));
autoLinkUser.Groups = groups.ToArray();
// the same for roles
foreach (var adGroup in groupsToRemove)
{
var userRole = autoLinkUser.Roles.FirstOrDefault(x => x.RoleId.Equals(adGroup.Alias));
if (userRole == null)
continue;
autoLinkUser.Roles.Remove(userRole);
}
// add new groups
foreach (var adGroup in groupsToAdd.Where(s => !string.IsNullOrWhiteSpace(s)))
{
var userService = UmbracoContext.Current.Application.Services.UserService;
var userGroup = userService.GetUserGroupByAlias(adGroup);
if (userGroup == null)
{
// Create new Group without permissions. They have to be
userGroup = new UserGroup { Alias = adGroup, Name = GroupLabelPrefix + adGroupNames[adGroup].Value };
userService.Save(userGroup);
}
autoLinkUser.AddRole(userGroup.Alias);
}
return adGroupNames.Any();
}
}
}
private static void OnAutoLinking(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
{
OnExternalLogin(autoLinkUser, loginInfo);
}
private const string ClaimsTypeRole = "http://schemas.xmlsoap.org/claims/Group";
// Only take AD groups into consideration that have start with this prefix.
private const string ActiveDirectoryRolePrefix = "SG-STA-Umbraco";
// Append this prefix to the group alias in order not to get confused with manually created groups
private const string GroupAliasPrefix = "AD";
// Append this prefix to the group label / name in order not to get confused with manually created groups
private const string GroupLabelPrefix = "AD Group: ";
private static bool OnExternalLogin(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
{
// customise the user permissions based on the role
var adGroupNames = loginInfo.ExternalIdentity.Claims
.Where(x => x.Type.Equals(ClaimsTypeRole, StringComparison.CurrentCultureIgnoreCase) && x.Value.StartsWith(ActiveDirectoryRolePrefix, StringComparison.CurrentCultureIgnoreCase))
// remove the prefix, add new one and clean string to Umbraco Alias standard
.ToDictionary(x => (GroupAliasPrefix + x.Value.Substring(ActiveDirectoryRolePrefix.Length)).ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase));
// figure out what groups to add or remove.
var groupsToRemove = autoLinkUser.Groups.Where(x => x.Alias.StartsWith(GroupAliasPrefix) && !adGroupNames.ContainsKey(x.Alias)).ToArray();
var groupsToAdd = adGroupNames.Keys.Where(newGroupAlias => !autoLinkUser.Groups.Any(x => x.Alias.Equals(newGroupAlias)));
// remove old groups
// for some reason it only works if we adjust the groups first and then the roles.
// only works when both are changed and only in that order :S
var groups = autoLinkUser.Groups.ToList();
foreach (var adGroup in groupsToRemove)
groups.RemoveAll(x => x.Alias.Equals(adGroup.Alias));
autoLinkUser.Groups = groups.ToArray();
// the same for roles
foreach (var adGroup in groupsToRemove)
{
var userRole = autoLinkUser.Roles.FirstOrDefault(x => x.RoleId.Equals(adGroup.Alias));
if (userRole == null)
continue;
autoLinkUser.Roles.Remove(userRole);
}
// add new groups
foreach (var adGroup in groupsToAdd.Where(s => !string.IsNullOrWhiteSpace(s)))
{
var userService = UmbracoContext.Current.Application.Services.UserService;
var userGroup = userService.GetUserGroupByAlias(adGroup);
if (userGroup == null)
{
// Create new Group without permissions. They have to be
userGroup = new UserGroup { Alias = adGroup, Name = GroupLabelPrefix + adGroupNames[adGroup].Value };
userService.Save(userGroup);
}
autoLinkUser.AddRole(userGroup.Alias);
}
return adGroupNames.Any();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment