using System; using System.Threading; using System.Web; using System.Web.Mvc; using System.Web.SessionState; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Extensions; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.DataHandler; using Microsoft.Owin.Security.DataProtection; using Owin; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security { /// /// Provides security/identity extension methods to IAppBuilder. /// public static class AppBuilderExtensions { /// /// Configure Default Identity User Manager for Umbraco /// /// /// /// /// /// public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, ServiceContext services, UmbracoMapper mapper, IContentSection contentSettings, IGlobalSettings globalSettings, MembershipProviderBase userMembershipProvider) { if (services == null) throw new ArgumentNullException(nameof(services)); if (userMembershipProvider == null) throw new ArgumentNullException(nameof(userMembershipProvider)); //Configure Umbraco user manager to be created per request app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( options, services.UserService, services.MemberTypeService, services.EntityService, services.ExternalLoginService, userMembershipProvider, mapper, contentSettings, globalSettings)); app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger())); } /// /// Configure a custom UserStore with the Identity User Manager for Umbraco /// /// /// /// /// /// /// public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, IRuntimeState runtimeState, IContentSection contentSettings, IGlobalSettings globalSettings, MembershipProviderBase userMembershipProvider, BackOfficeUserStore customUserStore) { if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); if (userMembershipProvider == null) throw new ArgumentNullException(nameof(userMembershipProvider)); if (customUserStore == null) throw new ArgumentNullException(nameof(customUserStore)); //Configure Umbraco user manager to be created per request app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( options, customUserStore, userMembershipProvider, contentSettings)); app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); } /// /// Configure a custom BackOfficeUserManager for Umbraco /// /// /// /// /// public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, IRuntimeState runtimeState, IGlobalSettings globalSettings, Func, IOwinContext, TManager> userManager) where TManager : BackOfficeUserManager where TUser : BackOfficeIdentityUser { if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); if (userManager == null) throw new ArgumentNullException(nameof(userManager)); //Configure Umbraco user manager to be created per request app.CreatePerOwinContext(userManager); app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext( (options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); } /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// /// /// /// /// /// /// /// /// /// By default this will be configured to execute on PipelineStage.Authenticate /// public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState,IUserService userService, IGlobalSettings globalSettings, ISecuritySection securitySection) { return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, userService, globalSettings, securitySection, PipelineStage.Authenticate); } /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// /// /// /// /// /// /// /// /// Configurable pipeline stage /// /// public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IUserService userService, IGlobalSettings globalSettings, ISecuritySection securitySection, PipelineStage stage) { //Create the default options and provider var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySection); authOptions.Provider = new BackOfficeCookieAuthenticationProvider(userService, runtimeState, globalSettings) { // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. OnValidateIdentity = context => { var identity = context.Identity; return SecurityStampValidator .OnValidateIdentity( TimeSpan.FromMinutes(30), async (manager, user) => { var regenerated = await manager.GenerateUserIdentityAsync(user); // Keep any custom claims from the original identity regenerated.MergeClaimsFromBackOfficeIdentity(identity); return regenerated; }, identity => identity.GetUserId())(context); } }; return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, globalSettings, securitySection, authOptions, stage); } /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// /// /// /// /// /// /// Custom auth cookie options can be specified to have more control over the cookie authentication logic /// /// Configurable pipeline stage /// /// public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, ISecuritySection securitySection, CookieAuthenticationOptions cookieOptions, PipelineStage stage) { if (app == null) throw new ArgumentNullException(nameof(app)); if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); if (cookieOptions == null) throw new ArgumentNullException(nameof(cookieOptions)); if (cookieOptions.Provider == null) throw new ArgumentNullException("cookieOptions.Provider cannot be null.", nameof(cookieOptions)); if (cookieOptions.Provider is BackOfficeCookieAuthenticationProvider == false) throw new ArgumentException($"cookieOptions.Provider must be of type {typeof(BackOfficeCookieAuthenticationProvider)}.", nameof(cookieOptions)); app.UseUmbracoBackOfficeCookieAuthenticationInternal(cookieOptions, runtimeState, stage); //don't apply if app is not ready if (runtimeState.Level != RuntimeLevel.Upgrade && runtimeState.Level != RuntimeLevel.Run) return app; var cookieAuthOptions = app.CreateUmbracoCookieAuthOptions( umbracoContextAccessor, globalSettings, runtimeState, securitySection, //This defines the explicit path read cookies from for this middleware new[] {$"{globalSettings.Path}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"}); cookieAuthOptions.Provider = cookieOptions.Provider; //This is a custom middleware, we need to return the user's remaining logged in seconds app.Use( cookieAuthOptions, Current.Configs.Global(), Current.Configs.Settings().Security, app.CreateLogger()); //This is required so that we can read the auth ticket format outside of this pipeline app.CreatePerOwinContext( (options, context) => new UmbracoAuthTicketDataProtector(cookieOptions.TicketDataFormat)); return app; } private static bool _markerSet = false; /// /// This registers the exact type of the user manager in owin so we can extract it /// when required in order to extract the user manager instance /// /// /// /// /// /// This is required because a developer can specify a custom user manager and due to generic types the key name will registered /// differently in the owin context /// private static void SetBackOfficeUserManagerType(this IAppBuilder app) where TManager : BackOfficeUserManager where TUser : BackOfficeIdentityUser { if (_markerSet) throw new InvalidOperationException("The back office user manager marker has already been set, only one back office user manager can be configured"); //on each request set the user manager getter - // this is required purely because Microsoft.Owin.IOwinContext is super inflexible with it's Get since it can only be // a generic strongly typed instance app.Use((context, func) => { context.Set(BackOfficeUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); return func(); }); } private static void UseUmbracoBackOfficeCookieAuthenticationInternal(this IAppBuilder app, CookieAuthenticationOptions options, IRuntimeState runtimeState, PipelineStage stage) { if (app == null) throw new ArgumentNullException(nameof(app)); if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); //First the normal cookie middleware app.Use(typeof(CookieAuthenticationMiddleware), app, options); //don't apply if app is not ready if (runtimeState.Level == RuntimeLevel.Upgrade || runtimeState.Level == RuntimeLevel.Run) { //Then our custom middlewares app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, Current.UmbracoContextAccessor); app.Use(typeof(FixWindowsAuthMiddlware)); } //Marks all of the above middlewares to execute on Authenticate app.UseStageMarker(stage); } /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct /// Umbraco back office configuration /// /// /// /// /// /// /// /// By default this will be configured to execute on PipelineStage.Authenticate /// public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState,IGlobalSettings globalSettings) { return app.UseUmbracoBackOfficeExternalCookieAuthentication(umbracoContextAccessor, runtimeState, globalSettings, PipelineStage.Authenticate); } /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct /// Umbraco back office configuration /// /// /// /// /// /// /// public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, PipelineStage stage) { if (app == null) throw new ArgumentNullException(nameof(app)); if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, AuthenticationMode = AuthenticationMode.Passive, CookieName = Constants.Security.BackOfficeExternalCookieName, ExpireTimeSpan = TimeSpan.FromMinutes(5), //Custom cookie manager so we can filter requests CookieManager = new BackOfficeCookieManager(umbracoContextAccessor, runtimeState, globalSettings), CookiePath = "/", CookieSecure = globalSettings.UseHttps ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest, CookieHttpOnly = true, CookieDomain = Current.Configs.Settings().Security.AuthCookieDomain }, stage); return app; } /// /// In order for preview to work this needs to be called /// /// /// /// /// /// /// /// /// This ensures that during a preview request that the back office use is also Authenticated and that the back office Identity /// is added as a secondary identity to the current IPrincipal so it can be used to Authorize the previewed document. /// /// /// By default this will be configured to execute on PipelineStage.PostAuthenticate /// public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, ISecuritySection securitySettings) { return app.UseUmbracoPreviewAuthentication(umbracoContextAccessor, runtimeState, globalSettings, securitySettings, PipelineStage.PostAuthenticate); } /// /// In order for preview to work this needs to be called /// /// /// /// /// /// /// /// /// /// This ensures that during a preview request that the back office use is also Authenticated and that the back office Identity /// is added as a secondary identity to the current IPrincipal so it can be used to Authorize the previewed document. /// public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, ISecuritySection securitySettings, PipelineStage stage) { if (runtimeState.Level != RuntimeLevel.Run) return app; var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings); app.Use(typeof(PreviewAuthenticationMiddleware), authOptions, Current.Configs.Global()); // This middleware must execute at least on PostAuthentication, by default it is on Authorize // The middleware needs to execute after the RoleManagerModule executes which is during PostAuthenticate, // currently I've had 100% success with ensuring this fires after RoleManagerModule even if this is set // to PostAuthenticate though not sure if that's always a guarantee so by default it's Authorize. if (stage < PipelineStage.PostAuthenticate) throw new InvalidOperationException("The stage specified for UseUmbracoPreviewAuthentication must be greater than or equal to " + PipelineStage.PostAuthenticate); app.UseStageMarker(stage); 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(); } /// /// Create the default umb cookie auth options /// /// /// /// /// /// /// /// public static UmbracoBackOfficeCookieAuthOptions CreateUmbracoCookieAuthOptions(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IGlobalSettings globalSettings, IRuntimeState runtimeState, ISecuritySection securitySettings, string[] explicitPaths = null) { //this is how aspnet wires up the default AuthenticationTicket protector so we'll use the same code var ticketDataFormat = new TicketDataFormat( app.CreateDataProtector(typeof (CookieAuthenticationMiddleware).FullName, Constants.Security.BackOfficeAuthenticationType, "v1")); var authOptions = new UmbracoBackOfficeCookieAuthOptions( explicitPaths, umbracoContextAccessor, securitySettings, globalSettings, runtimeState, ticketDataFormat); return authOptions; } } }