Gets U4-6753 Identity support must have an option to enable auto-linked accounts working
This commit is contained in:
@@ -12,6 +12,13 @@ namespace Umbraco.Core.Models.Identity
|
||||
public class BackOfficeIdentityUser : IdentityUser<int, IIdentityUserLogin, IdentityUserRole<string>, IdentityUserClaim<int>>
|
||||
{
|
||||
|
||||
public BackOfficeIdentityUser()
|
||||
{
|
||||
StartMediaId = -1;
|
||||
StartContentId = -1;
|
||||
Culture = Configuration.GlobalSettings.DefaultUILanguage;
|
||||
}
|
||||
|
||||
public virtual async Task<ClaimsIdentity> GenerateUserIdentityAsync(BackOfficeUserManager manager)
|
||||
{
|
||||
// NOTE the authenticationType must match the umbraco one
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
/// <param name="userId"/>
|
||||
/// <returns/>
|
||||
public Task<BackOfficeIdentityUser> FindByIdAsync(int userId)
|
||||
public async Task<BackOfficeIdentityUser> 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<BackOfficeIdentityUser>(user)));
|
||||
return await Task.FromResult(AssignLoginsCallback(Mapper.Map<BackOfficeIdentityUser>(user)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -184,7 +184,7 @@ namespace Umbraco.Core.Security
|
||||
/// </summary>
|
||||
/// <param name="userName"/>
|
||||
/// <returns/>
|
||||
public Task<BackOfficeIdentityUser> FindByNameAsync(string userName)
|
||||
public async Task<BackOfficeIdentityUser> FindByNameAsync(string userName)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var user = _userService.GetByUsername(userName);
|
||||
@@ -195,7 +195,7 @@ namespace Umbraco.Core.Security
|
||||
|
||||
var result = AssignLoginsCallback(Mapper.Map<BackOfficeIdentityUser>(user));
|
||||
|
||||
return Task.FromResult(result);
|
||||
return await Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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<bool> 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<BackOfficeController>("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);
|
||||
|
||||
@@ -7,6 +7,31 @@ namespace Umbraco.Web.Security.Identity
|
||||
{
|
||||
public static class AuthenticationOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Used during the External authentication process to assign external sign-in options
|
||||
/// that are used by the Umbraco authentication process.
|
||||
/// </summary>
|
||||
/// <param name="authOptions"></param>
|
||||
/// <param name="options"></param>
|
||||
public static void SetExternalAuthenticationOptions(
|
||||
this AuthenticationOptions authOptions,
|
||||
ExternalSignInAutoLinkOptions options)
|
||||
{
|
||||
authOptions.Description.Properties["ExternalSignInAutoLinkOptions"] = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used during the External authentication process to retrieve external sign-in options
|
||||
/// that have been set with SetExternalAuthenticationOptions
|
||||
/// </summary>
|
||||
/// <param name="authenticationDescription"></param>
|
||||
public static ExternalSignInAutoLinkOptions GetExternalAuthenticationOptions(this AuthenticationDescription authenticationDescription)
|
||||
{
|
||||
if (authenticationDescription.Properties.ContainsKey("ExternalSignInAutoLinkOptions") == false) return null;
|
||||
var options = authenticationDescription.Properties["ExternalSignInAutoLinkOptions"] as ExternalSignInAutoLinkOptions;
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the properties of the authentication description instance for use with Umbraco back office
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
|
||||
namespace Umbraco.Web.Security.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Options used to configure auto-linking external OAuth providers
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The default User Type alias to use for auto-linking users
|
||||
/// </summary>
|
||||
public string GetDefaultUserType(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
return _defaultUserType;
|
||||
}
|
||||
|
||||
private readonly string[] _defaultAllowedSections;
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed sections to use for auto-linking users
|
||||
/// </summary>
|
||||
public string[] GetDefaultAllowedSections(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
return _defaultAllowedSections;
|
||||
}
|
||||
|
||||
private readonly bool _autoLinkExternalAccount;
|
||||
|
||||
/// <summary>
|
||||
/// 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!!!
|
||||
/// </summary>
|
||||
public bool ShouldAutoLinkExternalAccount(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
return _autoLinkExternalAccount;
|
||||
}
|
||||
|
||||
private readonly string _autoLinkExternalAccountView;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string GetAutoLinkExternalAccountView(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
return _autoLinkExternalAccountView;
|
||||
}
|
||||
|
||||
private readonly string _defaultCulture;
|
||||
|
||||
/// <summary>
|
||||
/// The default Culture to use for auto-linking users
|
||||
/// </summary>
|
||||
public string GetDefaultCulture(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
return _defaultCulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@ namespace Umbraco.Web.Security.Identity
|
||||
{
|
||||
internal static class OwinExtensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Nasty little hack to get httpcontextbase from an owin context
|
||||
/// </summary>
|
||||
/// <param name="owinContext"></param>
|
||||
/// <returns></returns>
|
||||
public static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext)
|
||||
internal static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext)
|
||||
{
|
||||
return owinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
<Compile Include="HtmlHelperBackOfficeExtensions.cs" />
|
||||
<Compile Include="Media\EmbedProviders\Flickr.cs" />
|
||||
<Compile Include="PropertyEditors\DatePreValueEditor.cs" />
|
||||
<Compile Include="Security\Identity\ExternalSignInAutoLinkOptions.cs" />
|
||||
<Compile Include="Security\Identity\GetUserSecondsMiddleWare.cs" />
|
||||
<Compile Include="Media\EmbedProviders\OEmbedJson.cs" />
|
||||
<Compile Include="Media\EmbedProviders\OEmbedResponse.cs" />
|
||||
|
||||
Reference in New Issue
Block a user