Gets U4-6753 Identity support must have an option to enable auto-linked accounts working

This commit is contained in:
Shannon
2015-06-26 16:59:40 +02:00
parent 42b2b16f0e
commit b42959f663
7 changed files with 227 additions and 10 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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" />