diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 8404768f40..b8e60eb757 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -373,6 +373,19 @@ namespace Umbraco.Web.Editors if (loginInfo == null) throw new ArgumentNullException("loginInfo"); if (response == null) throw new ArgumentNullException("response"); + + //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); + } + + var autoLinkOptions = authType.GetExternalAuthenticationOptions(); + // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); if (user != null) @@ -383,12 +396,25 @@ namespace Umbraco.Web.Editors // ticket format, etc.. to create our back office user including the claims assigned and in this method we'd just ensure // that the ticket is created and stored and that the user is logged in. - //sign in - await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + var shouldSignIn = true; + if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) + { + shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); + if (shouldSignIn == false) + { + Logger.Warn("The AutoLinkOptions of the external authentication provider '" + loginInfo.Login.LoginProvider + "' have refused the login based on the OnExternalLogin method. Affected user id: '" + user.Id + "'"); + } + } + + if (shouldSignIn) + { + //sign in + await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + } } else { - if (await AutoLinkAndSignInExternalAccount(loginInfo) == false) + if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false) { ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; } @@ -403,97 +429,81 @@ namespace Umbraco.Web.Editors return response(); } - private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo) + private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions) { - //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); + if (autoLinkOptions == null) return false; - } - var autoLinkOptions = authType.GetExternalAuthenticationOptions(); - if (autoLinkOptions != null) + if (autoLinkOptions.ShouldAutoLinkExternalAccount(this.UmbracoContext, loginInfo) == false) + return true; + + //we are allowing auto-linking/creating of local accounts + if (loginInfo.Email.IsNullOrWhiteSpace()) { - if (autoLinkOptions.ShouldAutoLinkExternalAccount(UmbracoContext, loginInfo)) + this.ViewData[TokenExternalSignInError] = 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 = this.Services.UserService.GetByEmail(loginInfo.Email); + if (foundByEmail != null) { - //we are allowing auto-linking/creating of local accounts - if (loginInfo.Email.IsNullOrWhiteSpace()) + this.ViewData[TokenExternalSignInError] = 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 + { + if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); + if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); + + var groups = this.Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(this.UmbracoContext, loginInfo)); + + var autoLinkUser = BackOfficeIdentityUser.CreateNew( + loginInfo.Email, + loginInfo.Email, + autoLinkOptions.GetDefaultCulture(this.UmbracoContext, loginInfo)); + autoLinkUser.Name = loginInfo.ExternalIdentity.Name; + foreach (var userGroup in groups) { - ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + autoLinkUser.AddRole(userGroup.Alias); + } + + //call the callback if one is assigned + if (autoLinkOptions.OnAutoLinking != null) + { + autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); + } + + var userCreationResult = await this.UserManager.CreateAsync(autoLinkUser); + + if (userCreationResult.Succeeded == false) + { + this.ViewData[TokenExternalSignInError] = userCreationResult.Errors; } 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) + var linkResult = await this.UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + if (linkResult.Succeeded == false) { - ViewData[TokenExternalSignInError] = 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 }; + this.ViewData[TokenExternalSignInError] = linkResult.Errors; + + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await this.UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded == false) + { + //DOH! ... this isn't good, combine all errors to be shown + this.ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); + } } else { - if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); - if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); - - var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); - - var autoLinkUser = BackOfficeIdentityUser.CreateNew( - loginInfo.Email, - loginInfo.Email, - autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo)); - autoLinkUser.Name = loginInfo.ExternalIdentity.Name; - foreach (var userGroup in groups) - { - autoLinkUser.AddRole(userGroup.Alias); - } - - //call the callback if one is assigned - if (autoLinkOptions.OnAutoLinking != null) - { - autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); - } - - var userCreationResult = await UserManager.CreateAsync(autoLinkUser); - - if (userCreationResult.Succeeded == false) - { - ViewData[TokenExternalSignInError] = userCreationResult.Errors; - } - else - { - var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); - if (linkResult.Succeeded == false) - { - ViewData[TokenExternalSignInError] = 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 - ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); - } - } - else - { - //sign in - await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); - } - } + //sign in + await this.SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); } - } } - return true; - } - return false; + } + return true; } /// diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs index ab4f9482ad..b00e87e465 100644 --- a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -51,6 +51,12 @@ namespace Umbraco.Web.Security.Identity /// public Action OnAutoLinking { get; set; } + /// + /// A callback executed during every time a user authenticates using an external login. + /// returns a boolean indicating if sign in should continue or not. + /// + public Func OnExternalLogin { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use the overload specifying user groups instead")] public string GetDefaultUserType(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo)