Last active
November 4, 2021 07:05
-
-
Save Sam7/0b99d6c180bdaa445501d1b19bc3e451 to your computer and use it in GitHub Desktop.
Umbraco AD FS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static void OnAutoLinking(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) | |
{ | |
OnExternalLogin(autoLinkUser, loginInfo); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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