diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 1523cf9040..9a53023a8d 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -12,6 +12,13 @@ namespace Umbraco.Core.Models.Identity public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> { + public BackOfficeIdentityUser() + { + StartMediaId = -1; + StartContentId = -1; + Culture = Configuration.GlobalSettings.DefaultUILanguage; + } + public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) { // NOTE the authenticationType must match the umbraco one diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index f6d8222c44..cf433b729c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -75,12 +75,12 @@ namespace Umbraco.Core.Security { DefaultToLiveEditing = false, Email = user.Email, - Language = Configuration.GlobalSettings.DefaultUILanguage, + Language = user.Culture ?? Configuration.GlobalSettings.DefaultUILanguage, Name = user.Name, Username = user.UserName, - StartContentId = -1, - StartMediaId = -1, - IsLockedOut = false, + StartContentId = user.StartContentId == 0 ? -1 : user.StartContentId, + StartMediaId = user.StartMediaId == 0 ? -1 : user.StartMediaId, + IsLockedOut = user.LockoutEnabled, IsApproved = true }; @@ -168,7 +168,7 @@ namespace Umbraco.Core.Security /// /// /// - public Task FindByIdAsync(int userId) + public async Task FindByIdAsync(int userId) { ThrowIfDisposed(); var user = _userService.GetUserById(userId); @@ -176,7 +176,7 @@ namespace Umbraco.Core.Security { return null; } - return Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); + return await Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); } /// @@ -184,7 +184,7 @@ namespace Umbraco.Core.Security /// /// /// - public Task FindByNameAsync(string userName) + public async Task FindByNameAsync(string userName) { ThrowIfDisposed(); var user = _userService.GetByUsername(userName); @@ -195,7 +195,7 @@ namespace Umbraco.Core.Security var result = AssignLoginsCallback(Mapper.Map(user)); - return Task.FromResult(result); + return await Task.FromResult(result); } /// diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 8c1c23fbcb..ad1e4ea571 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -37,6 +37,7 @@ using System.Web; using AutoMapper; using Microsoft.AspNet.Identity.Owin; using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Task = System.Threading.Tasks.Task; using Umbraco.Web.Security.Identity; @@ -478,7 +479,10 @@ namespace Umbraco.Web.Editors } else { - ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + if (await AutoLinkAndSignInExternalAccount(loginInfo) == false) + { + ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + } //Remove the cookie otherwise this message will keep appearing if (Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName] != null) @@ -490,6 +494,104 @@ namespace Umbraco.Web.Editors return response(); } + private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo) + { + //Here we can check if the provider associated with the request has been configured to allow + // new users (auto-linked external accounts). This would never be used with public providers such as + // Google, unless you for some reason wanted anybody to be able to access the backend if they have a Google account + // .... not likely! + + var authType = OwinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == loginInfo.Login.LoginProvider); + if (authType == null) + { + Logger.Warn("Could not find external authentication provider registered: " + loginInfo.Login.LoginProvider); + return false; + } + + var autoLinkOptions = authType.GetExternalAuthenticationOptions(); + if (autoLinkOptions != null) + { + if (autoLinkOptions.ShouldAutoLinkExternalAccount(UmbracoContext, loginInfo)) + { + //we are allowing auto-linking/creating of local accounts + if (loginInfo.Email.IsNullOrWhiteSpace()) + { + ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + } + else + { + + //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address + var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); + if (foundByEmail != null) + { + ViewBag.ExternalSignInError = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; + } + else + { + var defaultUserType = autoLinkOptions.GetDefaultUserType(UmbracoContext, loginInfo); + var userType = Services.UserService.GetUserTypeByAlias(defaultUserType); + if (userType == null) + { + ViewBag.ExternalSignInError = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; + } + else + { + //var userMembershipProvider = global::Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); + + var autoLinkUser = new BackOfficeIdentityUser() + { + Email = loginInfo.Email, + Name = loginInfo.ExternalIdentity.Name, + UserTypeAlias = userType.Alias, + AllowedSections = autoLinkOptions.GetDefaultAllowedSections(UmbracoContext, loginInfo), + Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo), + UserName = loginInfo.Email + }; + var userCreationResult = await UserManager.CreateAsync(autoLinkUser); + + if (userCreationResult.Succeeded == false) + { + ViewBag.ExternalSignInError = userCreationResult.Errors; + } + else + { + var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + if (linkResult.Succeeded == false) + { + ViewBag.ExternalSignInError = linkResult.Errors; + + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded == false) + { + //DOH! ... this isn't good, combine all errors to be shown + ViewBag.ExternalSignInError = linkResult.Errors.Concat(deleteResult.Errors); + } + } + else + { + + //Ok, we're all linked up! Assign the auto-link options to a ViewBag property, this can be used + // in the view to render a custom view (AutoLinkExternalAccountView) if required, which will allow + // a developer to display a custom angular view to prompt the user for more information if required. + ViewBag.ExternalSignInAutoLinkOptions = autoLinkOptions; + + //sign in + await SignInAsync(autoLinkUser, isPersistent: false); + } + } + } + } + + } + } + return true; + } + + return false; + } + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) { OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs index 43b995bb06..2d4aef52fa 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs @@ -7,6 +7,31 @@ namespace Umbraco.Web.Security.Identity { public static class AuthenticationOptionsExtensions { + /// + /// Used during the External authentication process to assign external sign-in options + /// that are used by the Umbraco authentication process. + /// + /// + /// + public static void SetExternalAuthenticationOptions( + this AuthenticationOptions authOptions, + ExternalSignInAutoLinkOptions options) + { + authOptions.Description.Properties["ExternalSignInAutoLinkOptions"] = options; + } + + /// + /// Used during the External authentication process to retrieve external sign-in options + /// that have been set with SetExternalAuthenticationOptions + /// + /// + public static ExternalSignInAutoLinkOptions GetExternalAuthenticationOptions(this AuthenticationDescription authenticationDescription) + { + if (authenticationDescription.Properties.ContainsKey("ExternalSignInAutoLinkOptions") == false) return null; + var options = authenticationDescription.Properties["ExternalSignInAutoLinkOptions"] as ExternalSignInAutoLinkOptions; + return options; + } + /// /// Configures the properties of the authentication description instance for use with Umbraco back office /// diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs new file mode 100644 index 0000000000..9fb9a88a45 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Options used to configure auto-linking external OAuth providers + /// + public sealed class ExternalSignInAutoLinkOptions + { + + public ExternalSignInAutoLinkOptions( + bool autoLinkExternalAccount = false, + string defaultUserType = "editor", string[] defaultAllowedSections = null, string defaultCulture = null, string autoLinkExternalAccountView = null) + { + Mandate.ParameterNotNullOrEmpty(defaultUserType, "defaultUserType"); + + _defaultUserType = defaultUserType; + _defaultAllowedSections = defaultAllowedSections ?? new[] { "content", "media" }; + _autoLinkExternalAccount = autoLinkExternalAccount; + _autoLinkExternalAccountView = autoLinkExternalAccountView; + _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; + } + + private readonly string _defaultUserType; + + /// + /// The default User Type alias to use for auto-linking users + /// + public string GetDefaultUserType(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultUserType; + } + + private readonly string[] _defaultAllowedSections; + + /// + /// The default allowed sections to use for auto-linking users + /// + public string[] GetDefaultAllowedSections(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultAllowedSections; + } + + private readonly bool _autoLinkExternalAccount; + + /// + /// For private external auth providers such as Active Directory, which when set to true will automatically + /// create a local user if the external provider login was successful. + /// + /// For public auth providers this should always be false!!! + /// + public bool ShouldAutoLinkExternalAccount(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _autoLinkExternalAccount; + } + + private readonly string _autoLinkExternalAccountView; + + /// + /// Generally this is empty which means auto-linking will be silent, however in some cases developers may want to + /// prompt the user to enter additional user information that they want to save with the user that has been created. + /// + public string GetAutoLinkExternalAccountView(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _autoLinkExternalAccountView; + } + + private readonly string _defaultCulture; + + /// + /// The default Culture to use for auto-linking users + /// + public string GetDefaultCulture(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultCulture; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs index 4b83f97bd3..ceb0bdafb2 100644 --- a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs @@ -5,12 +5,13 @@ namespace Umbraco.Web.Security.Identity { internal static class OwinExtensions { + /// /// Nasty little hack to get httpcontextbase from an owin context /// /// /// - public static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) + internal static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) { return owinContext.Get(typeof(HttpContextBase).FullName); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ea3f838076..b9df10254d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -304,6 +304,7 @@ +