diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 6961f5c915..ee8b54f92b 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -12,6 +12,16 @@ namespace Umbraco.Web { public static class OwinExtensions { + public static void SetExternalLoginProviderErrors(this IOwinContext owinContext, BackOfficeExternalLoginProviderErrors errors) + { + // TODO: Once this is set, we could use more custom middleware to set the cookie and redirect so it's not up to the + // oauth provider to handle all of this manually + owinContext.Set(errors); + } + + internal static BackOfficeExternalLoginProviderErrors GetExternalLoginProviderErrors(this IOwinContext owinContext) + => owinContext.Get(); + /// /// Gets the for the Umbraco back office cookie /// diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 0a3e57c4fd..1ff5416dac 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Web.Mvc; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; @@ -266,7 +267,7 @@ namespace Umbraco.Web.Security { //Then our custom middlewares app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, Current.UmbracoContextAccessor); - app.Use(typeof(FixWindowsAuthMiddlware)); + app.Use(typeof(FixWindowsAuthMiddlware)); } //Marks all of the above middlewares to execute on Authenticate @@ -378,6 +379,19 @@ namespace Umbraco.Web.Security return app; } + /// + /// Enable the back office to detect and handle errors registered with external login providers + /// + /// + /// + /// + public static IAppBuilder UseUmbracoBackOfficeExternalLoginErrors(this IAppBuilder app, PipelineStage stage = PipelineStage.Authorize) + { + app.Use(typeof(BackOfficeExternalLoginProviderErrorMiddlware)); + app.UseStageMarker(stage); + return app; + } + public static void SanitizeThreadCulture(this IAppBuilder app) { Thread.CurrentThread.SanitizeThreadCulture(); diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs new file mode 100644 index 0000000000..5a08fdf1cf --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Web.Mvc; +using Microsoft.Owin; +using Newtonsoft.Json; +using Umbraco.Core; + +namespace Umbraco.Web.Security +{ + internal class BackOfficeExternalLoginProviderErrorMiddlware : OwinMiddleware + { + public BackOfficeExternalLoginProviderErrorMiddlware(OwinMiddleware next) : base(next) + { + } + + public override async Task Invoke(IOwinContext context) + { + if (!context.Request.Uri.IsClientSideRequest()) + { + // check if we have any errors registered + var errors = context.GetExternalLoginProviderErrors(); + if (errors != null) + { + // this is pretty nasty to resolve this from the MVC service locator but that's all we can really work with since that is where it is + var tempDataProvider = DependencyResolver.Current.GetService(); + + // create a 'fake' controller context for temp data to work. we want to use temp data because it's self managing and we won't have to + // deal with resetting anything and plus it's configurable (by default uses session). better than creating a state manager ourselves. + var controllerContext = new ControllerContext( + context.TryGetHttpContext().Result, + new System.Web.Routing.RouteData(), + new EmptyController()); + + tempDataProvider.SaveTempData(controllerContext, new Dictionary + { + [ViewDataExtensions.TokenExternalSignInError] = errors + }); + } + } + + if (Next != null) + { + await Next.Invoke(context); + } + } + + private class EmptyController : ControllerBase + { + protected override void ExecuteCore() + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs new file mode 100644 index 0000000000..85807d511f --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Security +{ + public class BackOfficeExternalLoginProviderErrors + { + public BackOfficeExternalLoginProviderErrors(string authenticationType, IEnumerable errors) + { + AuthenticationType = authenticationType; + Errors = errors; + } + + public string AuthenticationType { get; } + public IEnumerable Errors { get; } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs index d964678e03..4cd43f3b2b 100644 --- a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs +++ b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs @@ -1,10 +1,12 @@ using Microsoft.Owin; using Microsoft.Owin.Security; using System; +using System.Collections; using System.Runtime.Serialization; namespace Umbraco.Web.Security { + /// /// Options used to configure back office external login providers /// @@ -45,5 +47,22 @@ namespace Umbraco.Web.Security /// provider's redirect settings will win. /// public bool AutoRedirectLoginToExternalProvider { get; set; } + + /// + /// A path (can be a virtual path) to a custom angular view that can be used to customize the login page UI + /// + /// + /// The types of customization used for this view are: + /// - Displaying external login provider errors + /// - Displaying extra information, links, etc... to the user when logging out + /// - Displaying extra information, links, etc... to the user when logging in (if auto-login redirect is disabled) + /// + public string BackOfficeCustomLoginView { get; set; } + + /// + /// If set to true then Umbraco will not automatically show any external login provider errors for this provider and instead will leave it up to the custom view + /// assigned to display any errors + /// + public bool BackOfficeCustomLoginViewHandlesErrors { get; set; } } } diff --git a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs index 30038e1f31..3338344e73 100644 --- a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs +++ b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Security; namespace Umbraco.Web.Security { + /// /// This is used to inspect the request to see if 2 x identities are assigned: A windows one and a back office one. /// When this is the case, it means that auth has executed for Windows & auth has executed for our back office cookie diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 73b56de757..a410b9c83d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -257,6 +257,8 @@ + + diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 0633cca3a0..105d2aff4c 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -99,7 +99,8 @@ namespace Umbraco.Web app .UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authenticate) .UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, PipelineStage.Authenticate) - .UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authorize); + .UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authorize) + .UseUmbracoBackOfficeExternalLoginErrors(); } public static event EventHandler MiddlewareConfigured; diff --git a/src/Umbraco.Web/ViewDataExtensions.cs b/src/Umbraco.Web/ViewDataExtensions.cs index e47171f1ba..84731d6b11 100644 --- a/src/Umbraco.Web/ViewDataExtensions.cs +++ b/src/Umbraco.Web/ViewDataExtensions.cs @@ -48,11 +48,21 @@ namespace Umbraco.Web viewData[TokenUmbracoBaseFolder] = value; } + /// + /// Used by the back office login screen to get any registered external login provider errors + /// + /// + /// public static IEnumerable GetExternalSignInError(this ViewDataDictionary viewData) { return (IEnumerable)viewData[TokenExternalSignInError]; } + /// + /// Used by the back office controller to register any external login provider errors + /// + /// + /// public static void SetExternalSignInError(this ViewDataDictionary viewData, IEnumerable value) { viewData[TokenExternalSignInError] = value;